From b3579e623a6a73bd7b4ed4f25acbfc5dd2bd8b77 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 28 Oct 2026 13:32:33 -0400 Subject: [PATCH] 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 --- .../arcane/iris/command/CommandStudio.java | 27 ++++ .../test/gameplay/studio-manual-hotload.mjs | 72 +++++++++++ .../command/CommandStudioHotloadTest.java | 112 +++++++++++++++++ .../modded/command/ModdedCommandHelp.java | 1 + .../modded/command/ModdedStudioCommands.java | 73 +++++++++++ .../localization/ModdedCommandMessages.java | 15 +++ .../localization/ModdedHelpMessages.java | 5 + .../service/ModdedStudioHotloadService.java | 41 ++++-- .../ModdedStudioHotloadLanguageTest.java | 113 +++++++++++++++++ .../ModdedStudioHotloadServiceTest.java | 119 ++++++++++++++++++ build.gradle | 10 +- .../localization/BukkitCommandMessages.java | 15 +++ .../localization/DirectorCommandMessages.java | 5 + .../generation/BukkitChunkGenerator.java | 18 +++ .../generation/PlatformChunkGenerator.java | 2 + core/src/main/resources/languages/de_DE.toml | 8 ++ core/src/main/resources/languages/es_ES.toml | 8 ++ core/src/main/resources/languages/fi_FI.toml | 8 ++ core/src/main/resources/languages/fr_FR.toml | 8 ++ core/src/main/resources/languages/he_IL.toml | 8 ++ core/src/main/resources/languages/it_IT.toml | 8 ++ core/src/main/resources/languages/ja-JP.toml | 8 ++ core/src/main/resources/languages/ko_KR.toml | 8 ++ core/src/main/resources/languages/lt_LT.toml | 8 ++ core/src/main/resources/languages/nl_NL.toml | 8 ++ core/src/main/resources/languages/pl_PL.toml | 8 ++ core/src/main/resources/languages/pt_PT.toml | 8 ++ core/src/main/resources/languages/ru_RU.toml | 8 ++ core/src/main/resources/languages/tr_TR.toml | 8 ++ core/src/main/resources/languages/vi_VI.toml | 8 ++ core/src/main/resources/languages/zh_CN.toml | 8 ++ core/src/main/resources/languages/zh_TW.toml | 8 ++ .../bukkit/nms/BukkitBiomeLocateTest.java | 7 ++ ...BukkitChunkGeneratorManualHotloadTest.java | 106 ++++++++++++++++ .../generation/ObjectStudioGeometryTest.java | 40 +----- .../iris/testsupport/BukkitTestServer.java | 34 +++++ gradle/multiplexor.gradle | 3 +- 37 files changed, 902 insertions(+), 52 deletions(-) create mode 100644 adapters/bukkit/plugin/src/test/gameplay/studio-manual-hotload.mjs create mode 100644 adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandStudioHotloadTest.java create mode 100644 adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedStudioHotloadLanguageTest.java create mode 100644 core/src/test/java/art/arcane/iris/platform/generation/BukkitChunkGeneratorManualHotloadTest.java diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandStudio.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandStudio.java index 136c09b00..7bc889a94 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandStudio.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/command/CommandStudio.java @@ -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()) { diff --git a/adapters/bukkit/plugin/src/test/gameplay/studio-manual-hotload.mjs b/adapters/bukkit/plugin/src/test/gameplay/studio-manual-hotload.mjs new file mode 100644 index 000000000..2d61e97bc --- /dev/null +++ b/adapters/bukkit/plugin/src/test/gameplay/studio-manual-hotload.mjs @@ -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) + } + } + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandStudioHotloadTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandStudioHotloadTest.java new file mode 100644 index 000000000..a041e4c1b --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/command/CommandStudioHotloadTest.java @@ -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 = 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 pending = new CompletableFuture<>(); + UUID audience = UUID.randomUUID(); + when(studio.getActiveProject()).thenReturn(project); + when(generator.hotloadAsync()).thenReturn(pending); + + try (MockedStatic 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 messages = new ArrayList<>(); + private final List audiences = new ArrayList<>(); + + private RecordingSender() { + super(null); + } + + @Override + public void sendMessage(String message) { + messages.add(message); + audiences.add(LanguageAudience.current()); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java index e74212d58..99152a571 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java @@ -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"), diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java index 080312ab0..707f4fd7c 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStudioCommands.java @@ -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 GATE = NativeCommandRegistration.GAMEMASTERS; @@ -152,6 +155,10 @@ public final class ModdedStudioCommands { .executes(ModdedCommandTree.localized((CommandContext context) -> tpStudio(context.getSource())))); root.then(NativeCommandRegistration.literal("status") .executes(ModdedCommandTree.localized((CommandContext context) -> status(context.getSource())))); + root.then(NativeCommandRegistration.literal("hotload") + .executes(ModdedCommandTree.localized((CommandContext context) -> hotload(context.getSource())))); + root.then(NativeCommandRegistration.literal("refresh") + .executes(ModdedCommandTree.localized((CommandContext 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 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 hotloadTransition( + NativeCommandSource source, + UUID owner, + Executor feedback + ) { + return CompletableFuture.supplyAsync(() -> hotloadStudio(source, owner, feedback), feedback) + .thenCompose(request -> request); + } + + private static CompletableFuture 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 handles = ModdedDimensionManager.handles(); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCommandMessages.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCommandMessages.java index 7897d6f68..6c043e7fe 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCommandMessages.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCommandMessages.java @@ -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, diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedHelpMessages.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedHelpMessages.java index 219e33e73..d7e3f59d6 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedHelpMessages.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedHelpMessages.java @@ -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, diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java index 1ef12ab8d..47ca52014 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/service/ModdedStudioHotloadService.java @@ -123,6 +123,33 @@ public final class ModdedStudioHotloadService implements ModdedTickableService, watches.clear(); } + public CompletableFuture 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 created, KList changed, KList deleted) -> hotload(dimensionId, generator, engine), + (KList created, KList changed, KList 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) { diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedStudioHotloadLanguageTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedStudioHotloadLanguageTest.java new file mode 100644 index 000000000..30f2eeee7 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/command/ModdedStudioHotloadLanguageTest.java @@ -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 first."), + runCommand("studio refresh", false)); + } + + private List runCommand(String command, boolean playerCommand) throws Exception { + NativeModdedServer server = mock(NativeModdedServer.class); + List 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 messages = new ArrayList<>(); + doAnswer(invocation -> { + NativeCommandText text = invocation.getArgument(0); + messages.add(text.getString()); + return null; + }).when(source).sendFailure(any()); + CommandDispatcher 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; + } +} diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedStudioHotloadServiceTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedStudioHotloadServiceTest.java index a7e8d31a5..ee350f163 100644 --- a/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedStudioHotloadServiceTest.java +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/service/ModdedStudioHotloadServiceTest.java @@ -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 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 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; + } + } diff --git a/build.gradle b/build.gradle index 092c631ea..d89004fc9 100644 --- a/build.gradle +++ b/build.gradle @@ -89,7 +89,6 @@ Closure irisArtifactName = { String platform, String targetVersion -> String bukkitArtifactName = irisArtifactName('CraftBukkit', bukkitMinecraftRange) Map runtimeDownloadRelocations = (Map) ext.runtimeDownloadRelocations List runtimeDownloadedPrefixes = (List) ext.runtimeDownloadedPrefixes -File bukkitArtifact = layout.buildDirectory.file("libs/${bukkitArtifactName}").get().asFile apply plugin: ApiGenerator List nmsBindings = ['v26_2_R1', 'v26_3_R1'] @@ -542,19 +541,20 @@ List requiredBukkitArtifactEntries = [ tasks.register('verifyBukkitArtifact') { group = 'verification' dependsOn('jar') - File artifact = bukkitArtifact + Provider 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") } } diff --git a/core/src/main/java/art/arcane/iris/localization/BukkitCommandMessages.java b/core/src/main/java/art/arcane/iris/localization/BukkitCommandMessages.java index d87d00873..480004292 100644 --- a/core/src/main/java/art/arcane/iris/localization/BukkitCommandMessages.java +++ b/core/src/main/java/art/arcane/iris/localization/BukkitCommandMessages.java @@ -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, diff --git a/core/src/main/java/art/arcane/iris/localization/DirectorCommandMessages.java b/core/src/main/java/art/arcane/iris/localization/DirectorCommandMessages.java index 4cd012f9b..7c251f76f 100644 --- a/core/src/main/java/art/arcane/iris/localization/DirectorCommandMessages.java +++ b/core/src/main/java/art/arcane/iris/localization/DirectorCommandMessages.java @@ -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, diff --git a/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java index def60f243..668a2c7df 100644 --- a/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java @@ -1091,6 +1091,24 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun }); } + @Override + public CompletableFuture 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; diff --git a/core/src/main/java/art/arcane/iris/platform/generation/PlatformChunkGenerator.java b/core/src/main/java/art/arcane/iris/platform/generation/PlatformChunkGenerator.java index 23ba05a1c..c9137203f 100644 --- a/core/src/main/java/art/arcane/iris/platform/generation/PlatformChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/platform/generation/PlatformChunkGenerator.java @@ -44,6 +44,8 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider { void close(); + CompletableFuture hotloadAsync(); + default CompletableFuture closeAsync() { close(); return CompletableFuture.completedFuture(null); diff --git a/core/src/main/resources/languages/de_DE.toml b/core/src/main/resources/languages/de_DE.toml index ac104f0ca..48875f67f 100644 --- a/core/src/main/resources/languages/de_DE.toml +++ b/core/src/main/resources/languages/de_DE.toml @@ -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 [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" diff --git a/core/src/main/resources/languages/es_ES.toml b/core/src/main/resources/languages/es_ES.toml index 67a126d4b..5861ad541 100644 --- a/core/src/main/resources/languages/es_ES.toml +++ b/core/src/main/resources/languages/es_ES.toml @@ -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 [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" diff --git a/core/src/main/resources/languages/fi_FI.toml b/core/src/main/resources/languages/fi_FI.toml index ba0c528eb..063fca9e1 100644 --- a/core/src/main/resources/languages/fi_FI.toml +++ b/core/src/main/resources/languages/fi_FI.toml @@ -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 [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" diff --git a/core/src/main/resources/languages/fr_FR.toml b/core/src/main/resources/languages/fr_FR.toml index 06d165bd6..6eedd4121 100644 --- a/core/src/main/resources/languages/fr_FR.toml +++ b/core/src/main/resources/languages/fr_FR.toml @@ -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 [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" diff --git a/core/src/main/resources/languages/he_IL.toml b/core/src/main/resources/languages/he_IL.toml index c63fe427d..aaa89cd06 100644 --- a/core/src/main/resources/languages/he_IL.toml +++ b/core/src/main/resources/languages/he_IL.toml @@ -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 [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" = "חבילת המימד לפתוח סטודיו" diff --git a/core/src/main/resources/languages/it_IT.toml b/core/src/main/resources/languages/it_IT.toml index faf9fd758..290a26fc6 100644 --- a/core/src/main/resources/languages/it_IT.toml +++ b/core/src/main/resources/languages/it_IT.toml @@ -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 [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" diff --git a/core/src/main/resources/languages/ja-JP.toml b/core/src/main/resources/languages/ja-JP.toml index 5eb012f67..06f31c1a4 100644 --- a/core/src/main/resources/languages/ja-JP.toml +++ b/core/src/main/resources/languages/ja-JP.toml @@ -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 [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" = "スタジオを開くディメンションパック" diff --git a/core/src/main/resources/languages/ko_KR.toml b/core/src/main/resources/languages/ko_KR.toml index 6be20b39c..9523b463d 100644 --- a/core/src/main/resources/languages/ko_KR.toml +++ b/core/src/main/resources/languages/ko_KR.toml @@ -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 [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를 열 차원 팩" diff --git a/core/src/main/resources/languages/lt_LT.toml b/core/src/main/resources/languages/lt_LT.toml index 92d539d4b..b99b5c6b0 100644 --- a/core/src/main/resources/languages/lt_LT.toml +++ b/core/src/main/resources/languages/lt_LT.toml @@ -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 [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" diff --git a/core/src/main/resources/languages/nl_NL.toml b/core/src/main/resources/languages/nl_NL.toml index ac4c21989..c10bbc442 100644 --- a/core/src/main/resources/languages/nl_NL.toml +++ b/core/src/main/resources/languages/nl_NL.toml @@ -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 [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" diff --git a/core/src/main/resources/languages/pl_PL.toml b/core/src/main/resources/languages/pl_PL.toml index bb47f8283..c2bd4b35e 100644 --- a/core/src/main/resources/languages/pl_PL.toml +++ b/core/src/main/resources/languages/pl_PL.toml @@ -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 [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" diff --git a/core/src/main/resources/languages/pt_PT.toml b/core/src/main/resources/languages/pt_PT.toml index e6211a15f..48b651e52 100644 --- a/core/src/main/resources/languages/pt_PT.toml +++ b/core/src/main/resources/languages/pt_PT.toml @@ -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 [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" diff --git a/core/src/main/resources/languages/ru_RU.toml b/core/src/main/resources/languages/ru_RU.toml index 0bfd51311..79a2e558a 100644 --- a/core/src/main/resources/languages/ru_RU.toml +++ b/core/src/main/resources/languages/ru_RU.toml @@ -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 [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" = "Размерный пакет, чтобы открыть студию для" diff --git a/core/src/main/resources/languages/tr_TR.toml b/core/src/main/resources/languages/tr_TR.toml index 1746239ab..89aa5e5e3 100644 --- a/core/src/main/resources/languages/tr_TR.toml +++ b/core/src/main/resources/languages/tr_TR.toml @@ -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 [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" diff --git a/core/src/main/resources/languages/vi_VI.toml b/core/src/main/resources/languages/vi_VI.toml index 334bc1861..3f53831a0 100644 --- a/core/src/main/resources/languages/vi_VI.toml +++ b/core/src/main/resources/languages/vi_VI.toml @@ -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 [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" diff --git a/core/src/main/resources/languages/zh_CN.toml b/core/src/main/resources/languages/zh_CN.toml index 0c9952473..b525f6fdd 100644 --- a/core/src/main/resources/languages/zh_CN.toml +++ b/core/src/main/resources/languages/zh_CN.toml @@ -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 [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 中打开的维度包" diff --git a/core/src/main/resources/languages/zh_TW.toml b/core/src/main/resources/languages/zh_TW.toml index ea663861c..564034636 100644 --- a/core/src/main/resources/languages/zh_TW.toml +++ b/core/src/main/resources/languages/zh_TW.toml @@ -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 [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 中開啟的維度包" diff --git a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeLocateTest.java b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeLocateTest.java index e3f40de5c..39ffb8da3 100644 --- a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeLocateTest.java +++ b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeLocateTest.java @@ -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"))); diff --git a/core/src/test/java/art/arcane/iris/platform/generation/BukkitChunkGeneratorManualHotloadTest.java b/core/src/test/java/art/arcane/iris/platform/generation/BukkitChunkGeneratorManualHotloadTest.java new file mode 100644 index 000000000..12d32b535 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/platform/generation/BukkitChunkGeneratorManualHotloadTest.java @@ -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 scheduling = fixture.captureScheduling()) { + CompletableFuture 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 scheduling = fixture.captureScheduling()) { + CompletableFuture 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 scheduling = fixture.captureScheduling()) { + CompletableFuture failed = fixture.generator.hotloadAsync(); + fixture.queued.get().run(); + assertTrue(failed.isCompletedExceptionally()); + + CompletableFuture 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 queued = new AtomicReference<>(); + + private Fixture() throws Exception { + generator.setEngine(engine); + setField(generator, "studio", true); + setField(generator, "loadLock", new GenerationStageGate(1, () -> false)); + } + + private MockedStatic captureScheduling() { + MockedStatic scheduling = mockStatic(J.class); + scheduling.when(() -> J.a(any(Runnable.class))).thenAnswer(invocation -> { + queued.set(invocation.getArgument(0)); + return null; + }); + return scheduling; + } + } +} diff --git a/core/src/test/java/art/arcane/iris/studio/generation/ObjectStudioGeometryTest.java b/core/src/test/java/art/arcane/iris/studio/generation/ObjectStudioGeometryTest.java index 15ec6e69e..123c4afa9 100644 --- a/core/src/test/java/art/arcane/iris/studio/generation/ObjectStudioGeometryTest.java +++ b/core/src/test/java/art/arcane/iris/studio/generation/ObjectStudioGeometryTest.java @@ -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 registries = mockStatic(RegistryAccess.class); - MockedStatic 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) { } diff --git a/core/src/testFixtures/java/art/arcane/iris/testsupport/BukkitTestServer.java b/core/src/testFixtures/java/art/arcane/iris/testsupport/BukkitTestServer.java index 205d6e7c7..a9ac2db1f 100644 --- a/core/src/testFixtures/java/art/arcane/iris/testsupport/BukkitTestServer.java +++ b/core/src/testFixtures/java/art/arcane/iris/testsupport/BukkitTestServer.java @@ -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 registries = mockStatic(RegistryAccess.class); + MockedStatic 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 registries = new HashMap<>(); return mock(RegistryAccess.class, invocation -> { diff --git a/gradle/multiplexor.gradle b/gradle/multiplexor.gradle index e0ec80cf7..032f7fb38 100644 --- a/gradle/multiplexor.gradle +++ b/gradle/multiplexor.gradle @@ -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([