Added Studio hotload command

- /iris studio hotload and its refresh alias reload the open Studio pack on Bukkit
- Bukkit hotload refuses while Studio entry, pause or close is pending
- Modded servers register the same hotload and refresh commands
- Modded hotload reports busy while that Studio is already hotloading
- Hotload messages and help entries are localized in every language file
- Bukkit artifact verification reads the jar task output
- Multiplexor takes the Bukkit artifact from the packedJar task
- BukkitTestServer shares one-time registry setup across tests
- Tests and a gameplay scenario cover both hotload commands
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-28 13:32:33 -04:00
1 parent 300ecbfc8b
commit b3579e623a
37 files changed
+902 -52

No files matched your search

@@ -104,6 +104,7 @@ import java.util.function.Consumer;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.localization.BukkitCommandMessages;
import art.arcane.volmlib.util.localization.MessageArgument;
import art.arcane.volmlib.util.localization.LanguageAudience;
import art.arcane.iris.localization.BukkitCommandMessagesExtended;
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", descriptionKey = "iris.director.commandstudio.director.studio_commands")
public class CommandStudio implements DirectorExecutor {
@@ -203,6 +204,22 @@ public class CommandStudio implements DirectorExecutor {
}));
}
@Director(description = "Hotload the active Studio pack", descriptionKey = "iris.director.commandstudio.director.hotload_active_studio_pack", aliases = {"refresh"}, origin = DirectorOrigin.BOTH)
public void hotload() {
VolmitSender commandSender = sender();
IrisProject project = Iris.service(StudioSVC.class).getActiveProject();
PlatformChunkGenerator generator = project == null ? null : project.getActiveProvider();
if (generator == null) {
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_NO_OPEN_STUDIO_PROJECTS));
return;
}
UUID audience = LanguageAudience.current();
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_HOTLOADING));
generator.hotloadAsync().whenComplete((ignored, failure) ->
LanguageAudience.run(audience, () -> sendHotloadResult(commandSender, failure)));
}
@Director(description = "Toggle your Studio debug scoreboard", descriptionKey = "iris.director.commandstudio.director.toggle_your_studio_debug_scoreboard", aliases = {"board", "sidebar", "sb"}, origin = DirectorOrigin.PLAYER)
public void scoreboard() {
Player target = player();
@@ -642,6 +659,16 @@ public class CommandStudio implements DirectorExecutor {
sender().sendMessage(IrisLanguage.text(BukkitCommandMessagesExtended.COMMAND_STUDIO_DONE, MessageArgument.untrusted("value", report.getPath())));
}
private void sendHotloadResult(VolmitSender commandSender, Throwable failure) {
if (failure != null) {
Iris.reportError(failure);
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_HOTLOAD_FAILED,
MessageArgument.untrusted("value", String.valueOf(failure.getMessage()))));
return;
}
commandSender.sendMessage(IrisLanguage.text(BukkitCommandMessages.COMMAND_STUDIO_HOTLOADED));
}
private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) {
StudioSVC studioService = Iris.service(StudioSVC.class);
if (studioService != null && studioService.isProjectOpen()) {
@@ -0,0 +1,72 @@
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { setup, openStudio, closeStudio, sampleTerrain, readJson, writeJson,
logPosition, logSince } from './studio-support.mjs'
async function hotload(context, fixture, active, command) {
const before = await readJson(fixture.manifestPath(active.key))
const response = await context.command(command, /^Studio hotloaded\.$|^Studio hotload failed:/, 90000)
context.expect(response === 'Studio hotloaded.', 'The manual Studio command did not complete successfully', { command, response })
const after = await readJson(fixture.manifestPath(active.key))
context.expect(after.pendingActivationId === null, 'The manual hotload completed with a pending activation')
context.report.iris.manualHotloads.push({ command, response,
before: before.activeActivationId, after: after.activeActivationId })
return after
}
export default {
name: 'iris-studio-manual-hotload',
description: 'Refresh an open Studio through hotload and its refresh alias, verify terrain, and reject commands without a Studio.',
async run(context) {
const fixture = await setup(context)
const biomeFile = path.join(fixture.pack, 'biomes', 'flat.json')
const original = await readFile(biomeFile, 'utf8')
const logOffset = await logPosition(fixture)
const processId = (await context.observe()).processId
context.report.iris.manualHotloads = []
let active
try {
await context.step('Reject hotload without an open Studio', async () => {
await context.command('/iris studio hotload', /^No open studio projects\.$/, 10000)
})
await context.step('Open Studio and hotload an unchanged pack', async () => {
active = await openStudio(context, fixture)
await sampleTerrain(context, fixture, 0, fixture.state.material)
await hotload(context, fixture, active, '/iris studio hotload')
await sampleTerrain(context, fixture, 4096, fixture.state.material)
})
await context.step('Refresh an edited pack and inspect newly generated terrain', async () => {
const before = await readJson(fixture.manifestPath(active.key))
const biome = await readJson(biomeFile)
biome.name = 'Manual hotload terrain'
biome.layers[0].palette[0].block = 'minecraft:gold_block'
await writeJson(biomeFile, biome)
await hotload(context, fixture, active, '/iris studio refresh')
const after = await fixture.wait(async () => {
const manifest = await readJson(fixture.manifestPath(active.key))
return manifest.activeActivationId !== before.activeActivationId && manifest.pendingActivationId === null ? manifest : false
}, 'The edited Studio pack did not become active', 90000)
context.report.iris.activations.push({ before: before.activeActivationId,
after: after.activeActivationId, material: 'gold_block' })
await sampleTerrain(context, fixture, 16384, 'gold_block')
await sampleTerrain(context, fixture, 0, fixture.state.material)
})
await context.step('Close Studio and reject another refresh', async () => {
await closeStudio(context, fixture, active)
active = undefined
await context.command('/iris studio refresh', /^No open studio projects\.$/, 10000)
await context.command('/minecraft:seed', /Seed:/i, 10000)
})
context.expect((await context.observe()).processId === processId, 'The server restarted during manual Studio hotload')
const log = await logSince(fixture, logOffset)
context.expect(!/Studio generation update failed|Studio hotload failed|Unable to (capture terrain|save the terrain boundary)|ClosedChannelException|Chunk system error|server has stopped responding|could not acquire its engine runtime|Failed to list files/i.test(log),
'Manual Studio hotload emitted a generation, snapshot, storage, or watchdog failure')
} finally {
try {
if (active) await closeStudio(context, fixture, active)
} finally {
await writeFile(biomeFile, original)
}
}
}
}
@@ -0,0 +1,112 @@
package art.arcane.iris.command;
import art.arcane.iris.Iris;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
import art.arcane.iris.studio.StudioSVC;
import art.arcane.iris.studio.workspace.IrisProject;
import art.arcane.volmlib.util.director.annotations.Director;
import art.arcane.volmlib.util.localization.LanguageAudience;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.lang.reflect.Method;
import java.io.File;
import java.util.concurrent.CompletableFuture;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class CommandStudioHotloadTest {
@Test
public void hotloadAndRefreshReportWhenNoStudioIsOpen() throws Exception {
RecordingSender sender = new RecordingSender();
StudioSVC studio = mock(StudioSVC.class);
Method hotload = CommandStudio.class.getDeclaredMethod("hotload");
assertTrue(List.of(hotload.getAnnotation(Director.class).aliases()).contains("refresh"));
try (MockedStatic<Iris> iris = mockStatic(Iris.class)) {
iris.when(() -> Iris.service(StudioSVC.class)).thenReturn(studio);
hotload.invoke(new TestCommand(sender));
assertTrue(sender.messages.getLast(), sender.messages.getLast().contains("No open studio"));
}
}
@Test
public void completionFeedbackWaitsForTheHotload() {
assertCompletionFeedback(null, "Studio hotloaded.");
}
@Test
public void failedHotloadReportsFailureAndLogsTheCause() {
assertCompletionFeedback(new IllegalStateException("Invalid pack"), "Studio hotload failed: Invalid pack");
}
private void assertCompletionFeedback(Throwable failure, String expected) {
RecordingSender sender = new RecordingSender();
StudioSVC studio = mock(StudioSVC.class);
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
IrisProject project = new IrisProject(new File("tutorial"));
project.setActiveProvider(generator);
CompletableFuture<Void> pending = new CompletableFuture<>();
UUID audience = UUID.randomUUID();
when(studio.getActiveProject()).thenReturn(project);
when(generator.hotloadAsync()).thenReturn(pending);
try (MockedStatic<Iris> iris = mockStatic(Iris.class)) {
iris.when(() -> Iris.service(StudioSVC.class)).thenReturn(studio);
LanguageAudience.run(audience, () -> new TestCommand(sender).hotload());
assertNull(LanguageAudience.current());
verify(generator).hotloadAsync();
assertEquals(1, sender.messages.size());
assertTrue(sender.messages.getFirst().contains("Hotloading Studio"));
if (failure == null) {
pending.complete(null);
} else {
pending.completeExceptionally(failure);
iris.verify(() -> Iris.reportError(failure));
}
assertEquals(2, sender.messages.size());
assertTrue(sender.messages.getLast(), sender.messages.getLast().contains(expected));
assertEquals(List.of(audience, audience), sender.audiences);
assertNull(LanguageAudience.current());
}
}
private static final class TestCommand extends CommandStudio {
private final VolmitSender sender;
private TestCommand(VolmitSender sender) {
this.sender = sender;
}
@Override
public VolmitSender sender() {
return sender;
}
}
private static final class RecordingSender extends VolmitSender {
private final List<String> messages = new ArrayList<>();
private final List<UUID> audiences = new ArrayList<>();
private RecordingSender() {
super(null);
}
@Override
public void sendMessage(String message) {
messages.add(message);
audiences.add(LanguageAudience.current());
}
}
}
@@ -150,6 +150,7 @@ final class ModdedCommandHelp {
Entry.command("close", "", ModdedHelpMessages.COMMAND_CLOSE_CLOSE_THE_OPEN_STUDIO_DIMENSION_AND_DISCARD_ITS_WORLD, "x"),
Entry.command("tpstudio", "", ModdedHelpMessages.COMMAND_TPSTUDIO_TELEPORT_INTO_THE_OPEN_STUDIO_DIMENSION, "stp"),
Entry.command("status", "", ModdedHelpMessages.COMMAND_STATUS_SHOW_THE_OPEN_STUDIO_DIMENSION_AND_ITS_PACK),
Entry.command("hotload", "", ModdedHelpMessages.COMMAND_HOTLOAD_REFRESH_THE_OPEN_STUDIO, "refresh"),
Entry.command("noise", "[generator] [seed]", ModdedHelpMessages.COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY, "nmap"),
Entry.command("map", "", ModdedHelpMessages.COMMAND_MAP_OPEN_THE_VISION_MAP_GUI_ON_THE_SERVER_DISPLAY, "render"),
Entry.command("vscode", "[pack]", ModdedHelpMessages.COMMAND_VSCODE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK_AND_OPEN_IT_IN_YOUR, "vsc"),
@@ -49,6 +49,7 @@ import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.iris.modded.ModdedWorkspaceGenerator;
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
import art.arcane.iris.generation.concurrent.BurstExecutor;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.volmlib.util.collection.KMap;
@@ -85,6 +86,7 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.regex.Pattern;
@@ -92,6 +94,7 @@ import java.util.stream.Stream;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.iris.modded.localization.ModdedCommandMessages;
import art.arcane.volmlib.util.localization.LanguageAudience;
import art.arcane.volmlib.util.localization.MessageArgument;
public final class ModdedStudioCommands {
private static final Predicate<NativeCommandSource> GATE = NativeCommandRegistration.GAMEMASTERS;
@@ -152,6 +155,10 @@ public final class ModdedStudioCommands {
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> tpStudio(context.getSource()))));
root.then(NativeCommandRegistration.literal("status")
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> status(context.getSource()))));
root.then(NativeCommandRegistration.literal("hotload")
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> hotload(context.getSource()))));
root.then(NativeCommandRegistration.literal("refresh")
.executes(ModdedCommandTree.localized((CommandContext<NativeCommandSource> context) -> hotload(context.getSource()))));
root.then(workspaceTree("vscode", true));
root.then(workspaceTree("vsc", true));
root.then(workspaceTree("update", false));
@@ -653,6 +660,72 @@ public final class ModdedStudioCommands {
return transition;
}
private static int hotload(NativeCommandSource source) {
NativeProtocolPlayer player = source.player();
NativeModdedServer server = source.server();
UUID owner = player == null ? CONSOLE_OWNER : player.id();
UUID audience = source.playerId();
Executor feedback = task -> server.execute(() -> LanguageAudience.run(audience, task));
CompletableFuture<Void> transition = TRANSITIONS.submit(owner, () -> hotloadTransition(source, owner, feedback));
transition.whenComplete((ignored, failure) -> {
if (failure == null) {
return;
}
Throwable cause = unwrapFailure(failure);
ModdedIrisLog.error("Iris Studio hotload command failed", cause);
feedback.execute(() -> IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_FAILED,
MessageArgument.trusted("errorMessage", IrisLanguage.errorDetail(cause)))));
});
return 1;
}
private static CompletableFuture<Void> hotloadTransition(
NativeCommandSource source,
UUID owner,
Executor feedback
) {
return CompletableFuture.supplyAsync(() -> hotloadStudio(source, owner, feedback), feedback)
.thenCompose(request -> request);
}
private static CompletableFuture<Void> hotloadStudio(
NativeCommandSource source,
UUID owner,
Executor feedback
) {
String dimensionId = STUDIOS.get(owner);
if (dimensionId == null) {
IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN));
return CompletableFuture.completedFuture(null);
}
NativeWorld studio = ModdedDimensionManager.level(source.server(), dimensionId);
if (studio == null) {
STUDIOS.remove(owner, dimensionId);
IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_YOUR_STUDIO_DIMENSION_IS_NO_LONGER_LOADED_USE_IRIS_STUDIO));
return CompletableFuture.completedFuture(null);
}
Engine engine = IrisModdedCommands.engineFor(studio);
ModdedStudioHotloadService service = ModdedEngineBootstrap.services().service(ModdedStudioHotloadService.class);
if (engine == null || service == null) {
throw new IllegalStateException("The Studio engine or hotload service is unavailable.");
}
return service.requestHotload(dimensionId, engine)
.thenAcceptAsync(completed -> sendHotloadResult(source, completed), feedback);
}
private static void sendHotloadResult(NativeCommandSource source, boolean completed) {
if (completed) {
IrisModdedCommands.ok(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_COMPLETE));
} else {
IrisModdedCommands.fail(source, IrisLanguage.plain(
ModdedCommandMessages.MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_BUSY));
}
}
private static int status(NativeCommandSource source) {
NativeModdedServer server = source.server();
List<ModdedDimensionManager.Handle> handles = ModdedDimensionManager.handles();
@@ -1016,6 +1016,18 @@ public final class ModdedCommandMessages {
"iris.modded.moddedstudiocommands.studio_close_failed",
"Studio close failed: " + "{value}" + "{errorMessage}"
);
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_COMPLETE = TextKey.of(
"iris.modded.moddedstudiocommands.studio_hotload_complete",
"Studio hotloaded."
);
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_BUSY = TextKey.of(
"iris.modded.moddedstudiocommands.studio_hotload_busy",
"Studio is already checking or hotloading its pack. Try again after it finishes."
);
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_FAILED = TextKey.of(
"iris.modded.moddedstudiocommands.studio_hotload_failed",
"Studio hotload failed{errorMessage}"
);
public static final TextKey MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED = TextKey.of(
"iris.modded.moddedstudiocommands.studio_closed_was_evacuated_unloaded_its_region_data_deleted",
"Studio closed: " + "{dimensionId}" + " was evacuated, unloaded and its region data deleted."
@@ -1530,6 +1542,9 @@ public final class ModdedCommandMessages {
MODDED_STUDIO_COMMANDS_STUDIO_OPEN_NOW_RUNS_SEED_USE_IRIS_STUDIO_CLOSE_WHEN,
MODDED_STUDIO_COMMANDS_YOU_DO_NOT_HAVE_OPEN_STUDIO_USE_IRIS_STUDIO_OPEN,
MODDED_STUDIO_COMMANDS_STUDIO_CLOSE_FAILED,
MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_COMPLETE,
MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_BUSY,
MODDED_STUDIO_COMMANDS_STUDIO_HOTLOAD_FAILED,
MODDED_STUDIO_COMMANDS_STUDIO_CLOSED_WAS_EVACUATED_UNLOADED_ITS_REGION_DATA_DELETED,
MODDED_STUDIO_COMMANDS_NO_STUDIO_DIMENSIONS_ARE_CURRENTLY_OPEN,
MODDED_STUDIO_COMMANDS_ACTIVE_STUDIO_DIMENSION_S,
@@ -242,6 +242,10 @@ public final class ModdedHelpMessages {
"iris.modded.help.entry.command.status_studio",
"Show the open studio dimension and its pack"
);
public static final TextKey COMMAND_HOTLOAD_REFRESH_THE_OPEN_STUDIO = TextKey.of(
"iris.modded.help.entry.command.hotload_studio",
"Reload the open Studio pack for newly generated terrain"
);
public static final TextKey COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY = TextKey.of(
"iris.modded.help.entry.command.noise",
"Open the Noise Explorer GUI on the server display"
@@ -479,6 +483,7 @@ public final class ModdedHelpMessages {
COMMAND_CLOSE_CLOSE_THE_OPEN_STUDIO_DIMENSION_AND_DISCARD_ITS_WORLD,
COMMAND_TPSTUDIO_TELEPORT_INTO_THE_OPEN_STUDIO_DIMENSION,
COMMAND_STATUS_SHOW_THE_OPEN_STUDIO_DIMENSION_AND_ITS_PACK,
COMMAND_HOTLOAD_REFRESH_THE_OPEN_STUDIO,
COMMAND_NOISE_OPEN_THE_NOISE_EXPLORER_GUI_ON_THE_SERVER_DISPLAY,
COMMAND_MAP_OPEN_THE_VISION_MAP_GUI_ON_THE_SERVER_DISPLAY,
COMMAND_VSCODE_REGENERATE_THE_CODE_WORKSPACE_FOR_A_PACK_AND_OPEN_IT_IN_YOUR,
@@ -123,6 +123,33 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
watches.clear();
}
public CompletableFuture<Boolean> requestHotload(String dimensionId, Engine engine) {
if (!dimensionId.startsWith(STUDIO_DIMENSION_PREFIX) || !engine.isStudio()) {
return CompletableFuture.failedFuture(new IllegalArgumentException("Only Studio worlds can be hotloaded."));
}
ExecutorService active = executor;
if (active == null || active.isShutdown()) {
return CompletableFuture.failedFuture(new IllegalStateException("Iris Studio hotload is unavailable."));
}
Watch watch = watches.computeIfAbsent(dimensionId, (String key) -> new Watch());
if (!watch.busy.compareAndSet(false, true)) {
return CompletableFuture.completedFuture(false);
}
try {
return CompletableFuture.supplyAsync(() -> {
try {
hotload(dimensionId, engine);
return true;
} finally {
watch.busy.set(false);
}
}, active);
} catch (RejectedExecutionException rejected) {
watch.busy.set(false);
return CompletableFuture.failedFuture(rejected);
}
}
@Override
public void refreshWorkspace(Engine engine) {
writeWorkspace(engine, "workspace refresh");
@@ -254,7 +281,7 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
try {
active.execute(() -> {
try {
poll(dimensionId, watch, generator, engine);
poll(dimensionId, watch, engine);
} finally {
watch.busy.set(false);
}
@@ -332,14 +359,14 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
return isPregeneratorActive(engine);
}
private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) {
private void poll(String dimensionId, Watch watch, Engine engine) {
try {
if (watch.engine != engine) {
watch.close();
watch.engine = engine;
watch.folder = new ReactiveFolder(
engine.getData().getDataFolder(),
(KList<File> created, KList<File> changed, KList<File> deleted) -> hotload(dimensionId, generator, engine),
(KList<File> created, KList<File> changed, KList<File> deleted) -> hotload(dimensionId, engine),
new KList<>(".iob", ".json"),
new KList<>(".iris"),
new KList<>());
@@ -354,12 +381,12 @@ public final class ModdedStudioHotloadService implements ModdedTickableService,
}
}
private void hotload(String dimensionId, IrisModdedChunkGenerator generator, Engine engine) {
if (engine.isClosed()) {
return;
}
private void hotload(String dimensionId, Engine engine) {
long start = System.currentTimeMillis();
try {
if (engine.isClosed() || engine.isClosing()) {
throw new IllegalStateException("The Studio world is closing or closed.");
}
engine.hotloadSilently();
ModdedIrisLog.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
} catch (Throwable e) {
@@ -0,0 +1,113 @@
package art.arcane.iris.modded.command;
import art.arcane.iris.localization.IrisLanguage;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandSource;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeCommandText;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeModdedServer;
import art.arcane.volmlib.nativelib.minecraft26_2.modded.NativeProtocolPlayer;
import art.arcane.volmlib.util.localization.LanguageAudience;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ModdedStudioHotloadLanguageTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
private final UUID playerId = UUID.randomUUID();
private File dataFolder;
@BeforeClass
public static void bootstrap() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Before
public void setUp() throws Exception {
dataFolder = temporaryFolder.newFolder();
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
Files.writeString(dataFolder.toPath().resolve("languages/de_DE.toml"),
"\"iris.modded.moddedstudiocommands.you_do_not_have_open_studio_use_iris_studio_open\" = \"Kein Studio geöffnet.\"\n");
IrisLanguage.start();
IrisLanguage.selections().selectPlayer(playerId, "de_DE").get(5L, TimeUnit.SECONDS);
ModdedStudioCommands.clear();
}
@After
public void tearDown() {
ModdedStudioCommands.clear();
IrisLanguage.shutdown();
assertTrue(IrisLanguage.reload(dataFolder, "en_US"));
}
@Test
public void delayedHotloadFeedbackKeepsThePlayersLanguage() throws Exception {
assertEquals(List.of("Kein Studio geöffnet."), runCommand("studio hotload", true));
}
@Test
public void delayedConsoleRefreshUsesTheServerLanguage() throws Exception {
assertEquals(List.of("You do not have an open studio. Use /iris studio open <pack> first."),
runCommand("studio refresh", false));
}
private List<String> runCommand(String command, boolean playerCommand) throws Exception {
NativeModdedServer server = mock(NativeModdedServer.class);
List<Runnable> tasks = new ArrayList<>();
doAnswer(invocation -> {
tasks.add(invocation.getArgument(0));
return null;
}).when(server).execute(any(Runnable.class));
NativeCommandSource source = mock(NativeCommandSource.class);
when(source.server()).thenReturn(server);
when(source.isGameMaster()).thenReturn(true);
if (playerCommand) {
NativeProtocolPlayer player = mock(NativeProtocolPlayer.class);
when(player.id()).thenReturn(playerId);
when(source.player()).thenReturn(player);
when(source.playerId()).thenReturn(playerId);
}
List<String> messages = new ArrayList<>();
doAnswer(invocation -> {
NativeCommandText text = invocation.getArgument(0);
messages.add(text.getString());
return null;
}).when(source).sendFailure(any());
CommandDispatcher<NativeCommandSource> dispatcher = new CommandDispatcher<>();
dispatcher.register(ModdedStudioCommands.tree("studio"));
assertEquals(1, dispatcher.execute(command, source));
assertTrue(messages.isEmpty());
assertNull(LanguageAudience.current());
UUID ambientAudience = playerCommand ? UUID.randomUUID() : playerId;
try (LanguageAudience.Scope audience = LanguageAudience.open(ambientAudience)) {
while (!tasks.isEmpty()) {
tasks.removeFirst().run();
assertEquals(ambientAudience, LanguageAudience.current());
}
}
return messages;
}
}
@@ -1,5 +1,6 @@
package art.arcane.iris.modded.service;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.modded.ModdedScheduler;
import art.arcane.volmlib.util.collection.KList;
@@ -8,19 +9,130 @@ import org.mockito.InOrder;
import java.lang.reflect.Field;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ModdedStudioHotloadServiceTest {
@Test
public void manualHotloadRunsWithoutAFileChangeOnTheWorker() throws Exception {
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
Engine engine = studioEngine();
Thread caller = Thread.currentThread();
doAnswer(invocation -> {
assertNotSame(caller, Thread.currentThread());
return null;
}).when(engine).hotloadSilently();
service.onEnable();
try {
assertTrue(service.requestHotload("irisworldgen:studio_test", engine).get(5L, TimeUnit.SECONDS));
verify(engine).hotloadSilently();
} finally {
service.onDisable();
}
}
@Test
public void overlappingManualHotloadIsRejected() throws Exception {
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
Engine engine = studioEngine();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
doAnswer(invocation -> {
entered.countDown();
assertTrue(release.await(5L, TimeUnit.SECONDS));
return null;
}).when(engine).hotloadSilently();
service.onEnable();
try {
CompletableFuture<Boolean> first = service.requestHotload("irisworldgen:studio_test", engine);
assertTrue(entered.await(5L, TimeUnit.SECONDS));
assertFalse(service.requestHotload("irisworldgen:studio_test", engine).get(5L, TimeUnit.SECONDS));
release.countDown();
assertTrue(first.get(5L, TimeUnit.SECONDS));
verify(engine).hotloadSilently();
} finally {
release.countDown();
service.onDisable();
}
}
@Test
public void closingStudioIsNotHotloadedAfterWaitingInTheQueue() throws Exception {
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
Engine engine = studioEngine();
ExecutorService executor = Executors.newSingleThreadExecutor();
CountDownLatch release = new CountDownLatch(1);
executor.submit(() -> {
try {
assertTrue(release.await(5L, TimeUnit.SECONDS));
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException(failure);
}
});
setExecutor(service, executor);
try {
CompletableFuture<Boolean> requested = service.requestHotload("irisworldgen:studio_test", engine);
when(engine.isClosing()).thenReturn(true);
release.countDown();
assertThrows(ExecutionException.class, () -> requested.get(5L, TimeUnit.SECONDS));
verify(engine, never()).hotloadSilently();
} finally {
release.countDown();
service.onDisable();
}
}
@Test
public void manualHotloadRejectsProductionWorlds() throws Exception {
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
Engine engine = studioEngine();
service.onEnable();
try {
assertThrows(ExecutionException.class, () -> service.requestHotload("irisworldgen:production", engine)
.get(5L, TimeUnit.SECONDS));
when(engine.isStudio()).thenReturn(false);
assertThrows(ExecutionException.class, () -> service.requestHotload("irisworldgen:studio_test", engine)
.get(5L, TimeUnit.SECONDS));
verify(engine, never()).hotloadSilently();
} finally {
service.onDisable();
}
}
@Test
public void failedManualHotloadReleasesTheWatchForRetry() throws Exception {
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
Engine engine = studioEngine();
doThrow(new IllegalStateException("Invalid pack")).doNothing().when(engine).hotloadSilently();
service.onEnable();
try {
assertThrows(ExecutionException.class, () -> service.requestHotload("irisworldgen:studio_test", engine)
.get(5L, TimeUnit.SECONDS));
assertTrue(service.requestHotload("irisworldgen:studio_test", engine).get(5L, TimeUnit.SECONDS));
} finally {
service.onDisable();
}
}
@Test
public void shutdownDrainsWorkerBeforeAllowingRestart() throws Exception {
ModdedStudioHotloadService service = new ModdedStudioHotloadService();
@@ -102,4 +214,11 @@ public class ModdedStudioHotloadServiceTest {
field.set(service, executor);
}
private static Engine studioEngine() {
Engine engine = mock(Engine.class);
when(engine.isStudio()).thenReturn(true);
when(engine.getDimension()).thenReturn(new IrisDimension());
return engine;
}
}
+5 -5
View File
@@ -89,7 +89,6 @@ Closure<String> irisArtifactName = { String platform, String targetVersion ->
String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange)
Map<String, String> runtimeDownloadRelocations = (Map<String, String>) ext.runtimeDownloadRelocations
List<String> runtimeDownloadedPrefixes = (List<String>) ext.runtimeDownloadedPrefixes
File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile
apply plugin: ApiGenerator
List<String> nmsBindings = ['v26_2_R1', 'v26_3_R1']
@@ -542,19 +541,20 @@ List<String> requiredBukkitArtifactEntries = [
tasks.register('verifyBukkitArtifact') {
group = 'verification'
dependsOn('jar')
File artifact = bukkitArtifact
Provider<RegularFile> artifact = tasks.named('jar', Jar).flatMap { Jar task -> task.archiveFile }
inputs.file(artifact)
inputs.files(nativeRuntime)
inputs.files(commandHandlerSources)
inputs.files(mantleComponentSources)
doLast {
File archive = artifact.get().asFile
BukkitArtifactVerifier.verify(
artifact,
archive,
requiredBukkitArtifactEntries,
16,
runtimeDownloadedPrefixes)
NativeRuntimeArtifacts.verify(artifact, nativeRuntimeModules, embeddedNatives())
logger.lifecycle("Verified ${artifact.name} packaging and class reference graph")
NativeRuntimeArtifacts.verify(archive, nativeRuntimeModules, embeddedNatives())
logger.lifecycle("Verified ${archive.name} packaging and class reference graph")
}
}
@@ -138,6 +138,18 @@ public final class BukkitCommandMessages {
"iris.bukkit.commandstudio.closing_studio",
C.YELLOW + "Closing studio..."
);
public static final TextKey COMMAND_STUDIO_HOTLOADING = TextKey.of(
"iris.bukkit.commandstudio.hotloading",
C.YELLOW + "Hotloading Studio..."
);
public static final TextKey COMMAND_STUDIO_HOTLOADED = TextKey.of(
"iris.bukkit.commandstudio.hotloaded",
C.GREEN + "Studio hotloaded."
);
public static final TextKey COMMAND_STUDIO_HOTLOAD_FAILED = TextKey.of(
"iris.bukkit.commandstudio.hotload_failed",
C.RED + "Studio hotload failed: {value}"
);
public static final TextKey COMMAND_STUDIO_STUDIO_CLOSE_FAILED = TextKey.of(
"iris.bukkit.commandstudio.studio_close_failed",
C.RED + "Studio close failed: " + "{value}"
@@ -317,6 +329,9 @@ public final class BukkitCommandMessages {
COMMAND_STUDIO_TREES_OBJECTS_ARE_UNDER_OBJECTS_VANILLA_REFERENCE_THEM_FROM_BIOME,
COMMAND_STUDIO_NO_OPEN_STUDIO_PROJECTS,
COMMAND_STUDIO_CLOSING_STUDIO,
COMMAND_STUDIO_HOTLOADING,
COMMAND_STUDIO_HOTLOADED,
COMMAND_STUDIO_HOTLOAD_FAILED,
COMMAND_STUDIO_STUDIO_CLOSE_FAILED,
COMMAND_STUDIO_STUDIO_CLOSE_FAILED_2,
COMMAND_STUDIO_STUDIO_CLOSED_REMAINING_WORLD_FAMILY_CLEANUP_WAS_QUEUED_STARTUP_FALLBACK,
@@ -725,6 +725,10 @@ public final class DirectorCommandMessages {
"iris.director.commandstudio.param.dimension_open_vscode",
"The dimension to open VSCode for"
);
public static final TextKey COMMAND_STUDIO_DIRECTOR_HOTLOAD_ACTIVE_STUDIO_PACK = TextKey.of(
"iris.director.commandstudio.director.hotload_active_studio_pack",
"Hotload the active Studio pack"
);
public static final TextKey COMMAND_STUDIO_DIRECTOR_CLOSE_OPEN_STUDIO_PROJECT = TextKey.of(
"iris.director.commandstudio.director.close_open_studio_project",
"Close an open studio project"
@@ -1058,6 +1062,7 @@ public final class DirectorCommandMessages {
COMMAND_STUDIO_DIRECTOR_OPEN_VSCODE_DIMENSION,
COMMAND_STUDIO_PARAM_DIMENSION_OPEN_VSCODE,
COMMAND_STUDIO_DIRECTOR_CLOSE_OPEN_STUDIO_PROJECT,
COMMAND_STUDIO_DIRECTOR_HOTLOAD_ACTIVE_STUDIO_PACK,
COMMAND_STUDIO_DIRECTOR_TOGGLE_YOUR_STUDIO_DEBUG_SCOREBOARD,
COMMAND_STUDIO_DIRECTOR_CREATE_NEW_STUDIO_PROJECT,
COMMAND_STUDIO_PARAM_NAME_THIS_NEW_IRIS_PROJECT,
@@ -1091,6 +1091,24 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
});
}
@Override
public CompletableFuture<Void> hotloadAsync() {
if (!canHotloadStudio()) {
return CompletableFuture.failedFuture(new IllegalStateException("Studio is not ready for hotload."));
}
return withExclusiveControlFuture(() -> {
if (!canHotloadStudio()) {
throw new IllegalStateException("Studio is not ready for hotload.");
}
getEngine().hotload();
});
}
private boolean canHotloadStudio() {
return !initialEntryPending && !studioUpdatesPaused
&& shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive) && getEngine() != null;
}
private void hotloadFromWatcher() {
if (initialEntryPending || studioUpdatesPaused || !shouldRunStudioHotload(isStudio(), closing, jigsawStudioActive)) {
return;
@@ -44,6 +44,8 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
void close();
CompletableFuture<Void> hotloadAsync();
default CompletableFuture<Void> closeAsync() {
close();
return CompletableFuture.completedFuture(null);
@@ -345,6 +345,7 @@ success = "Einstellungen und Sprache {locale} wurden neu geladen."
failed = "Die Einstellungen wurden neu geladen, aber die Sprache {locale} wurde abgelehnt; {activeLocale} bleibt aktiv."
[iris.modded.help]
"entry.command.hotload_studio" = "Das geöffnete Studio-Pack für neu generiertes Gelände neu laden"
unknown_section = "Unbekannter Iris-Hilfeabschnitt: {section}"
back_hover = "Zurück zum {parent}."
command_parameter = "Befehlsparameter"
@@ -642,6 +643,9 @@ placed_pieces_at_your_location = "§a'{structure}' ({value} Teile) an deiner Pos
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eStudio wird im laufenden Betrieb neu geladen..."
hotloaded = "§aStudio wurde im laufenden Betrieb neu geladen."
hotload_failed = "§cFehler beim Neuladen von Studio: {value}"
importing_vanilla_content_into = "§aInhalte aus vanilla werden in §f{value}§a importiert..."
importvanilla_complete_objects_structures_written_failed = "§aimportvanilla abgeschlossen: §f{imported}§a objects/structures geschrieben, §f{failed}§a fehlgeschlagen."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Bäume und Objekte liegen unter objects/vanilla/...; referenziere sie in den Objektplatzierungen eines Bioms."
@@ -1018,6 +1022,9 @@ place_failed_partial_changes_recorded_undo = "Platzierung fehlgeschlagen: {value
placed_pieces_write_s_at_your_location_iris_object_undo = "'{key}' an deiner Position platziert ({value} Teile, {value2} Schreibvorgänge{tileNote}). Mit /iris object undo kannst du die Platzierung rückgängig machen."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio wurde im laufenden Betrieb neu geladen."
studio_hotload_busy = "Studio prüft sein Pack bereits oder lädt es neu. Versuche es erneut, sobald der Vorgang abgeschlossen ist."
studio_hotload_failed = "Fehler beim Neuladen von Studio{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Gib ein Dimensions-Pack an: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Noise Explorer wird auf dem Serverbildschirm geöffnet."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Diese Dimension wird nicht von Iris generiert. Führe /iris studio noise in einer Iris-Dimension aus, um Generatoren aufzulösen, oder lasse den Generatornamen weg."
@@ -1274,6 +1281,7 @@ replace_installed_types = "Installierte Packs: {value}"
"param.iris_structure_key_place" = "Der Irisstrukturschlüssel zum Platzieren"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Aktives Studio-Pack im laufenden Betrieb neu laden"
"director.studio_commands" = "Studio-Befehle"
"director.open_new_studio_world" = "Eröffne eine neue Studiowelt"
"param.dimension_pack_open_studio" = "Dimensions-Pack, für das ein Studio geöffnet werden soll"
@@ -345,6 +345,7 @@ success = "Se recargaron la configuración y el idioma {locale}."
failed = "Se recargó la configuración, pero se rechazó el idioma {locale}; se seguirá usando {activeLocale}."
[iris.modded.help]
"entry.command.hotload_studio" = "Recargar el pack abierto de Studio para el terreno que se genere a continuación"
unknown_section = "Sección de ayuda de Iris desconocida: {section}"
back_hover = "Volver a {parent}."
command_parameter = "Parámetro del comando"
@@ -642,6 +643,9 @@ placed_pieces_at_your_location = "§aSe colocó '{structure}' ({value} piezas) e
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eRecargando Studio en caliente..."
hotloaded = "§aStudio recargado en caliente."
hotload_failed = "§cError al recargar Studio en caliente: {value}"
importing_vanilla_content_into = "§aImportando contenido vanilla en §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§aimportvanilla completado: §f{imported}§a objects/structures escritos, §f{failed}§a fallidos."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Los árboles/objetos están en objects/vanilla/...; haz referencia a ellos desde las colocaciones de objetos del bioma."
@@ -1018,6 +1022,9 @@ place_failed_partial_changes_recorded_undo = "El lugar falló: {value} (cambios
placed_pieces_write_s_at_your_location_iris_object_undo = "Se colocó '{key}' ({value} piezas, {value2} escritura(s){tileNote}) en tu posición. Usa /iris object undo para revertirlo."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio recargado en caliente."
studio_hotload_busy = "Studio ya está comprobando o recargando su pack. Inténtalo de nuevo cuando termine."
studio_hotload_failed = "Error al recargar Studio en caliente{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Indica un pack de dimensión: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Abriendo Noise Explorer en la pantalla del servidor."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Esta dimensión no está generada por Iris; ejecuta /iris studio noise desde una dimensión de Iris para resolver generadores u omite el nombre del generador."
@@ -1274,6 +1281,7 @@ replace_installed_types = "Packs instalados: {value}"
"param.iris_structure_key_place" = "La clave de la estructura de Iris que se colocará"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Recargar en caliente el pack activo de Studio"
"director.studio_commands" = "Comandos de Studio"
"director.open_new_studio_world" = "Abrir un mundo de Studio nuevo"
"param.dimension_pack_open_studio" = "El pack de dimensión para el que se abrirá Studio"
@@ -350,6 +350,7 @@ success = "Asetukset ja kieli {locale} ladattiin uudelleen."
failed = "Asetukset ladattiin uudelleen, mutta kieli {locale} hylättiin; jatketaan kielen {activeLocale} käyttöä."
[iris.modded.help]
"entry.command.hotload_studio" = "Lataa avoin Studio-paketti uudelleen uutta maastoa varten"
unknown_section = "Tuntematon Iris Tukiosa: {section}"
back_hover = "Palaa {parent}."
command_parameter = "Komentoparametri"
@@ -647,6 +648,9 @@ placed_pieces_at_your_location = "§aPaikka{structure}' ({value} Palaset) sijain
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eLadataan Studiota uudelleen..."
hotloaded = "§aStudio ladattu uudelleen."
hotload_failed = "§cStudion uudelleenlataus epäonnistui: {value}"
importing_vanilla_content_into = "§aVanillapitoisuuden tuominen §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§atuontivanilja valmis: §f{imported}§a objects/structures kirjallinen §f{failed}§a Epäonnistui."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Puut/objektit ovat objects/vanilla/...; viite ne biome objektisijoituksista."
@@ -1023,6 +1027,9 @@ place_failed_partial_changes_recorded_undo = "Paikka epäonnistui: {value} (osit
placed_pieces_write_s_at_your_location_iris_object_undo = "Paikka{key}' ({value} palat, {value2} kirjoita(s){tileNote}) sijaintinne. /iris object undo Palauttaa."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio ladattu uudelleen."
studio_hotload_busy = "Studio tarkistaa tai lataa pakettiaan jo uudelleen. Yritä uudelleen, kun toiminto on valmis."
studio_hotload_failed = "Studion uudelleenlataus epäonnistui{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Antakaa mittapaketti: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Avataan Melu Explorer palvelimen näytöllä."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Tämä ulottuvuus ei synny Iris; juokse /iris studio noise alkaen Iris ulottuvuus ratkaista generaattoreita, tai jättää generaattorin nimi."
@@ -1279,6 +1286,7 @@ replace_installed_types = "Asennetut paketit: {value}"
"param.iris_structure_key_place" = "Iris rakenne avain sijoittaa"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Lataa aktiivinen Studio-paketti uudelleen"
"director.studio_commands" = "Studiokomennot"
"director.open_new_studio_world" = "Avaa uusi studiomaailma"
"param.dimension_pack_open_studio" = "Ulottuvuuspaketti studion avaamiseksi"
@@ -345,6 +345,7 @@ success = "Les paramètres et la langue {locale} ont été rechargés."
failed = "Les paramètres ont été rechargés, mais la langue {locale} a été refusée ; la langue {activeLocale} reste utilisée."
[iris.modded.help]
"entry.command.hotload_studio" = "Recharger le pack Studio ouvert pour les terrains générés ensuite"
unknown_section = "Section d'aide Iris inconnue : {section}"
back_hover = "Revenir à {parent}."
command_parameter = "Paramètre de commande"
@@ -642,6 +643,9 @@ placed_pieces_at_your_location = "§aLa structure '{structure}' ({value} pièces
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eRechargement à chaud de Studio..."
hotloaded = "§aStudio rechargé à chaud."
hotload_failed = "§cÉchec du rechargement à chaud de Studio: {value}"
importing_vanilla_content_into = "§aImportation du contenu vanilla dans §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§aimportvanilla terminé : §f{imported}§a objects/structures écrits, §f{failed}§a échecs."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Les arbres/objets se trouvent dans objects/vanilla/... ; référencez-les depuis les placements d'objets du biome."
@@ -1018,6 +1022,9 @@ place_failed_partial_changes_recorded_undo = "Lieu échoué: {value} (variations
placed_pieces_write_s_at_your_location_iris_object_undo = "'{key}' placé ({value} pièces, {value2} écriture(s){tileNote}) à votre position. Utilisez /iris object undo pour annuler."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio rechargé à chaud."
studio_hotload_busy = "Studio vérifie ou recharge déjà son pack. Réessayez une fois l’opération terminée."
studio_hotload_failed = "Échec du rechargement à chaud de Studio{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Indiquez un pack de dimension : /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Ouverture de Noise Explorer sur l'écran du serveur."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Cette dimension n'est pas générée par Iris ; exécutez /iris studio noise depuis une dimension Iris pour résoudre les générateurs, ou omettez le nom du générateur."
@@ -1274,6 +1281,7 @@ replace_installed_types = "Packs installés : {value}"
"param.iris_structure_key_place" = "La clé de la structure Iris à placer"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Recharger à chaud le pack Studio actif"
"director.studio_commands" = "Commandes Studio"
"director.open_new_studio_world" = "Ouvrir un nouveau monde Studio"
"param.dimension_pack_open_studio" = "Le pack de dimension pour lequel ouvrir Studio"
@@ -350,6 +350,7 @@ success = "ההגדרות והשפה {locale} נטענו מחדש."
failed = "ההגדרות נטענו מחדש, אך השפה {locale} נדחתה; ממשיכים עם {activeLocale}."
[iris.modded.help]
"entry.command.hotload_studio" = "טעינה מחדש של חבילת Studio הפתוחה עבור שטח שייווצר בהמשך"
unknown_section = "לא ידוע Iris עזרה סעיף: {section}"
back_hover = "חזרה {parent}."
command_parameter = "פרמטר פיקוד"
@@ -647,6 +648,9 @@ placed_pieces_at_your_location = "§aמקום \"{structure}' ({value} חתיכו
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eטוען מחדש את Studio..."
hotloaded = "§aStudio נטען מחדש."
hotload_failed = "§cהטעינה מחדש של Studio נכשלה: {value}"
importing_vanilla_content_into = "§aייבוא תוכן vanilla §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§aייבוא: §f{imported}§a objects/structures כתוב, §f{failed}§a נכשל."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7עצים/אובייקטים נמצאים תחת objects/vanilla/...; יש להפנות אליהם ממיקומי אובייקטים של biome."
@@ -1023,6 +1027,9 @@ place_failed_partial_changes_recorded_undo = "המקום נכשל: {value} (שי
placed_pieces_write_s_at_your_location_iris_object_undo = "מקום \"{key}' ({value} חתיכות, {value2} תגית(s){tileNote}) במיקום שלך. /iris object undo לחזור."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio נטען מחדש."
studio_hotload_busy = "Studio כבר בודק או טוען מחדש את החבילה שלו. יש לנסות שוב לאחר סיום הפעולה."
studio_hotload_failed = "הטעינה מחדש של Studio נכשלה{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "לספק ערכת מימד: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "פתח את כלי חיפוש רעש בתצוגת השרת."
this_dimension_is_not_generated_by_iris_run_iris_studio = "מימד זה אינו נוצר על ידי Irisלרוץ /iris studio noise מתוך A Iris ממד כדי לפתור גנרטורים, או להמיט את שם הגנרטור."
@@ -1279,6 +1286,7 @@ replace_installed_types = "חבילות מותקנות: {value}"
"param.iris_structure_key_place" = "מפתח המבנה של iris"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "טעינה מחדש של חבילת Studio הפעילה"
"director.studio_commands" = "פיקוד סטודיו"
"director.open_new_studio_world" = "פתח עולם אולפן חדש"
"param.dimension_pack_open_studio" = "חבילת המימד לפתוח סטודיו"
@@ -345,6 +345,7 @@ success = "Le impostazioni e la lingua {locale} sono state ricaricate."
failed = "Le impostazioni sono state ricaricate, ma la lingua {locale} è stata rifiutata; si continuerà a usare {activeLocale}."
[iris.modded.help]
"entry.command.hotload_studio" = "Ricarica il pacchetto Studio aperto per il terreno generato successivamente"
unknown_section = "Sezione della guida Iris sconosciuta: {section}"
back_hover = "Torna a {parent}."
command_parameter = "Parametro di comando"
@@ -642,6 +643,9 @@ placed_pieces_at_your_location = "§a'{structure}' ({value} pezzi) posizionata n
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eRicaricamento a caldo di Studio..."
hotloaded = "§aStudio ricaricato a caldo."
hotload_failed = "§cRicaricamento a caldo di Studio non riuscito: {value}"
importing_vanilla_content_into = "§aImportazione del contenuto di vanilla in §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§aimportvanilla completato: §f{imported}§a objects/structures scritti, §f{failed}§a non riusciti."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Gli alberi e gli oggetti si trovano in objects/vanilla/...; aggiungili ai posizionamenti degli oggetti del bioma."
@@ -1018,6 +1022,9 @@ place_failed_partial_changes_recorded_undo = "Posizionamento non riuscito: {valu
placed_pieces_write_s_at_your_location_iris_object_undo = "'{key}' posizionata nella tua posizione ({value} pezzi, {value2} scritture{tileNote}). Usa /iris object undo per annullare."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio ricaricato a caldo."
studio_hotload_busy = "Studio sta già verificando o ricaricando il suo pacchetto. Riprova al termine dell’operazione."
studio_hotload_failed = "Ricaricamento a caldo di Studio non riuscito{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Specifica un Pack di dimensione: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Aprire il Noise Explorer sul display del server."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Questa dimensione non è generata da Iris. Esegui /iris studio noise da una dimensione Iris per risolvere i generatori, oppure ometti il nome del generatore."
@@ -1274,6 +1281,7 @@ replace_installed_types = "Pack installati: {value}"
"param.iris_structure_key_place" = "La chiave della struttura di iris"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Ricarica a caldo il pacchetto Studio attivo"
"director.studio_commands" = "Comandi dello studio"
"director.open_new_studio_world" = "Aprire un nuovo mondo di studio"
"param.dimension_pack_open_studio" = "Il Pack di dimensione per cui aprire uno studio"
@@ -348,6 +348,7 @@ success = "設定と言語 {locale} を再読み込みしました。"
failed = "設定は再読み込みされましたが、言語 {locale} は拒否されました。引き続き {activeLocale} を使用します。"
[iris.modded.help]
"entry.command.hotload_studio" = "開いている Studio パックを再読み込みし、これから生成される地形に適用"
unknown_section = "不明な Iris ヘルプセクション: {section}"
back_hover = "{parent} に戻ります。"
command_parameter = "コマンドパラメーター"
@@ -645,6 +646,9 @@ placed_pieces_at_your_location = "§a現在位置に '{structure}'({value} ピ
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eStudio をホットリロード中..."
hotloaded = "§aStudio のホットリロードが完了しました。"
hotload_failed = "§cStudio のホットリロードに失敗しました: {value}"
importing_vanilla_content_into = "§aバニラコンテンツを §f{value}§a にインポートしています..."
importvanilla_complete_objects_structures_written_failed = "§aimportvanilla 完了: §f{imported}§a 件を objects/structures に書き込み、§f{failed}§a 件が失敗しました。"
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7木やオブジェクトは objects/vanilla/... にあります。バイオームのオブジェクト配置から参照してください。"
@@ -1021,6 +1025,9 @@ place_failed_partial_changes_recorded_undo = "配置に失敗しました: {valu
placed_pieces_write_s_at_your_location_iris_object_undo = "現在位置に '{key}' を配置しました({value} ピース、書き込み {value2}{tileNote})。元に戻すには /iris object undo を実行してください。"
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio のホットリロードが完了しました。"
studio_hotload_busy = "Studio はすでにパックを確認中またはホットリロード中です。完了してから再試行してください。"
studio_hotload_failed = "Studio のホットリロードに失敗しました{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "ディメンションパックを指定してください: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "サーバー画面でノイズエクスプローラーを開いています。"
this_dimension_is_not_generated_by_iris_run_iris_studio = "このディメンションは Iris によって生成されていません。ジェネレーターを解決するには Iris ディメンション内で /iris studio noise を実行するか、ジェネレーター名を省略してください。"
@@ -1277,6 +1284,7 @@ replace_installed_types = "インストール済みのパック: {value}"
"param.iris_structure_key_place" = "配置する Iris 構造物キー"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "アクティブな Studio パックをホットリロード"
"director.studio_commands" = "スタジオコマンド"
"director.open_new_studio_world" = "新しいスタジオワールドを開きます"
"param.dimension_pack_open_studio" = "スタジオを開くディメンションパック"
@@ -348,6 +348,7 @@ success = "설정과 {locale} 언어를 다시 불러왔습니다."
failed = "설정은 다시 불러왔지만 {locale} 언어가 거부되어 {activeLocale}을 계속 사용합니다."
[iris.modded.help]
"entry.command.hotload_studio" = "열려 있는 Studio 팩을 다시 불러와 새로 생성되는 지형에 적용"
unknown_section = "알 수 없는 Iris 도움말 섹션: {section}"
back_hover = "{parent}(으)로 돌아갑니다."
command_parameter = "명령어 매개변수"
@@ -645,6 +646,9 @@ placed_pieces_at_your_location = "§a장소 '{structure}' ({value} 당신의 위
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eStudio 핫 리로드 중..."
hotloaded = "§aStudio 핫 리로드가 완료되었습니다."
hotload_failed = "§cStudio 핫 리로드 실패: {value}"
importing_vanilla_content_into = "§avanilla 콘텐츠 가져 오기 §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§aimportvanilla 완전한: §f{imported}§a objects/structures 작성, §f{failed}§a 실패."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7나무/부대는 아래에 있습니다 objects/vanilla/...; biome 객체 배치에서 참조."
@@ -1021,6 +1025,9 @@ place_failed_partial_changes_recorded_undo = "장소 실패: {value} (불행의
placed_pieces_write_s_at_your_location_iris_object_undo = "장소 '{key}' ({value} 조각, {value2} 쓰기(s){tileNote}) 귀하의 위치에. /iris object undo 뒤로."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio 핫 리로드가 완료되었습니다."
studio_hotload_busy = "Studio가 이미 팩을 확인하거나 핫 리로드하고 있습니다. 완료된 후 다시 시도하세요."
studio_hotload_failed = "Studio 핫 리로드 실패{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "차원 팩을 제공: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "서버 디스플레이에서 소음 탐험 공구를 엽니다."
this_dimension_is_not_generated_by_iris_run_iris_studio = "이 차원은 Iris가 생성하지 않았습니다. 생성기를 확인하려면 Iris 차원에서 /iris studio noise를 실행하거나 생성기 이름을 생략하세요."
@@ -1277,6 +1284,7 @@ replace_installed_types = "설치된 팩: {value}"
"param.iris_structure_key_place" = "배치할 Iris 구조물 키"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "활성 Studio 팩 핫 리로드"
"director.studio_commands" = "Studio 명령어"
"director.open_new_studio_world" = "새 Studio 월드를 엽니다"
"param.dimension_pack_open_studio" = "Studio를 열 차원 팩"
@@ -350,6 +350,7 @@ success = "Nustatymai ir kalba {locale} įkelti iš naujo."
failed = "Nustatymai įkelti iš naujo, tačiau kalba {locale} atmesta; toliau naudojama {activeLocale}."
[iris.modded.help]
"entry.command.hotload_studio" = "Iš naujo įkelti atidarytą Studio paketą naujai generuojamam reljefui"
unknown_section = "Nežinoma Iris pagalbos skyrius: {section}"
back_hover = "Grįžti į {parent}."
command_parameter = "Komandų parametras"
@@ -647,6 +648,9 @@ placed_pieces_at_your_location = "§aPateikta \"{structure}' ({value} vienetų).
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eStudio įkeliama iš naujo..."
hotloaded = "§aStudio įkelta iš naujo."
hotload_failed = "§cNepavyko iš naujo įkelti Studio: {value}"
importing_vanilla_content_into = "§avanilla kiekio importavimas į §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§aimportvanilės komplektas: §f{imported}§a objects/structures raštu, §f{failed}§a Nepavyko."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Medžiai / objektai yra pagal objects/vanilla/...; nuoroda į juos iš biomo objekto vietas."
@@ -1023,6 +1027,9 @@ place_failed_partial_changes_recorded_undo = "Vieta nepavyko: {value} (daliniai
placed_pieces_write_s_at_your_location_iris_object_undo = "Pateikta \"{key}' ({value} vienetai, {value2} rašymas (-ai){tileNote}) jūsų vietoje. /iris object undo grįžti."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio įkelta iš naujo."
studio_hotload_busy = "Studio jau tikrina arba iš naujo įkelia savo paketą. Bandykite dar kartą, kai veiksmas bus baigtas."
studio_hotload_failed = "Nepavyko iš naujo įkelti Studio{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Pateikite matmenų paketą: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Atidaryti triukšmo naršyklę serverio ekrane."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Šis matmuo nėra generuojamas Iris; paleisti /iris studio noise iš Iris dimensija išspręsti generatorius, arba praleisti generatoriaus pavadinimą."
@@ -1279,6 +1286,7 @@ replace_installed_types = "Įdiegti paketai: {value}"
"param.iris_structure_key_place" = "Raktas į rainelės struktūrą"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Iš naujo įkelti aktyvų Studio paketą"
"director.studio_commands" = "Studijos komandos"
"director.open_new_studio_world" = "Atverti naują studijos pasaulį"
"param.dimension_pack_open_studio" = "Matmenų paketas, skirtas studijai atidaryti"
@@ -345,6 +345,7 @@ success = "De instellingen en taal {locale} zijn opnieuw geladen."
failed = "De instellingen zijn opnieuw geladen, maar taal {locale} is geweigerd; {activeLocale} blijft actief."
[iris.modded.help]
"entry.command.hotload_studio" = "Het geopende Studio-pakket opnieuw laden voor nieuw gegenereerd terrein"
unknown_section = "Onbekend Iris help sectie: {section}"
back_hover = "Terug naar {parent}."
command_parameter = "Opdrachtparameter"
@@ -642,6 +643,9 @@ placed_pieces_at_your_location = "§aGeplaatst '{structure}' ({value} stukken) o
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eStudio wordt opnieuw geladen..."
hotloaded = "§aStudio is opnieuw geladen."
hotload_failed = "§cOpnieuw laden van Studio mislukt: {value}"
importing_vanilla_content_into = "§aImporteren vanilla-inhoud in §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§aimportvanilla voltooid: §f{imported}§a objects/structures schriftelijk, §f{failed}§a mislukt."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Bomen/objecten onder objects/vanilla/...; referentie van biome object plaatsingen."
@@ -1018,6 +1022,9 @@ place_failed_partial_changes_recorded_undo = "Plaats mislukt: {value} (gedeeltel
placed_pieces_write_s_at_your_location_iris_object_undo = "Geplaatst '{key}' ({value} stukken, {value2} schrijven(s){tileNote}) op uw locatie. /iris object undo om terug te keren."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio is opnieuw geladen."
studio_hotload_busy = "Studio controleert of herlaadt het pakket al. Probeer het opnieuw zodra dit klaar is."
studio_hotload_failed = "Opnieuw laden van Studio mislukt{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Geef een maatpakket: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Het openen van de Noise Explorer op het serverscherm."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Deze dimensie wordt niet gegenereerd door Iris; rennen /iris studio noise van een Iris dimensie om generatoren op te lossen, of de naam van de generator weg te laten."
@@ -1274,6 +1281,7 @@ replace_installed_types = "Geïnstalleerde packs: {value}"
"param.iris_structure_key_place" = "De iris structuur sleutel om te plaatsen"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Het actieve Studio-pakket opnieuw laden"
"director.studio_commands" = "Studio-opdrachten"
"director.open_new_studio_world" = "Open een nieuwe studio wereld"
"param.dimension_pack_open_studio" = "Het dimensiepakket om een studio te openen voor"
@@ -350,6 +350,7 @@ success = "Ustawienia i język {locale} zostały ponownie wczytane."
failed = "Ustawienia zostały ponownie wczytane, ale język {locale} został odrzucony; nadal używany jest {activeLocale}."
[iris.modded.help]
"entry.command.hotload_studio" = "Ponownie wczytaj otwarty pakiet Studio dla nowo generowanego terenu"
unknown_section = "Nieznany Iris sekcja pomocnicza: {section}"
back_hover = "Powrót do {parent}."
command_parameter = "Parametr poleceń"
@@ -647,6 +648,9 @@ placed_pieces_at_your_location = "§aUmieszczone \"{structure}' ({value} sztuk)
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§ePonowne wczytywanie Studio..."
hotloaded = "§aStudio zostało ponownie wczytane."
hotload_failed = "§cPonowne wczytanie Studio nie powiodło się: {value}"
importing_vanilla_content_into = "§aImportowanie zawartości vanilla do §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§aimportanyl ukończono: §f{imported}§a objects/structures na piśmie, §f{failed}§a Nie udało się."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Drzewa / obiekty są pod objects/vanilla/...; odnosić się do nich z biomów obiektów."
@@ -1023,6 +1027,9 @@ place_failed_partial_changes_recorded_undo = "Miejsce nie powiodło się: {value
placed_pieces_write_s_at_your_location_iris_object_undo = "Umieszczone \"{key}' ({value} części, {value2} zapis (y){tileNote}) w twoim miejscu. /iris object undo do powrotu."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio zostało ponownie wczytane."
studio_hotload_busy = "Studio już sprawdza lub ponownie wczytuje swój pakiet. Spróbuj ponownie po zakończeniu tej operacji."
studio_hotload_failed = "Ponowne wczytanie Studio nie powiodło się{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Należy podać zestaw wymiarów: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Otwarcie Noise Explorer na wyświetlaczu serwera."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Ten wymiar nie jest generowany przez Iris; uruchomić /iris studio noise od Iris wymiar umożliwiający rozwiązanie generatorów lub pominięcie nazwy generatora."
@@ -1279,6 +1286,7 @@ replace_installed_types = "Zainstalowane paczki: {value}"
"param.iris_structure_key_place" = "Klucz struktury tęczówki do umieszczenia"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Ponownie wczytaj aktywny pakiet Studio"
"director.studio_commands" = "Polecenia studia"
"director.open_new_studio_world" = "Otwórz nowy świat studia"
"param.dimension_pack_open_studio" = "Pakiet wymiar otworzyć studio dla"
@@ -345,6 +345,7 @@ success = "As definições e o idioma {locale} foram recarregados."
failed = "As definições foram recarregadas, mas o idioma {locale} foi rejeitado; será mantido {activeLocale}."
[iris.modded.help]
"entry.command.hotload_studio" = "Recarregar o pacote aberto do Studio para o terreno gerado a partir de agora"
unknown_section = "Desconhecido Iris secção de ajuda: {section}"
back_hover = "Voltar para {parent}."
command_parameter = "Parâmetro de comando"
@@ -642,6 +643,9 @@ placed_pieces_at_your_location = "§aColocado '{structure}' ({value} peças) na
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eA recarregar o Studio..."
hotloaded = "§aStudio recarregado."
hotload_failed = "§cFalha ao recarregar o Studio: {value}"
importing_vanilla_content_into = "§aImportando conteúdo de vanilla para §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§aimportvanilla completa: §f{imported}§a objects/structures escrita, §f{failed}§a falhou."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Árvores/objectos estão abaixo objects/vanilla/...; referencia-os a partir de posicionamentos de objetos de bioma."
@@ -1018,6 +1022,9 @@ place_failed_partial_changes_recorded_undo = "O local falhou: {value} (alteraç
placed_pieces_write_s_at_your_location_iris_object_undo = "Colocado '{key}' ({value} peças, {value2} escrever( s){tileNote}) na sua localização. /iris object undo Reverter."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio recarregado."
studio_hotload_busy = "O Studio já está a verificar ou a recarregar o seu pacote. Tenta novamente quando terminar."
studio_hotload_failed = "Falha ao recarregar o Studio{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Fornecer um pacote de dimensões: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Abrindo o Explorador de Ruído na tela do servidor."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Esta dimensão não é gerada por Iris; executar /iris studio noise de uma Iris dimensão para resolver geradores, ou omitir o nome do gerador."
@@ -1274,6 +1281,7 @@ replace_installed_types = "Packs instalados: {value}"
"param.iris_structure_key_place" = "A chave de estrutura da íris para colocar"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Recarregar o pacote ativo do Studio"
"director.studio_commands" = "Comandos de Estúdio"
"director.open_new_studio_world" = "Abrir um novo mundo de estúdio"
"param.dimension_pack_open_studio" = "O pacote de dimensões para abrir um estúdio para"
@@ -350,6 +350,7 @@ success = "Настройки и язык {locale} были перезагруж
failed = "Настройки были перезагружены, но язык {locale} был отклонён; продолжает использоваться {activeLocale}."
[iris.modded.help]
"entry.command.hotload_studio" = "Перезагрузить открытый пак Studio для вновь генерируемой местности"
unknown_section = "неизвестный Iris Раздел помощи: {section}"
back_hover = "Вернуться в {parent}."
command_parameter = "Командный параметр"
@@ -647,6 +648,9 @@ placed_pieces_at_your_location = "§aПомещение '{structure}' ({value}
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eГорячая перезагрузка Studio..."
hotloaded = "§aГорячая перезагрузка Studio завершена."
hotload_failed = "§cОшибка горячей перезагрузки Studio: {value}"
importing_vanilla_content_into = "§aИмпорт vanilla контента §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§aИмпорт vanilla завершён: §f{imported}§a objects/structures записано, §f{failed}§a ошибок."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Деревья и объекты находятся под objects/vanilla/...; ссылаться на них из размещения объектов биома."
@@ -1023,6 +1027,9 @@ place_failed_partial_changes_recorded_undo = "Место провалилось:
placed_pieces_write_s_at_your_location_iris_object_undo = "Помещение '{key}' ({value} кусочки, {value2} писать(ы){tileNote}) в вашем месте. /iris object undo Чтобы вернуться."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Горячая перезагрузка Studio завершена."
studio_hotload_busy = "Studio уже проверяет или перезагружает свой пак. Повторите попытку после завершения операции."
studio_hotload_failed = "Ошибка горячей перезагрузки Studio{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Предоставьте размерный пакет: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Откройте Инструмент исследования шума на дисплее сервера."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Это измерение не генерируется Irisбегать /iris studio noise из одного Iris Размер для разрешения генераторов, или опустить имя генератора."
@@ -1279,6 +1286,7 @@ replace_installed_types = "Установленные паки: {value}"
"param.iris_structure_key_place" = "Структура радужной оболочки ключ к месту"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Горячая перезагрузка активного пака Studio"
"director.studio_commands" = "Студия команд"
"director.open_new_studio_world" = "Откройте новый студийный мир"
"param.dimension_pack_open_studio" = "Размерный пакет, чтобы открыть студию для"
@@ -350,6 +350,7 @@ success = "Ayarlar ve {locale} dili yeniden yüklendi."
failed = "Ayarlar yeniden yüklendi, ancak {locale} dili reddedildi; {activeLocale} kullanılmaya devam ediliyor."
[iris.modded.help]
"entry.command.hotload_studio" = "Yeni oluşturulacak arazi için açık Studio paketini yeniden yükle"
unknown_section = "Bilinmeyen Iris Yardım bölümü: {section}"
back_hover = "Geri dön {parent}."
command_parameter = "Komut parametresi"
@@ -647,6 +648,9 @@ placed_pieces_at_your_location = "§ayerleştirildi ‘{structure}' ({value} par
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eStudio yeniden yükleniyor..."
hotloaded = "§aStudio yeniden yüklendi."
hotload_failed = "§cStudio yeniden yüklenemedi: {value}"
importing_vanilla_content_into = "§avanilla içeriği ithal etmek §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§aimportvanilla tamamlandı: §f{imported}§a objects/structures yazılı, §f{failed}§a başarısız oldu."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Ağaçlar/objects altında objects/vanilla/...Onlara biyome nesne yerleştirmelerinden atıfta bulun."
@@ -1023,6 +1027,9 @@ place_failed_partial_changes_recorded_undo = "Yer başarısız oldu: {value} (Re
placed_pieces_write_s_at_your_location_iris_object_undo = "yerleştirildi ‘{key}' ({value} parçalar, {value2} Yaz(s){tileNote}) Senin konumunuzda. /iris object undo iptal etmek için."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio yeniden yüklendi."
studio_hotload_busy = "Studio paketini zaten denetliyor veya yeniden yüklüyor. İşlem tamamlandıktan sonra tekrar deneyin."
studio_hotload_failed = "Studio yeniden yüklenemedi{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Bir boyut paketi sağlayın: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Gürültü Explorer'ı sunucu ekranında açın."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Bu boyut tarafından oluşturulmuyor Iris; koşmak /iris studio noise Birinden Iris Jeneratörleri çözmek için boyut veya jeneratör adını omit."
@@ -1279,6 +1286,7 @@ replace_installed_types = "Yüklü paketler: {value}"
"param.iris_structure_key_place" = "iris yapı anahtarı yer için"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Etkin Studio paketini yeniden yükle"
"director.studio_commands" = "Stüdyo Komutları"
"director.open_new_studio_world" = "Yeni bir stüdyo dünyası açın"
"param.dimension_pack_open_studio" = "Bir stüdyo açmak için boyut paketi"
@@ -348,6 +348,7 @@ success = "Đã tải lại cài đặt và ngôn ngữ {locale}."
failed = "Đã tải lại cài đặt, nhưng ngôn ngữ {locale} bị từ chối; tiếp tục sử dụng {activeLocale}."
[iris.modded.help]
"entry.command.hotload_studio" = "Tải lại gói Studio đang mở để áp dụng cho địa hình được tạo tiếp theo"
unknown_section = "Không rõ Iris Phần trợ giúp: {section}"
back_hover = "Trở về {parent}."
command_parameter = "Tham số lệnh"
@@ -645,6 +646,9 @@ placed_pieces_at_your_location = "§aĐã đặt '{structure}' ({value} Các m
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§eĐang tải lại Studio..."
hotloaded = "§aĐã tải lại Studio."
hotload_failed = "§cTải lại Studio thất bại: {value}"
importing_vanilla_content_into = "§aNhập nội dung vanilla vào §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§ađã nhập xong: §f{imported}§a objects/structures Đã viết, §f{failed}§a bị lỗi."
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7Cây objects/vanilla/...; tham khảo chúng từ vị trí vật thể sinh học."
@@ -1021,6 +1025,9 @@ place_failed_partial_changes_recorded_undo = "Nơi bị lỗi: {value} (các tha
placed_pieces_write_s_at_your_location_iris_object_undo = "Đã đặt '{key}' ({value} Từng mảnh, {value2} ghi (s){tileNote}) tại địa điểm của bạn. /iris object undo để trở lại."
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Đã tải lại Studio."
studio_hotload_busy = "Studio đang kiểm tra hoặc tải lại gói của mình. Hãy thử lại sau khi hoàn tất."
studio_hotload_failed = "Tải lại Studio thất bại{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "Cung cấp một gói chiều: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "Mở hệ thống nhiễu trên màn hình máy chủ."
this_dimension_is_not_generated_by_iris_run_iris_studio = "Kích thước này không được tạo ra bởi Iris; chạy /iris studio noise từ một Iris Kích thước để giải quyết trình tạo, hoặc bỏ tên máy phát."
@@ -1277,6 +1284,7 @@ replace_installed_types = "Các gói đã cài đặt: {value}"
"param.iris_structure_key_place" = "Name"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "Tải lại gói Studio đang hoạt động"
"director.studio_commands" = "Lệnh phòng thu"
"director.open_new_studio_world" = "Mở một phòng thu mới"
"param.dimension_pack_open_studio" = "Name"
@@ -348,6 +348,7 @@ success = "设置和语言 {locale} 已重新加载。"
failed = "设置已重新加载,但语言 {locale} 验证失败;继续使用 {activeLocale}。"
[iris.modded.help]
"entry.command.hotload_studio" = "重新加载已打开的 Studio 包,应用于之后生成的地形"
unknown_section = "未知的 Iris 帮助章节:{section}"
back_hover = "返回 {parent}。"
command_parameter = "命令参数"
@@ -645,6 +646,9 @@ placed_pieces_at_your_location = "§a已放置 '{structure}' ({value}在您的
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§e正在热重载 Studio..."
hotloaded = "§aStudio 热重载完成。"
hotload_failed = "§cStudio 热重载失败: {value}"
importing_vanilla_content_into = "§a正在将原版内容导入 §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§a原版内容导入完成:已写入 §f{imported}§a 个 objects/structures,§f{failed}§a 个失败。"
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7树和对象位于 objects/vanilla/...;请从群系的对象放置中引用它们。"
@@ -1021,6 +1025,9 @@ place_failed_partial_changes_recorded_undo = "位置失败 : {value} (部分更
placed_pieces_write_s_at_your_location_iris_object_undo = "已放置 '{key}' ({value}块,{value2}写入{tileNote}在你的地点。 /iris object undo将返回。"
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio 热重载完成。"
studio_hotload_busy = "Studio 正在检查或热重载其包,请在完成后重试。"
studio_hotload_failed = "Studio 热重载失败{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "提供一个维度包: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "在服务器显示上打开噪音探索器 。"
this_dimension_is_not_generated_by_iris_run_iris_studio = "此维度不是由 Iris; 运行 /iris studio noise 从 a Iris 以解析生成器,或省略生成器名称。"
@@ -1277,6 +1284,7 @@ replace_installed_types = "已安装的包:{value}"
"param.iris_structure_key_place" = "要放置的Iris结构键"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "热重载当前启用的 Studio 包"
"director.studio_commands" = "Studio 命令"
"director.open_new_studio_world" = "打开新的 Studio 世界"
"param.dimension_pack_open_studio" = "要在 Studio 中打开的维度包"
@@ -348,6 +348,7 @@ success = "設定和語言 {locale} 已重新載入。"
failed = "設定已重新載入,但語言 {locale} 驗證失敗;繼續使用 {activeLocale}。"
[iris.modded.help]
"entry.command.hotload_studio" = "重新載入已開啟的 Studio 套件,套用至之後生成的地形"
unknown_section = "未知的 Iris 說明章節:{section}"
back_hover = "返回 {parent}。"
command_parameter = "命令參數"
@@ -645,6 +646,9 @@ placed_pieces_at_your_location = "§a已放置 '{structure}' ({value}在您的
placement_changed_no_blocks = "§cStructure '{structure}' assembled {value} pieces but changed 0 blocks at your location. Check that the selected variants contain non-air blocks and that the placement is above the world's minimum height."
[iris.bukkit.commandstudio]
hotloading = "§e正在熱重載 Studio..."
hotloaded = "§aStudio 熱重載完成。"
hotload_failed = "§cStudio 熱重載失敗: {value}"
importing_vanilla_content_into = "§a正在將原版內容匯入 §f{value}§a..."
importvanilla_complete_objects_structures_written_failed = "§a原版內容匯入完成:已寫入 §f{imported}§a 個 objects/structures,§f{failed}§a 個失敗。"
trees_objects_are_under_objects_vanilla_reference_them_from_biome = "§7樹木和物件位於 objects/vanilla/...;請從生態域的物件放置中引用它們。"
@@ -1021,6 +1025,9 @@ place_failed_partial_changes_recorded_undo = "位置失敗 : {value} (部分更
placed_pieces_write_s_at_your_location_iris_object_undo = "已放置 '{key}' ({value}塊,{value2}寫入{tileNote}在你的地點。 /iris object undo將返回。"
[iris.modded.moddedstudiocommands]
studio_hotload_complete = "Studio 熱重載完成。"
studio_hotload_busy = "Studio 正在檢查或熱重載其套件,請在完成後重試。"
studio_hotload_failed = "Studio 熱重載失敗{errorMessage}"
provide_dimension_pack_iris_studio_open_pack_seed = "提供一個維度包: /iris studio open <pack> [seed]"
opening_noise_explorer_on_server_display = "在伺服器顯示上開啟噪音探索器 。"
this_dimension_is_not_generated_by_iris_run_iris_studio = "此維度不是由 Iris; 執行 /iris studio noise 從 a Iris 以解析生成器,或省略生成器名稱。"
@@ -1277,6 +1284,7 @@ replace_installed_types = "已安裝的包:{value}"
"param.iris_structure_key_place" = "要放置的Iris結構鍵"
[iris.director.commandstudio]
"director.hotload_active_studio_pack" = "熱重載目前啟用的 Studio 套件"
"director.studio_commands" = "Studio 命令"
"director.open_new_studio_world" = "開啟新的 Studio 世界"
"param.dimension_pack_open_studio" = "要在 Studio 中開啟的維度包"
@@ -10,6 +10,7 @@ import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.ResourceLoader;
import art.arcane.iris.testsupport.BukkitTestServer;
import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.nativelib.terrain.NativeBiomeRegistry;
@@ -17,6 +18,7 @@ import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourcePolicy;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.reflect.Field;
@@ -35,6 +37,11 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class BukkitBiomeLocateTest {
@BeforeClass
public static void initializeBukkit() {
BukkitTestServer.initializeRegistries();
}
@Test
public void locateResolvesVisibleBiomesOnlyInColumnsThatCanHoldTheTarget() throws Exception {
IrisBiome isles = new IrisBiome().setCustomDerivitives(new KList<>(new IrisBiomeCustom().setId("mirage")));
@@ -0,0 +1,106 @@
package art.arcane.iris.platform.generation;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.platform.generation.BukkitChunkGenerator.GenerationStageGate;
import art.arcane.iris.world.task.J;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
public class BukkitChunkGeneratorManualHotloadTest {
@Test
public void manualHotloadCompletesAfterExclusiveEngineRefresh() throws Exception {
Fixture fixture = new Fixture();
try (MockedStatic<J> scheduling = fixture.captureScheduling()) {
CompletableFuture<Void> result = fixture.generator.hotloadAsync();
assertFalse(result.isDone());
verify(fixture.engine, never()).hotload();
fixture.queued.get().run();
result.join();
verify(fixture.engine).hotload();
}
}
@Test
public void manualHotloadRejectsUnavailableStudios() throws Exception {
for (String state : new String[]{"initialEntryPending", "studioUpdatesPaused", "closing", "jigsawStudioActive"}) {
Fixture fixture = new Fixture();
setField(fixture.generator, state, true);
assertTrue(state, fixture.generator.hotloadAsync().isCompletedExceptionally());
verify(fixture.engine, never()).hotload();
}
Fixture fixture = new Fixture();
setField(fixture.generator, "studio", false);
assertTrue(fixture.generator.hotloadAsync().isCompletedExceptionally());
verify(fixture.engine, never()).hotload();
}
@Test
public void queuedHotloadFailsWhenStudioClosesBeforeExecution() throws Exception {
Fixture fixture = new Fixture();
try (MockedStatic<J> scheduling = fixture.captureScheduling()) {
CompletableFuture<Void> result = fixture.generator.hotloadAsync();
setField(fixture.generator, "closing", true);
fixture.queued.get().run();
assertTrue(result.isCompletedExceptionally());
verify(fixture.engine, never()).hotload();
}
}
@Test
public void failedHotloadReleasesControlForTheNextRequest() throws Exception {
Fixture fixture = new Fixture();
doThrow(new IllegalStateException("Invalid pack")).doNothing().when(fixture.engine).hotload();
try (MockedStatic<J> scheduling = fixture.captureScheduling()) {
CompletableFuture<Void> failed = fixture.generator.hotloadAsync();
fixture.queued.get().run();
assertTrue(failed.isCompletedExceptionally());
CompletableFuture<Void> retried = fixture.generator.hotloadAsync();
fixture.queued.get().run();
retried.join();
}
}
private static void setField(BukkitChunkGenerator generator, String name, Object value) throws Exception {
Field field = BukkitChunkGenerator.class.getDeclaredField(name);
field.setAccessible(true);
field.set(generator, value);
}
private static final class Fixture {
private final Engine engine = mock(Engine.class);
private final BukkitChunkGenerator generator = mock(BukkitChunkGenerator.class, CALLS_REAL_METHODS);
private final AtomicReference<Runnable> queued = new AtomicReference<>();
private Fixture() throws Exception {
generator.setEngine(engine);
setField(generator, "studio", true);
setField(generator, "loadLock", new GenerationStageGate(1, () -> false));
}
private MockedStatic<J> captureScheduling() {
MockedStatic<J> scheduling = mockStatic(J.class);
scheduling.when(() -> J.a(any(Runnable.class))).thenAnswer(invocation -> {
queued.set(invocation.getArgument(0));
return null;
});
return scheduling;
}
}
}
@@ -14,14 +14,7 @@ import art.arcane.iris.world.IrisWorld;
import art.arcane.iris.generation.block.TileData;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.iris.world.storage.matter.PreObjectMatterTest;
import art.arcane.volmlib.util.collection.KMap;
import io.papermc.paper.InternalAPIBridge;
import io.papermc.paper.registry.RegistryAccess;
import net.kyori.adventure.key.Key;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.block.Biome;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
@@ -34,7 +27,6 @@ import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
@@ -48,7 +40,6 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.RETURNS_DEFAULTS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -60,23 +51,8 @@ public class ObjectStudioGeometryTest {
@BeforeClass
public static void initializeBukkit() throws Exception {
PreObjectMatterTest.setUpBukkit();
RegistryAccess access = BukkitTestServer.registryAccess();
InternalAPIBridge bridge = mock(InternalAPIBridge.class, invocation -> {
if (invocation.getMethod().getName().equals("constructLegacyCustomBiome")) {
return biome(Key.key("minecraft:custom"));
}
return RETURNS_DEFAULTS.answer(invocation);
});
try (MockedStatic<RegistryAccess> registries = mockStatic(RegistryAccess.class);
MockedStatic<InternalAPIBridge> internals = mockStatic(InternalAPIBridge.class)) {
registries.when(RegistryAccess::registryAccess).thenReturn(access);
internals.when(InternalAPIBridge::get).thenReturn(bridge);
ClassLoader loader = ObjectStudioGenerator.class.getClassLoader();
Class.forName(Registry.class.getName(), true, loader);
Class.forName(Biome.class.getName(), true, loader);
Class.forName(ObjectStudioGenerator.class.getName(), true, loader);
}
BukkitTestServer.initializeRegistries();
Class.forName(ObjectStudioGenerator.class.getName(), true, ObjectStudioGenerator.class.getClassLoader());
}
@@ -283,18 +259,6 @@ public class ObjectStudioGeometryTest {
}
}
private static Biome biome(Key key) {
NamespacedKey namespaced = new NamespacedKey(key.namespace(), key.value());
return (Biome) Proxy.newProxyInstance(Biome.class.getClassLoader(), new Class<?>[]{Biome.class},
(proxy, method, arguments) -> switch (method.getName()) {
case "getKey" -> namespaced;
case "hashCode" -> namespaced.hashCode();
case "equals" -> proxy == arguments[0];
case "toString", "name" -> key.value();
default -> null;
});
}
private record Position(int x, int y, int z) {
}
@@ -1,6 +1,7 @@
package art.arcane.iris.testsupport;
import io.papermc.paper.InternalAPIBridge;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import net.kyori.adventure.key.Key;
@@ -16,6 +17,7 @@ import org.bukkit.plugin.PluginManager;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.mockito.MockedStatic;
import org.mockito.invocation.InvocationOnMock;
import java.lang.reflect.Proxy;
@@ -30,6 +32,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.RETURNS_DEFAULTS;
public final class BukkitTestServer {
@@ -37,6 +40,7 @@ public final class BukkitTestServer {
private static final String DEFAULT_VERSION = "1.0";
private static final Object LOCK = new Object();
private static volatile Server installed;
private static volatile boolean registriesInitialized;
private BukkitTestServer() {
}
@@ -82,6 +86,36 @@ public final class BukkitTestServer {
}
}
public static void initializeRegistries() {
if (registriesInitialized) {
return;
}
synchronized (LOCK) {
if (registriesInitialized) {
return;
}
install();
RegistryAccess access = registryAccess();
InternalAPIBridge bridge = mock(InternalAPIBridge.class, invocation ->
invocation.getMethod().getName().equals("constructLegacyCustomBiome")
? biome(Key.key("minecraft:custom"))
: RETURNS_DEFAULTS.answer(invocation));
try (MockedStatic<RegistryAccess> registries = mockStatic(RegistryAccess.class);
MockedStatic<InternalAPIBridge> internals = mockStatic(InternalAPIBridge.class)) {
registries.when(RegistryAccess::registryAccess).thenReturn(access);
internals.when(InternalAPIBridge::get).thenReturn(bridge);
ClassLoader loader = BukkitTestServer.class.getClassLoader();
Class.forName(Registry.class.getName(), true, loader);
Class.forName(Biome.class.getName(), true, loader);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Bukkit registries are missing from the test classpath", e);
}
registriesInitialized = true;
}
}
public static RegistryAccess registryAccess() {
Map<Object, Object> registries = new HashMap<>();
return mock(RegistryAccess.class, invocation -> {
+1 -2
View File
@@ -34,8 +34,7 @@ project.ext.multiplexor = [
]
gradle.projectsEvaluated {
File bukkitSource = tasks.named('jar', Jar).get().archiveFile.get().asFile
File bukkitArtifact = new File(bukkitSource.parentFile, bukkitSource.name.replaceFirst(/\.jar$/, '-packed.jar'))
File bukkitArtifact = tasks.named('packedJar').get().outputJar.get().asFile
File apiArtifact = tasks.named('irisApi', GenerateApiTask).get().outputFile.locationOnly.get().asFile
File spiArtifact = project(':spi').tasks.named('jar', Jar).get().archiveFile.locationOnly.get().asFile
buildTargets.addAll([