diff --git a/README.md b/README.md index 658f78d4a..dc9d0a495 100644 --- a/README.md +++ b/README.md @@ -250,9 +250,11 @@ Per-platform tasks: `./gradlew buildBukkit`, `buildFabric`, `buildForge`, `build SPI jar (the pure-JVM adapter/platform contract, not the stable plugin API) is built to `spi/build/libs/` by `./gradlew :spi:jar`. -`./gradlew buildAll` is a different task: it builds every platform and copies the jars into a -consumer dropin tree for a local test server. It defaults to `build/consumers/` inside the repo; -override with `-Plocation=/path/to/consumers`. +`./gradlew buildAll` also runs `buildAllToOut` and copies the jars into a consumer dropin tree +for a local test server. The consumer directory defaults to `../../[Minecraft Server]/consumers/` +when it exists, otherwise `build/consumers/` inside the repo. `-Plocation=/path/to/consumers` +changes only the consumer directory. Both tasks copy all four verified platform jars into +`../PluginOuts/`. If you need help compiling as a developer or contributor, ask in the Discord. diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java index b5a1e0986..3f89ea16a 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/core/commands/CommandPregen.java @@ -18,6 +18,7 @@ package art.arcane.iris.core.commands; +import art.arcane.iris.core.localization.IrisMessages; import art.arcane.iris.Iris; import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.pregenerator.PregenTask; @@ -33,7 +34,6 @@ import org.bukkit.util.Vector; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.BukkitCommandMessagesExtended; -import art.arcane.iris.core.localization.ModdedCommandMessages; import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.volmlib.util.localization.MessageArgument; @Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!", descriptionKey = "iris.director.commandpregen.director.pregenerate_your_iris_worlds") @@ -62,7 +62,7 @@ public class CommandPregen implements DirectorExecutor { if (PregeneratorJob.getInstance() != null) { // Reject like the modded adapter does; never silently kill a running job. - sender().sendMessage(IrisLanguage.text(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS)); + sender().sendMessage(IrisLanguage.text(IrisMessages.PREGEN_ALREADY_RUNNING)); return; } diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientToasts.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientToasts.java index 0521211a8..f98dc230d 100644 --- a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientToasts.java +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisClientToasts.java @@ -1,6 +1,6 @@ package art.arcane.iris.client; -import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.modded.localization.ClientUiMessages; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.spi.protocol.IrisMessage; import art.arcane.volmlib.util.localization.MessageArgument; diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisPregenHud.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisPregenHud.java index 8dfa599a5..94e2fe60d 100644 --- a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisPregenHud.java +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisPregenHud.java @@ -1,6 +1,6 @@ package art.arcane.iris.client; -import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.modded.localization.ClientUiMessages; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.iris.spi.protocol.IrisMessage; diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisVisionScreen.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisVisionScreen.java index 4ca86a448..b07a1dc54 100644 --- a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisVisionScreen.java +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisVisionScreen.java @@ -1,6 +1,6 @@ package art.arcane.iris.client; -import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.modded.localization.ClientUiMessages; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.spi.protocol.IrisMessage; import art.arcane.volmlib.util.localization.MessageArgument; diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisWhatOverlay.java b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisWhatOverlay.java index 7c284fd21..41a347500 100644 --- a/adapters/client-common/src/main/java/art/arcane/iris/client/IrisWhatOverlay.java +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/IrisWhatOverlay.java @@ -1,6 +1,6 @@ package art.arcane.iris.client; -import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.modded.localization.ClientUiMessages; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.spi.protocol.IrisMessage; import art.arcane.volmlib.util.localization.MessageArgument; diff --git a/adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldOpenFlowsMixin.java b/adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldOpenFlowsMixin.java index fad50a286..0350044be 100644 --- a/adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldOpenFlowsMixin.java +++ b/adapters/client-common/src/main/java/art/arcane/iris/client/mixin/IrisWorldOpenFlowsMixin.java @@ -1,6 +1,6 @@ package art.arcane.iris.client.mixin; -import art.arcane.iris.core.localization.ClientUiMessages; +import art.arcane.iris.modded.localization.ClientUiMessages; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.modded.IrisModdedChunkGenerator; import art.arcane.iris.modded.ModdedMixinFlags; diff --git a/adapters/fabric/build.gradle b/adapters/fabric/build.gradle index 439fa3d27..afc069dae 100644 --- a/adapters/fabric/build.gradle +++ b/adapters/fabric/build.gradle @@ -20,12 +20,24 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.gradle.api.tasks.compile.JavaCompile import org.gradle.jvm.toolchain.JavaLanguageVersion +buildscript { + repositories { + mavenCentral() + maven { url = uri('https://jitpack.io') } + } + dependencies { + classpath('com.github.VolmitSoftware.VolmLib:packaging:master-SNAPSHOT') + } +} + plugins { id 'java' id 'net.fabricmc.fabric-loom' version '1.17.14' alias(libs.plugins.shadow) } +apply plugin: 'art.arcane.volmit-packaging' + apply from: file('../../gradle/modded-generation-build-revision.gradle') Properties rootProperties = new Properties() @@ -320,3 +332,14 @@ tasks.matching { it.name == 'remapJar' }.configureEach { tasks.named('assemble').configure { dependsOn(tasks.named('shadowJar')) } + +pluginPackaging { + artifacts { + create('distribution') { + taskName = 'shadowJar' + policyName = 'Iris Fabric' + prunePrefixes = ['art/arcane/volmlib/'] + keepPrefixes = ['art/arcane/volmlib/util/matter/slices/'] + } + } +} diff --git a/adapters/forge/build.gradle b/adapters/forge/build.gradle index 1cc1e7588..9c77d50d3 100644 --- a/adapters/forge/build.gradle +++ b/adapters/forge/build.gradle @@ -20,12 +20,24 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.gradle.api.tasks.compile.JavaCompile import org.gradle.jvm.toolchain.JavaLanguageVersion +buildscript { + repositories { + mavenCentral() + maven { url = uri('https://jitpack.io') } + } + dependencies { + classpath('com.github.VolmitSoftware.VolmLib:packaging:master-SNAPSHOT') + } +} + plugins { id 'java' id 'net.minecraftforge.gradle' version '7.0.31' alias(libs.plugins.shadow) } +apply plugin: 'art.arcane.volmit-packaging' + apply from: file('../../gradle/modded-generation-build-revision.gradle') Properties rootProperties = new Properties() @@ -294,3 +306,14 @@ tasks.named('jar').configure { tasks.named('assemble').configure { dependsOn(tasks.named('shadowJar')) } + +pluginPackaging { + artifacts { + create('distribution') { + taskName = 'shadowJar' + policyName = 'Iris Forge' + prunePrefixes = ['art/arcane/volmlib/'] + keepPrefixes = ['art/arcane/volmlib/util/matter/slices/'] + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java index 5d8a9ba68..646ebfd38 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/IrisModdedCommands.java @@ -22,7 +22,7 @@ import art.arcane.iris.modded.ModdedIrisLog; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.IrisMessages; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.iris.core.localization.PackDownloadMessages; import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.iris.core.lifecycle.LifecycleOperationCoordinator; 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 f29523a17..1bc0c377f 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 @@ -20,7 +20,7 @@ package art.arcane.iris.modded.command; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.IrisMessages; -import art.arcane.iris.core.localization.ModdedHelpMessages; +import art.arcane.iris.modded.localization.ModdedHelpMessages; import art.arcane.volmlib.util.director.help.DirectorHelpMessages; import art.arcane.volmlib.util.localization.MessageArgument; import art.arcane.volmlib.util.localization.TextKey; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java index a7642c62d..4ff0dbdb8 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDatapackCommands.java @@ -51,7 +51,7 @@ import java.util.Set; import java.util.function.Predicate; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.volmlib.util.localization.MessageArgument; public final class ModdedDatapackCommands { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDeveloperCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDeveloperCommands.java index ade09c587..c434c05ed 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDeveloperCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedDeveloperCommands.java @@ -32,7 +32,7 @@ import java.util.Enumeration; import java.util.function.Predicate; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.volmlib.util.localization.MessageArgument; final class ModdedDeveloperCommands { private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java index 13d92da56..b561728b6 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedEditCommands.java @@ -22,7 +22,7 @@ import art.arcane.iris.modded.ModdedIrisLog; import art.arcane.iris.core.gui.GuiHost; import art.arcane.iris.core.loader.IrisRegistrant; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGoldenHash.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGoldenHash.java index 45e19e25a..72c6a28e6 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGoldenHash.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedGoldenHash.java @@ -36,7 +36,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.iris.core.localization.RuntimeProgressMessages; import art.arcane.volmlib.util.localization.MessageArgument; public final class ModdedGoldenHash { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java index 27ac43ad5..0b8c4f4f4 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedLocateCommands.java @@ -20,7 +20,7 @@ package art.arcane.iris.modded.command; import art.arcane.iris.modded.ModdedIrisLog; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.GenerationSessionException; import art.arcane.iris.engine.framework.GenerationSessionLease; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java index 603aa8e2d..79e9860f2 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedObjectCommands.java @@ -69,7 +69,7 @@ import java.util.UUID; import java.util.function.Predicate; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.volmlib.util.localization.MessageArgument; public final class ModdedObjectCommands { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java index 19c24830c..b278eafe9 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPackCommands.java @@ -41,7 +41,7 @@ import java.util.Map; import java.util.function.Predicate; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.volmlib.util.localization.MessageArgument; public final class ModdedPackCommands { private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java index a8bbee792..dafdf3a60 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedPregenCommands.java @@ -18,8 +18,9 @@ package art.arcane.iris.modded.command; +import art.arcane.iris.core.localization.IrisMessages; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.iris.engine.framework.Engine; import art.arcane.volmlib.util.localization.MessageArgument; @@ -59,7 +60,7 @@ final class ModdedPregenCommands { return 0; } if (!started) { - IrisModdedCommands.fail(source, IrisLanguage.plain(ModdedCommandMessages.IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS)); + IrisModdedCommands.fail(source, IrisLanguage.plain(IrisMessages.PREGEN_ALREADY_RUNNING)); return 0; } ModdedPregenBossBar.begin(source.getPlayer()); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java index 6947fd595..086f4cd79 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedRegen.java @@ -62,7 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; public final class ModdedRegen { private static final int APPLY_AHEAD = 8; private static final long CHUNK_SLOT_TIMEOUT_MILLIS = 120000L; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStructureCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStructureCommands.java index d135b9621..2fe7a5a08 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStructureCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedStructureCommands.java @@ -49,7 +49,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.volmlib.util.localization.MessageArgument; public final class ModdedStructureCommands { private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); 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 7b3fe1a23..0538cdf0f 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 @@ -90,7 +90,7 @@ import java.util.regex.Pattern; import java.util.stream.Stream; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.volmlib.util.localization.MessageArgument; public final class ModdedStudioCommands { private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWhatCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWhatCommands.java index 9c7016f05..7404d40dc 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWhatCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWhatCommands.java @@ -21,7 +21,7 @@ package art.arcane.iris.modded.command; import art.arcane.iris.modded.ModdedIrisLog; import art.arcane.iris.core.localization.IrisLanguage; import art.arcane.iris.core.localization.IrisMessages; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.GenerationSessionException; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java index b86e7dc16..f189c0207 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java @@ -52,7 +52,7 @@ import java.util.concurrent.ThreadLocalRandom; import java.util.function.Predicate; import art.arcane.iris.core.localization.IrisLanguage; -import art.arcane.iris.core.localization.ModdedCommandMessages; +import art.arcane.iris.modded.localization.ModdedCommandMessages; import art.arcane.iris.core.localization.RuntimeUiMessages; import art.arcane.volmlib.util.localization.MessageArgument; public final class ModdedWorldCommands { diff --git a/core/src/main/java/art/arcane/iris/core/localization/ClientUiMessages.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ClientUiMessages.java similarity index 99% rename from core/src/main/java/art/arcane/iris/core/localization/ClientUiMessages.java rename to adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ClientUiMessages.java index 518461e8d..a7d740caa 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/ClientUiMessages.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ClientUiMessages.java @@ -1,4 +1,4 @@ -package art.arcane.iris.core.localization; +package art.arcane.iris.modded.localization; import art.arcane.volmlib.util.localization.MessageKey; import art.arcane.volmlib.util.localization.PluralKey; diff --git a/core/src/main/java/art/arcane/iris/core/localization/ModdedCommandMessages.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCommandMessages.java similarity index 99% rename from core/src/main/java/art/arcane/iris/core/localization/ModdedCommandMessages.java rename to adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCommandMessages.java index 465c74939..2371e68ca 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/ModdedCommandMessages.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedCommandMessages.java @@ -1,4 +1,4 @@ -package art.arcane.iris.core.localization; +package art.arcane.iris.modded.localization; import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.localization.MessageKey; @@ -127,10 +127,6 @@ public final class ModdedCommandMessages { "iris.modded.irismoddedcommands.current_dimension_is_not_generated_by_iris_name_one_explicitly", "The current dimension (" + "{value}" + ") is not generated by Iris. Name one explicitly: /iris pregen start " + "{radius}" + " ; see /iris info for loaded Iris dimensions." ); - public static final TextKey IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS = TextKey.of( - "iris.modded.irismoddedcommands.pregeneration_task_is_already_running_stop_it_first_with_iris", - "A pregeneration task is already running. Stop it first with /iris pregen stop." - ); public static final TextKey IRIS_MODDED_COMMANDS_PREGEN_STARTED_BY_BLOCKS_FROM_PROGRESS_LOGS_CONSOLE_SEE_IRIS = TextKey.of( "iris.modded.irismoddedcommands.pregen_started_by_blocks_from_progress_logs_console_see_iris", "Pregen started in " + "{value}" + " of " + "{value2}" + " by " + "{value3}" + " blocks from " + "{centerX}" + "," + "{centerZ}" + "." + "{modeNote}" + " Progress logs to console; see /iris pregen status." + "{guiNote}" @@ -1284,7 +1280,6 @@ public final class ModdedCommandMessages { IRIS_MODDED_COMMANDS_THIS_DIMENSION_IS_NOT_GENERATED_BY_IRIS_5, IRIS_MODDED_COMMANDS_IS_NOT_GENERATED_BY_IRIS_SEE_IRIS_INFO_LOADED_IRIS, IRIS_MODDED_COMMANDS_CURRENT_DIMENSION_IS_NOT_GENERATED_BY_IRIS_NAME_ONE_EXPLICITLY, - IRIS_MODDED_COMMANDS_PREGENERATION_TASK_IS_ALREADY_RUNNING_STOP_IT_FIRST_WITH_IRIS, IRIS_MODDED_COMMANDS_PREGEN_STARTED_BY_BLOCKS_FROM_PROGRESS_LOGS_CONSOLE_SEE_IRIS, IRIS_MODDED_COMMANDS_STOPPING_PREGENERATION_FINISHING_UP_CURRENT_REGION, IRIS_MODDED_COMMANDS_NO_ACTIVE_PREGENERATION_TASK_STOP, diff --git a/core/src/main/java/art/arcane/iris/core/localization/ModdedHelpMessages.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedHelpMessages.java similarity index 99% rename from core/src/main/java/art/arcane/iris/core/localization/ModdedHelpMessages.java rename to adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedHelpMessages.java index 658bed468..219e33e73 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/ModdedHelpMessages.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedHelpMessages.java @@ -1,4 +1,4 @@ -package art.arcane.iris.core.localization; +package art.arcane.iris.modded.localization; import art.arcane.volmlib.util.localization.MessageKey; import art.arcane.volmlib.util.localization.TextKey; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedMessageContributor.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedMessageContributor.java new file mode 100644 index 000000000..0c508c5cc --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/localization/ModdedMessageContributor.java @@ -0,0 +1,18 @@ +package art.arcane.iris.modded.localization; + +import art.arcane.iris.core.localization.IrisMessageContributor; +import art.arcane.volmlib.util.localization.MessageKey; + +import java.util.ArrayList; +import java.util.List; + +public final class ModdedMessageContributor implements IrisMessageContributor { + @Override + public List keys() { + List keys = new ArrayList<>(); + keys.addAll(ModdedCommandMessages.keys()); + keys.addAll(ModdedHelpMessages.keys()); + keys.addAll(ClientUiMessages.keys()); + return List.copyOf(keys); + } +} diff --git a/adapters/modded-common/src/main/resources/META-INF/services/art.arcane.iris.core.localization.IrisMessageContributor b/adapters/modded-common/src/main/resources/META-INF/services/art.arcane.iris.core.localization.IrisMessageContributor new file mode 100644 index 000000000..30f318686 --- /dev/null +++ b/adapters/modded-common/src/main/resources/META-INF/services/art.arcane.iris.core.localization.IrisMessageContributor @@ -0,0 +1 @@ +art.arcane.iris.modded.localization.ModdedMessageContributor diff --git a/adapters/modded-common/src/test/java/art/arcane/iris/modded/localization/ModdedMessageContributorTest.java b/adapters/modded-common/src/test/java/art/arcane/iris/modded/localization/ModdedMessageContributorTest.java new file mode 100644 index 000000000..634ca6702 --- /dev/null +++ b/adapters/modded-common/src/test/java/art/arcane/iris/modded/localization/ModdedMessageContributorTest.java @@ -0,0 +1,37 @@ +package art.arcane.iris.modded.localization; + +import art.arcane.iris.core.localization.IrisLanguage; +import art.arcane.iris.core.localization.IrisMessages; +import art.arcane.volmlib.util.localization.MessageKey; +import art.arcane.volmlib.util.localization.VolmitLocales; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class ModdedMessageContributorTest { + @Test + public void loaderDiscoversModdedAndClientMessages() { + for (MessageKey key : new ModdedMessageContributor().keys()) { + assertNotNull(key.id(), IrisLanguage.catalog().key(key.id())); + assertEquals(key.id(), key.englishValue(), IrisLanguage.catalog().require(key.id()).englishValue()); + } + assertEquals("A pregeneration task is already running. Stop it first with /iris pregen stop.", + IrisLanguage.plain(IrisMessages.PREGEN_ALREADY_RUNNING)); + } + + @Test + public void completeLoaderCatalogStillMatchesEveryDownloadableLocale() throws Exception { + Path sourceRoot = Path.of("../../core/src/main/resources/languages"); + for (String locale : VolmitLocales.nonEnglish()) { + JsonObject document = JsonParser.parseString(Files.readString(sourceRoot.resolve(locale + ".json"))) + .getAsJsonObject(); + assertEquals(locale, IrisLanguage.catalog().ids(), document.getAsJsonObject("messages").keySet()); + } + } +} diff --git a/adapters/neoforge/build.gradle b/adapters/neoforge/build.gradle index 81eff1590..3e43b7e5a 100644 --- a/adapters/neoforge/build.gradle +++ b/adapters/neoforge/build.gradle @@ -20,12 +20,24 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.gradle.api.tasks.compile.JavaCompile import org.gradle.jvm.toolchain.JavaLanguageVersion +buildscript { + repositories { + mavenCentral() + maven { url = uri('https://jitpack.io') } + } + dependencies { + classpath('com.github.VolmitSoftware.VolmLib:packaging:master-SNAPSHOT') + } +} + plugins { id 'java' id 'net.neoforged.moddev' version '2.0.141' alias(libs.plugins.shadow) } +apply plugin: 'art.arcane.volmit-packaging' + apply from: file('../../gradle/modded-generation-build-revision.gradle') Properties rootProperties = new Properties() @@ -270,3 +282,14 @@ tasks.named('jar').configure { tasks.named('assemble').configure { dependsOn(tasks.named('shadowJar')) } + +pluginPackaging { + artifacts { + create('distribution') { + taskName = 'shadowJar' + policyName = 'Iris NeoForge' + prunePrefixes = ['art/arcane/volmlib/'] + keepPrefixes = ['art/arcane/volmlib/util/matter/slices/'] + } + } +} diff --git a/build.gradle b/build.gradle index 90886034b..562cdcc55 100644 --- a/build.gradle +++ b/build.gradle @@ -33,11 +33,23 @@ buildscript { } } +buildscript { + repositories { + mavenCentral() + maven { url = uri('https://jitpack.io') } + } + dependencies { + classpath('com.github.VolmitSoftware.VolmLib:packaging:master-SNAPSHOT') + } +} + plugins { id 'java' id 'java-library' } +apply plugin: 'art.arcane.volmit-packaging' + group = 'art.arcane' version = providers.gradleProperty('irisVersion').getOrElse('4.1.0-26.2') @@ -167,9 +179,6 @@ tasks.named('jar', Jar).configure { delete(layout.buildDirectory.file("libs/Iris-${project.version}.jar")) } archiveFileName.set(bukkitArtifactName) - doLast { - JarCompactor.compact(archiveFile.get().asFile) - } } tasks.register('iris', Copy) { @@ -611,7 +620,7 @@ tasks.register('buildAllToOut') { tasks.register('buildAll') { group = 'iris' outputs.upToDateWhen { false } - dependsOn('iris', 'verifyModdedArtifacts') + dependsOn('iris', 'verifyModdedArtifacts', 'buildAllToOut') doLast { delete( file("${bukkitConsumerPath}/Iris-Fabric.jar"), @@ -746,3 +755,17 @@ if (!JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_25)) { System.err.println() System.exit(69) } + +pluginPackaging { + artifacts { + create('distribution') { + taskName = 'jar' + policyName = 'Iris Bukkit' + stripDirectories = true + stripLocalVariables = true + releaseCompression = providers.gradleProperty('compactRelease').map { it.toBoolean() }.getOrElse(false) + prunePrefixes = ['art/arcane/volmlib/'] + keepPrefixes = ['art/arcane/volmlib/util/matter/slices/'] + } + } +} diff --git a/buildSrc/src/main/java/ClassReferences.java b/buildSrc/src/main/java/ClassReferences.java index 75b3e1a18..20f2c1790 100644 --- a/buildSrc/src/main/java/ClassReferences.java +++ b/buildSrc/src/main/java/ClassReferences.java @@ -1,3 +1,10 @@ +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashSet; @@ -6,8 +13,8 @@ import java.util.Set; /** * Reads the {@code CONSTANT_Class} entries of a class file. That is exactly the set of types the - * class links against - supertypes, field and method owners, casts, catches, constant class - * literals. Annotation types live in the annotation attributes as plain UTF-8 descriptors and are + * class names directly - supertypes, field and method owners, casts, catches, constant class + * literals. Field and method descriptors are also traversed to include signature-only types. Annotation types live in the annotation attributes as plain UTF-8 descriptors and are * deliberately not reported: the JVM skips an annotation whose type is absent. */ public final class ClassReferences { @@ -42,6 +49,7 @@ public final class ClassReferences { int constantCount = readUnsignedShort(classFile, 8); String[] utf8 = new String[constantCount]; List classNameIndexes = new ArrayList<>(); + List descriptorIndexes = new ArrayList<>(); int offset = 10; int index = 1; while (index < constantCount) { @@ -58,10 +66,18 @@ public final class ClassReferences { classNameIndexes.add(readUnsignedShort(classFile, offset)); offset += 2; } - case CONSTANT_STRING, CONSTANT_METHOD_TYPE, CONSTANT_MODULE, CONSTANT_PACKAGE -> offset += 2; + case CONSTANT_METHOD_TYPE -> { + descriptorIndexes.add(readUnsignedShort(classFile, offset)); + offset += 2; + } + case CONSTANT_NAME_AND_TYPE -> { + descriptorIndexes.add(readUnsignedShort(classFile, offset + 2)); + offset += 4; + } + case CONSTANT_STRING, CONSTANT_MODULE, CONSTANT_PACKAGE -> offset += 2; case CONSTANT_METHOD_HANDLE -> offset += 3; case CONSTANT_INTEGER, CONSTANT_FLOAT, CONSTANT_FIELDREF, CONSTANT_METHODREF, - CONSTANT_INTERFACE_METHODREF, CONSTANT_NAME_AND_TYPE, CONSTANT_DYNAMIC, + CONSTANT_INTERFACE_METHODREF, CONSTANT_DYNAMIC, CONSTANT_INVOKE_DYNAMIC -> offset += 4; case CONSTANT_LONG, CONSTANT_DOUBLE -> { offset += 8; @@ -83,9 +99,41 @@ public final class ClassReferences { references.add(name); } } + for (int descriptorIndex : descriptorIndexes) { + addType(Type.getType(utf8[descriptorIndex]), references); + } + new ClassReader(classFile).accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + addType(Type.getType(descriptor), references); + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, + String[] exceptions) { + addType(Type.getMethodType(descriptor), references); + return null; + } + }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); return references; } + private static void addType(Type type, Set references) { + switch (type.getSort()) { + case Type.ARRAY -> addType(type.getElementType(), references); + case Type.OBJECT -> references.add(type.getInternalName()); + case Type.METHOD -> { + addType(type.getReturnType(), references); + for (Type argument : type.getArgumentTypes()) { + addType(argument, references); + } + } + default -> { + } + } + } + private static String normalize(String rawName) { if (rawName == null || rawName.isEmpty()) { return null; diff --git a/buildSrc/src/main/java/JarCompactor.java b/buildSrc/src/main/java/JarCompactor.java deleted file mode 100644 index 1906ea8fe..000000000 --- a/buildSrc/src/main/java/JarCompactor.java +++ /dev/null @@ -1,121 +0,0 @@ -import org.apache.commons.compress.archivers.zip.ExtraFieldUtils; -import org.apache.commons.compress.archivers.zip.Zip64Mode; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; -import org.gradle.api.GradleException; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.AtomicMoveNotSupportedException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.Set; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public final class JarCompactor { - private static final int BUFFER_BYTES = 64 * 1024; - - private JarCompactor() { - } - - public static void compact(File artifact) { - if (artifact == null || !artifact.isFile()) { - throw new GradleException("Cannot compact missing jar artifact: " + artifact); - } - - Path source = artifact.toPath(); - Path temporary = null; - try { - temporary = Files.createTempFile(source.getParent(), artifact.getName(), ".compact"); - rewrite(source, temporary); - replace(temporary, source); - } catch (IOException exception) { - if (temporary != null) { - try { - Files.deleteIfExists(temporary); - } catch (IOException cleanupFailure) { - exception.addSuppressed(cleanupFailure); - } - } - throw new GradleException("Unable to compact jar artifact " + artifact.getAbsolutePath(), exception); - } - } - - private static void rewrite(Path source, Path destination) throws IOException { - byte[] buffer = new byte[BUFFER_BYTES]; - try (ZipFile input = new ZipFile(source.toFile()); - ZipArchiveOutputStream output = new ZipArchiveOutputStream(destination)) { - if (!output.isSeekable()) { - throw new IOException("Jar compaction requires seekable output."); - } - output.setLevel(9); - output.setUseZip64(Zip64Mode.Never); - output.setCreateUnicodeExtraFields(ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NEVER); - Set names = new HashSet<>(); - Enumeration entries = input.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - if (entry.isDirectory()) { - continue; - } - if (!names.add(entry.getName())) { - throw new IOException("Duplicate jar entry: " + entry.getName()); - } - output.putArchiveEntry(copyMetadata(entry)); - copyEntry(input, output, entry, buffer); - output.closeArchiveEntry(); - } - } - } - - private static void copyEntry(ZipFile input, ZipArchiveOutputStream output, ZipEntry entry, byte[] buffer) - throws IOException { - CRC32 crc = new CRC32(); - long size = 0L; - try (InputStream content = input.getInputStream(entry)) { - int read; - while ((read = content.read(buffer)) != -1) { - output.write(buffer, 0, read); - crc.update(buffer, 0, read); - size += read; - } - } - if (size != entry.getSize() || crc.getValue() != entry.getCrc()) { - throw new IOException("Jar entry failed size or CRC verification: " + entry.getName()); - } - } - - private static ZipArchiveEntry copyMetadata(ZipEntry source) throws IOException { - ZipArchiveEntry target = new ZipArchiveEntry(source.getName()); - target.setMethod(ZipEntry.DEFLATED); - if (source.getTime() >= 0L) { - target.setTime(source.getTime()); - } - if (source.getComment() != null) { - target.setComment(source.getComment()); - } - if (source.getExtra() != null) { - target.setExtraFields(ExtraFieldUtils.parse(source.getExtra(), true, - ZipArchiveEntry.ExtraFieldParsingMode.BEST_EFFORT)); - } - return target; - } - - private static void replace(Path temporary, Path source) throws IOException { - try { - Files.move( - temporary, - source, - StandardCopyOption.ATOMIC_MOVE, - StandardCopyOption.REPLACE_EXISTING); - } catch (AtomicMoveNotSupportedException exception) { - Files.move(temporary, source, StandardCopyOption.REPLACE_EXISTING); - } - } -} diff --git a/buildSrc/src/test/java/ClassReferencesTest.java b/buildSrc/src/test/java/ClassReferencesTest.java new file mode 100644 index 000000000..1b5cccd44 --- /dev/null +++ b/buildSrc/src/test/java/ClassReferencesTest.java @@ -0,0 +1,55 @@ +import org.junit.Test; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import java.util.Set; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ClassReferencesTest { + @Test + public void includesDeclarationOnlyFieldAndMethodTypes() { + ClassWriter writer = createClass(); + writer.visitField(Opcodes.ACC_PUBLIC, "field", "[[Lexample/FieldType;", null, null).visitEnd(); + writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, "method", + "(Lexample/Argument;[I)[Lexample/Result;", null, null).visitEnd(); + writer.visitEnd(); + + Set references = ClassReferences.read(writer.toByteArray()); + assertTrue(references.containsAll(Set.of("example/FieldType", "example/Argument", "example/Result"))); + assertFalse(references.contains("I")); + } + + @Test + public void includesMemberReferenceAndMethodTypeDescriptors() { + ClassWriter writer = createClass(); + writer.newMethod("example/Owner", "method", "(Lexample/Argument;)Lexample/Result;", false); + writer.newConst(Type.getMethodType("(Lexample/HandleArgument;)Lexample/HandleResult;")); + writer.visitEnd(); + + Set references = ClassReferences.read(writer.toByteArray()); + assertTrue(references.containsAll(Set.of("example/Owner", "example/Argument", "example/Result", + "example/HandleArgument", "example/HandleResult"))); + } + + @Test + public void doesNotTreatStringsOrAnnotationOnlyTypesAsLinkage() { + ClassWriter writer = createClass(); + writer.newConst("Lexample/StringValue;"); + writer.visitAnnotation("Lexample/Annotation;", true).visitEnd(); + writer.visitEnd(); + + Set references = ClassReferences.read(writer.toByteArray()); + assertFalse(references.contains("example/StringValue")); + assertFalse(references.contains("example/Annotation")); + } + + private static ClassWriter createClass() { + ClassWriter writer = new ClassWriter(0); + writer.visit(Opcodes.V25, Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, + "example/Consumer", null, "java/lang/Object", null); + return writer; + } +} diff --git a/buildSrc/src/test/java/JarCompactorTest.java b/buildSrc/src/test/java/JarCompactorTest.java deleted file mode 100644 index e1a9dfb06..000000000 --- a/buildSrc/src/test/java/JarCompactorTest.java +++ /dev/null @@ -1,180 +0,0 @@ -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; -import org.apache.commons.compress.archivers.zip.ZipFile; -import org.gradle.api.GradleException; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.security.MessageDigest; -import java.util.Enumeration; -import java.util.HexFormat; -import java.util.Map; -import java.util.TreeMap; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; - -public class JarCompactorTest { - @Rule - public final TemporaryFolder temporaryFolder = new TemporaryFolder(); - - @Test - public void preservesFilesAndOmitsDirectoryEntries() throws Exception { - File artifact = temporaryFolder.newFile("artifact.jar"); - byte[] content = "Iris artifact content".getBytes(StandardCharsets.UTF_8); - try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact))) { - output.putNextEntry(new JarEntry("example/")); - output.closeEntry(); - output.putNextEntry(new JarEntry("example/value.txt")); - output.write(content); - output.closeEntry(); - } - - JarCompactor.compact(artifact); - - try (JarFile jar = new JarFile(artifact)) { - assertNull(jar.getJarEntry("example/")); - assertArrayEquals(content, jar.getInputStream( - jar.getJarEntry("example/value.txt")).readAllBytes()); - } - } - - @Test - public void preservesJarIdentityAndProducesDeterministicSeekableOutput() throws Exception { - File artifact = temporaryFolder.newFile("identity.jar"); - try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact)); - InputStream classResource = getClass().getResourceAsStream("/JarCompactorTest.class")) { - output.setLevel(9); - writeEntry(output, "META-INF/MANIFEST.MF", "Manifest-Version: 1.0\r\nMulti-Release: true\r\n\r\n" - .getBytes(StandardCharsets.UTF_8)); - writeEntry(output, "JarCompactorTest.class", classResource.readAllBytes()); - writeEntry(output, "META-INF/iris/generation-kernels/abi-1.revision", "registry fixture".getBytes(StandardCharsets.UTF_8)); - writeEntry(output, "example/caf\u00e9.txt", "unicode name".getBytes(StandardCharsets.UTF_8)); - for (int index = 0; index < 100; index++) { - writeEntry(output, "example/item-" + index + ".txt", ("value-" + index).getBytes(StandardCharsets.UTF_8)); - } - } - Map original = identity(artifact); - long originalBytes = artifact.length(); - - JarCompactor.compact(artifact); - - assertEquals(original, identity(artifact)); - assertTrue(artifact.length() < originalBytes); - try (JarFile jar = new JarFile(artifact)) { - assertEquals("true", jar.getManifest().getMainAttributes().getValue("Multi-Release")); - assertEquals(0xCA, jar.getInputStream(jar.getJarEntry("JarCompactorTest.class")).read()); - } - try (ZipFile zip = ZipFile.builder().setFile(artifact).get()) { - Enumeration entries = zip.getEntries(); - while (entries.hasMoreElements()) { - assertFalse(entries.nextElement().getGeneralPurposeBit().usesDataDescriptor()); - } - } - byte[] first = Files.readAllBytes(artifact.toPath()); - JarCompactor.compact(artifact); - assertArrayEquals(first, Files.readAllBytes(artifact.toPath())); - } - - @Test - public void rejectsMalformedArchivesWithoutReplacingTheSource() throws Exception { - File artifact = temporaryFolder.newFile("invalid.jar"); - byte[] original = "not a zip archive".getBytes(StandardCharsets.UTF_8); - Files.write(artifact.toPath(), original); - - assertThrows(GradleException.class, () -> JarCompactor.compact(artifact)); - - assertArrayEquals(original, Files.readAllBytes(artifact.toPath())); - assertEquals(1, temporaryFolder.getRoot().listFiles().length); - } - - @Test - public void rejectsCorruptPayloadWithoutReplacingTheSource() throws Exception { - File artifact = temporaryFolder.newFile("corrupt.jar"); - byte[] payload = "checked-payload".getBytes(StandardCharsets.UTF_8); - CRC32 crc = new CRC32(); - crc.update(payload); - try (JarOutputStream output = new JarOutputStream(new FileOutputStream(artifact))) { - JarEntry entry = new JarEntry("value.txt"); - entry.setMethod(ZipEntry.STORED); - entry.setSize(payload.length); - entry.setCrc(crc.getValue()); - output.putNextEntry(entry); - output.write(payload); - output.closeEntry(); - } - byte[] corrupt = Files.readAllBytes(artifact.toPath()); - int payloadOffset = new String(corrupt, StandardCharsets.ISO_8859_1).indexOf("checked-payload"); - assertTrue(payloadOffset > 0); - corrupt[payloadOffset] ^= 1; - Files.write(artifact.toPath(), corrupt); - - assertThrows(GradleException.class, () -> JarCompactor.compact(artifact)); - - assertArrayEquals(corrupt, Files.readAllBytes(artifact.toPath())); - } - - @Test - public void rejectsDuplicateEntriesWithoutReplacingTheSource() throws Exception { - File artifact = temporaryFolder.newFile("duplicate.jar"); - try (ZipArchiveOutputStream output = new ZipArchiveOutputStream(artifact)) { - for (int index = 0; index < 2; index++) { - output.putArchiveEntry(new ZipArchiveEntry("duplicate.txt")); - output.write(index); - output.closeArchiveEntry(); - } - } - byte[] original = Files.readAllBytes(artifact.toPath()); - - assertThrows(GradleException.class, () -> JarCompactor.compact(artifact)); - - assertArrayEquals(original, Files.readAllBytes(artifact.toPath())); - } - - private static void writeEntry(JarOutputStream output, String name, byte[] content) throws Exception { - JarEntry entry = new JarEntry(name); - entry.setTime(315_532_800_000L); - entry.setComment("preserved entry comment"); - entry.setExtra(new byte[]{(byte) 0xFE, (byte) 0xCA, 1, 0, 7}); - output.putNextEntry(entry); - output.write(content); - output.closeEntry(); - } - - private static Map identity(File artifact) throws Exception { - Map entries = new TreeMap<>(); - try (JarFile jar = new JarFile(artifact)) { - Enumeration contents = jar.entries(); - while (contents.hasMoreElements()) { - JarEntry entry = contents.nextElement(); - byte[] bytes; - try (InputStream input = jar.getInputStream(entry)) { - bytes = input.readAllBytes(); - } - entries.put(entry.getName(), new EntryIdentity( - HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(bytes)), - entry.getSize(), entry.getCrc(), entry.getTime(), entry.getComment(), - entry.getExtra() == null ? "" : HexFormat.of().formatHex(entry.getExtra()))); - } - } - return entries; - } - - private record EntryIdentity(String sha256, long size, long crc, long time, String comment, String extra) { - } -} diff --git a/core/build.gradle b/core/build.gradle index 42711770d..9da211f23 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -415,10 +415,10 @@ List unusedVolmLibClasses = [ 'art/arcane/volmlib/util/collection/StateList', 'art/arcane/iris/core/pack/IrisPack', 'art/arcane/iris/core/pack/IrisPackRepository', - 'art/arcane/iris/core/jobs/DownloadJob', - 'art/arcane/iris/core/jobs/JobCollection', - 'art/arcane/iris/core/jobs/ParallelQueueJob', - 'art/arcane/iris/core/jobs/QueueJob', + 'art/arcane/iris/util/common/scheduling/jobs/DownloadJob', + 'art/arcane/iris/util/common/scheduling/jobs/JobCollection', + 'art/arcane/iris/util/common/scheduling/jobs/ParallelQueueJob', + 'art/arcane/iris/util/common/scheduling/jobs/QueueJob', 'art/arcane/iris/engine/framework/placer/WorldObjectPlacer', 'art/arcane/iris/engine/object/IPostBlockAccess', 'art/arcane/iris/engine/object/IrisPotionEffect', diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/CapabilitySnapshot.java b/core/src/main/java/art/arcane/iris/core/lifecycle/CapabilitySnapshot.java index 6edba406a..3c871d7bf 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/CapabilitySnapshot.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/CapabilitySnapshot.java @@ -46,7 +46,7 @@ public final class CapabilitySnapshot { private final Method removeLevelMethod; private final String paperLikeResolution; - private CapabilitySnapshot( + CapabilitySnapshot( ServerFamily serverFamily, boolean regionizedRuntime, Object worldsProvider, @@ -259,94 +259,6 @@ public final class CapabilitySnapshot { ); } - public static CapabilitySnapshot forTesting(ServerFamily serverFamily, boolean regionizedRuntime, boolean worldsProviderHealthy, boolean paperLikeRuntimeHealthy) { - Object minecraftServer = paperLikeRuntimeHealthy ? new TestingPaperLikeServer("datapack-registry", "server-registry") : null; - Method createLevelMethod = null; - Field worldLoaderContextField = null; - Method serverRegistryAccessMethod = null; - try { - createLevelMethod = paperLikeRuntimeHealthy - ? TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class) - : null; - worldLoaderContextField = paperLikeRuntimeHealthy - ? CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext") - : null; - serverRegistryAccessMethod = paperLikeRuntimeHealthy - ? CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class) - : null; - } catch (NoSuchMethodException | NoSuchFieldException e) { - throw new IllegalStateException(e); - } - return new CapabilitySnapshot( - serverFamily, - regionizedRuntime, - worldsProviderHealthy ? new Object() : null, - worldsProviderHealthy ? Object.class : null, - worldsProviderHealthy ? Object.class : null, - worldsProviderHealthy ? "test-provider" : "inactive", - null, - minecraftServer, - createLevelMethod, - paperLikeRuntimeHealthy ? PaperLikeFlavor.CURRENT_INFO_AND_DATA : PaperLikeFlavor.UNSUPPORTED, - null, - null, - null, - null, - null, - null, - worldLoaderContextField, - serverRegistryAccessMethod, - null, - null, - null, - null, - null, - null, - paperLikeRuntimeHealthy ? "available(test)" : "unsupported(test)" - ); - } - - public static CapabilitySnapshot forTestingRuntimeRegistries(ServerFamily serverFamily, boolean regionizedRuntime, Object datapackDimensions, Object serverRegistryAccess) { - TestingPaperLikeServer minecraftServer = new TestingPaperLikeServer(datapackDimensions, serverRegistryAccess); - Method createLevelMethod; - Field worldLoaderContextField; - Method registryAccessMethod; - try { - createLevelMethod = TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class); - worldLoaderContextField = CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext"); - registryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class); - } catch (NoSuchMethodException | NoSuchFieldException e) { - throw new IllegalStateException(e); - } - return new CapabilitySnapshot( - serverFamily, - regionizedRuntime, - null, - null, - null, - "inactive", - null, - minecraftServer, - createLevelMethod, - PaperLikeFlavor.CURRENT_INFO_AND_DATA, - null, - null, - null, - null, - null, - null, - worldLoaderContextField, - registryAccessMethod, - null, - null, - null, - null, - null, - null, - "available(test-runtime-registries)" - ); - } - public ServerFamily serverFamily() { return serverFamily; } @@ -578,35 +490,4 @@ public final class CapabilitySnapshot { return new Object[]{null, null, null, "inactive(service scan found nothing)"}; } - private static final class TestingPaperLikeServer { - private final TestingWorldLoaderContext worldLoaderContext; - private final Object registryAccess; - - private TestingPaperLikeServer(Object datapackDimensions, Object registryAccess) { - this.worldLoaderContext = new TestingWorldLoaderContext(datapackDimensions); - this.registryAccess = registryAccess; - } - - @SuppressWarnings("unused") - private void createLevel(Object levelStem, Object worldLoadingInfoAndData, Object worldDataAndGenSettings) { - } - - @SuppressWarnings("unused") - private Object registryAccess() { - return registryAccess; - } - } - - private static final class TestingWorldLoaderContext { - private final Object datapackDimensions; - - private TestingWorldLoaderContext(Object datapackDimensions) { - this.datapackDimensions = datapackDimensions; - } - - @SuppressWarnings("unused") - private Object datapackDimensions() { - return datapackDimensions; - } - } } diff --git a/core/src/main/java/art/arcane/iris/core/localization/IrisLanguage.java b/core/src/main/java/art/arcane/iris/core/localization/IrisLanguage.java index d2cbe6628..6c98df20e 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/IrisLanguage.java +++ b/core/src/main/java/art/arcane/iris/core/localization/IrisLanguage.java @@ -568,15 +568,20 @@ public final class IrisLanguage { }); } - private static void validateDownload(String locale, String raw) { - LocalizationValidator.validate(CATALOG, List.of(parseOverlay("download:" + locale, locale, raw))) - .throwIfInvalid(); + static void validateDownload(String locale, String raw) { + LocaleOverlay overlay = parseDownloadedOverlay("download:" + locale, locale, raw); + LocalizationValidator.validate(CATALOG, List.of(overlay)).throwIfInvalid(); + for (MessageKey key : CATALOG.keys()) { + if (overlay.value(key.id()) == null) { + throw new IllegalArgumentException("Downloaded Iris locale " + locale + " is incomplete: " + key.id()); + } + } } private static LocaleOverlay loadDownloadedOverlay(File root, String locale) { RemoteLanguageCatalog.CacheResult cached = remote(root).read(locale, IrisLanguage::validateDownload); if (cached.state() == RemoteLanguageCatalog.CacheState.VALID) { - return parseOverlay(cached.file().toString(), locale, cached.content()); + return parseDownloadedOverlay(cached.file().toString(), locale, cached.content()); } if (cached.failure() != null) { IrisLogging.error("Ignoring invalid downloaded Iris locale " + locale + "."); @@ -585,6 +590,17 @@ public final class IrisLanguage { return null; } + static LocaleOverlay parseDownloadedOverlay(String source, String locale, String raw) { + LocaleOverlay parsed = parseOverlay(source, locale, raw); + LocaleOverlay.Builder builder = LocaleOverlay.builder(source, locale); + for (Map.Entry entry : parsed.values().entrySet()) { + if (CATALOG.key(entry.getKey()) != null) { + builder.put(entry.getKey(), entry.getValue()); + } + } + return builder.build(); + } + static LocaleOverlay parseOverlay(String source, String locale, String raw) { JsonElement parsed = JsonParser.parseString(raw == null || raw.isBlank() ? "{}" : raw); if (!parsed.isJsonObject()) { diff --git a/core/src/main/java/art/arcane/iris/core/localization/IrisMessageContributor.java b/core/src/main/java/art/arcane/iris/core/localization/IrisMessageContributor.java new file mode 100644 index 000000000..94c81e700 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/localization/IrisMessageContributor.java @@ -0,0 +1,9 @@ +package art.arcane.iris.core.localization; + +import art.arcane.volmlib.util.localization.MessageKey; + +import java.util.List; + +public interface IrisMessageContributor { + List keys(); +} diff --git a/core/src/main/java/art/arcane/iris/core/localization/IrisMessages.java b/core/src/main/java/art/arcane/iris/core/localization/IrisMessages.java index 71561b14b..8db0e3b12 100644 --- a/core/src/main/java/art/arcane/iris/core/localization/IrisMessages.java +++ b/core/src/main/java/art/arcane/iris/core/localization/IrisMessages.java @@ -7,8 +7,14 @@ import art.arcane.volmlib.util.localization.TextKey; import art.arcane.volmlib.util.localization.VolmitLocales; import java.util.List; +import java.util.ServiceLoader; public final class IrisMessages { + public static final TextKey PREGEN_ALREADY_RUNNING = TextKey.of( + "iris.modded.irismoddedcommands.pregeneration_task_is_already_running_stop_it_first_with_iris", + "A pregeneration task is already running. Stop it first with /iris pregen stop." + ); + public static final TextKey COMMAND_PERMISSION_DENIED = TextKey.of( "iris.command.permission_denied", "You lack the permission '{permission}'" @@ -67,6 +73,7 @@ public final class IrisMessages { ); private static final List RUNTIME_KEYS = List.of( + PREGEN_ALREADY_RUNNING, COMMAND_PERMISSION_DENIED, COMMAND_UNKNOWN, COMMAND_PLAYER_ONLY, @@ -101,16 +108,16 @@ public final class IrisMessages { builder.addAll(RUNTIME_KEYS); builder.addAll(BukkitCommandMessages.keys()); builder.addAll(BukkitCommandMessagesExtended.keys()); - builder.addAll(ModdedCommandMessages.keys()); builder.addAll(DirectorCommandMessages.keys()); - builder.addAll(ModdedHelpMessages.keys()); builder.addAll(RuntimeUiMessages.keys()); builder.addAll(BukkitRuntimeMessages.keys()); builder.addAll(RuntimeProgressMessages.keys()); builder.addAll(PackDownloadMessages.keys()); - builder.addAll(ClientUiMessages.keys()); builder.addAll(BukkitUiMessages.keys()); builder.addAll(DesktopUiMessages.keys()); + for (IrisMessageContributor contributor : ServiceLoader.load(IrisMessageContributor.class, IrisMessages.class.getClassLoader())) { + builder.addAll(contributor.keys()); + } return builder.build(); } } diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/BukkitPublicBackendTest.java b/core/src/test/java/art/arcane/iris/core/lifecycle/BukkitPublicBackendTest.java index ff491676c..f9a9da581 100644 --- a/core/src/test/java/art/arcane/iris/core/lifecycle/BukkitPublicBackendTest.java +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/BukkitPublicBackendTest.java @@ -41,7 +41,7 @@ public class BukkitPublicBackendTest { worldName, worldKey, World.Environment.NORMAL, generator, biomeProvider, WorldType.NORMAL, true, false, 123L, false, false, WorldLifecycleCaller.CREATE); BukkitPublicBackend backend = new BukkitPublicBackend( - CapabilitySnapshot.forTesting(ServerFamily.PAPER, false, false, false)); + CapabilitySnapshotFixtures.forTesting(ServerFamily.PAPER, false, false, false)); doAnswer(invocation -> { assertSame(generator, WorldLifecycleStaging.peekStemGenerator(worldName)); diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/CapabilitySnapshotFixtures.java b/core/src/test/java/art/arcane/iris/core/lifecycle/CapabilitySnapshotFixtures.java new file mode 100644 index 000000000..51a4db7ee --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/CapabilitySnapshotFixtures.java @@ -0,0 +1,131 @@ +package art.arcane.iris.core.lifecycle; + +import art.arcane.iris.core.lifecycle.CapabilitySnapshot.PaperLikeFlavor; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +final class CapabilitySnapshotFixtures { + private CapabilitySnapshotFixtures() { + } + + static CapabilitySnapshot forTesting(ServerFamily serverFamily, boolean regionizedRuntime, boolean worldsProviderHealthy, boolean paperLikeRuntimeHealthy) { + Object minecraftServer = paperLikeRuntimeHealthy ? new TestingPaperLikeServer("datapack-registry", "server-registry") : null; + Method createLevelMethod = null; + Field worldLoaderContextField = null; + Method serverRegistryAccessMethod = null; + try { + createLevelMethod = paperLikeRuntimeHealthy + ? TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class) + : null; + worldLoaderContextField = paperLikeRuntimeHealthy + ? CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext") + : null; + serverRegistryAccessMethod = paperLikeRuntimeHealthy + ? CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class) + : null; + } catch (NoSuchMethodException | NoSuchFieldException e) { + throw new IllegalStateException(e); + } + return new CapabilitySnapshot( + serverFamily, + regionizedRuntime, + worldsProviderHealthy ? new Object() : null, + worldsProviderHealthy ? Object.class : null, + worldsProviderHealthy ? Object.class : null, + worldsProviderHealthy ? "test-provider" : "inactive", + null, + minecraftServer, + createLevelMethod, + paperLikeRuntimeHealthy ? PaperLikeFlavor.CURRENT_INFO_AND_DATA : PaperLikeFlavor.UNSUPPORTED, + null, + null, + null, + null, + null, + null, + worldLoaderContextField, + serverRegistryAccessMethod, + null, + null, + null, + null, + null, + null, + paperLikeRuntimeHealthy ? "available(test)" : "unsupported(test)" + ); + } + + static CapabilitySnapshot forTestingRuntimeRegistries(ServerFamily serverFamily, boolean regionizedRuntime, Object datapackDimensions, Object serverRegistryAccess) { + TestingPaperLikeServer minecraftServer = new TestingPaperLikeServer(datapackDimensions, serverRegistryAccess); + Method createLevelMethod; + Field worldLoaderContextField; + Method registryAccessMethod; + try { + createLevelMethod = TestingPaperLikeServer.class.getDeclaredMethod("createLevel", Object.class, Object.class, Object.class); + worldLoaderContextField = CapabilityResolution.resolveField(TestingPaperLikeServer.class, "worldLoaderContext"); + registryAccessMethod = CapabilityResolution.resolveServerRegistryAccessMethod(TestingPaperLikeServer.class); + } catch (NoSuchMethodException | NoSuchFieldException e) { + throw new IllegalStateException(e); + } + return new CapabilitySnapshot( + serverFamily, + regionizedRuntime, + null, + null, + null, + "inactive", + null, + minecraftServer, + createLevelMethod, + PaperLikeFlavor.CURRENT_INFO_AND_DATA, + null, + null, + null, + null, + null, + null, + worldLoaderContextField, + registryAccessMethod, + null, + null, + null, + null, + null, + null, + "available(test-runtime-registries)" + ); + } + + private static final class TestingPaperLikeServer { + private final TestingWorldLoaderContext worldLoaderContext; + private final Object registryAccess; + + private TestingPaperLikeServer(Object datapackDimensions, Object registryAccess) { + this.worldLoaderContext = new TestingWorldLoaderContext(datapackDimensions); + this.registryAccess = registryAccess; + } + + @SuppressWarnings("unused") + private void createLevel(Object levelStem, Object worldLoadingInfoAndData, Object worldDataAndGenSettings) { + } + + @SuppressWarnings("unused") + private Object registryAccess() { + return registryAccess; + } + } + + private static final class TestingWorldLoaderContext { + private final Object datapackDimensions; + + private TestingWorldLoaderContext(Object datapackDimensions) { + this.datapackDimensions = datapackDimensions; + } + + @SuppressWarnings("unused") + private Object datapackDimensions() { + return datapackDimensions; + } + } +} diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleDiagnosticsTest.java b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleDiagnosticsTest.java index 69a7e07ff..129cb38d4 100644 --- a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleDiagnosticsTest.java +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleDiagnosticsTest.java @@ -15,7 +15,7 @@ import static org.junit.Assert.fail; public class WorldLifecycleDiagnosticsTest { @Test public void studioCreateSelectionFailurePrintsFullStacktrace() { - WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PAPER, false, false, false)); + WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshotFixtures.forTesting(ServerFamily.PAPER, false, false, false)); WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); ByteArrayOutputStream output = new ByteArrayOutputStream(); diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java index 3f72b9a75..59a026d8b 100644 --- a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleRuntimeLevelStemTest.java @@ -24,7 +24,7 @@ public class WorldLifecycleRuntimeLevelStemTest { public void runtimeStemUsesFullServerRegistryAccessForPlatformGenerators() throws Exception { Object datapackDimensions = new MissingDimensionTypeRegistry(); Object serverRegistryAccess = new Object(); - CapabilitySnapshot capabilities = CapabilitySnapshot.forTestingRuntimeRegistries(ServerFamily.PURPUR, false, datapackDimensions, serverRegistryAccess); + CapabilitySnapshot capabilities = CapabilitySnapshotFixtures.forTestingRuntimeRegistries(ServerFamily.PURPUR, false, datapackDimensions, serverRegistryAccess); WorldLifecycleRequest request = new WorldLifecycleRequest( "studio", NamespacedKey.minecraft("studio"), diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleSelectionTest.java b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleSelectionTest.java index f20fe5f91..4ee9ac57c 100644 --- a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleSelectionTest.java +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleSelectionTest.java @@ -21,7 +21,7 @@ public class WorldLifecycleSelectionTest { @Test public void studioSelectsPaperLikeBackendOnPaper() { - WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PAPER, false, false, true)); + WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshotFixtures.forTesting(ServerFamily.PAPER, false, false, true)); WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); @@ -29,7 +29,7 @@ public class WorldLifecycleSelectionTest { @Test public void studioSelectsPaperLikeBackendOnPurpur() { - WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true)); + WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshotFixtures.forTesting(ServerFamily.PURPUR, false, false, true)); WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); @@ -37,7 +37,7 @@ public class WorldLifecycleSelectionTest { @Test public void studioSelectsPaperLikeBackendOnCanvas() { - WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.CANVAS, true, false, true)); + WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshotFixtures.forTesting(ServerFamily.CANVAS, true, false, true)); WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); @@ -45,7 +45,7 @@ public class WorldLifecycleSelectionTest { @Test public void studioSelectsPaperLikeBackendOnFolia() { - WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, true, false, true)); + WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshotFixtures.forTesting(ServerFamily.FOLIA, true, false, true)); WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); @@ -53,7 +53,7 @@ public class WorldLifecycleSelectionTest { @Test public void studioSelectsBukkitBackendOnSpigot() { - WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.SPIGOT, false, false, false)); + WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshotFixtures.forTesting(ServerFamily.SPIGOT, false, false, false)); WorldLifecycleRequest request = new WorldLifecycleRequest("studio", NamespacedKey.minecraft("studio"), World.Environment.NORMAL, null, null, null, true, false, 1337L, true, false, WorldLifecycleCaller.STUDIO); assertEquals("bukkit_public", service.selectCreateBackend(request).backendName()); @@ -61,7 +61,7 @@ public class WorldLifecycleSelectionTest { @Test public void persistentCreatePrefersBukkitBackendOnPaperLikeServers() { - WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true)); + WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshotFixtures.forTesting(ServerFamily.PURPUR, false, false, true)); WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", NamespacedKey.minecraft("persistent"), World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); assertEquals("bukkit_public", service.selectCreateBackend(request).backendName()); @@ -69,7 +69,7 @@ public class WorldLifecycleSelectionTest { @Test public void persistentCreateSelectsPaperLikeBackendOnFolia() { - WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, true, false, true)); + WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshotFixtures.forTesting(ServerFamily.FOLIA, true, false, true)); WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", NamespacedKey.minecraft("persistent"), World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); @@ -77,7 +77,7 @@ public class WorldLifecycleSelectionTest { @Test public void persistentCreateSelectsPaperLikeBackendOnCanvas() { - WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.CANVAS, true, false, true)); + WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshotFixtures.forTesting(ServerFamily.CANVAS, true, false, true)); WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", NamespacedKey.minecraft("persistent"), World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); assertEquals("paper_like_runtime", service.selectCreateBackend(request).backendName()); @@ -85,7 +85,7 @@ public class WorldLifecycleSelectionTest { @Test public void persistentCreateFailsClosedOnFoliaWithoutRuntime() { - WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.FOLIA, false, false, false)); + WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshotFixtures.forTesting(ServerFamily.FOLIA, false, false, false)); WorldLifecycleRequest request = new WorldLifecycleRequest("persistent", NamespacedKey.minecraft("persistent"), World.Environment.NORMAL, null, null, null, true, false, 1337L, false, false, WorldLifecycleCaller.CREATE); assertThrows(IllegalStateException.class, () -> service.selectCreateBackend(request)); @@ -93,7 +93,7 @@ public class WorldLifecycleSelectionTest { @Test public void unloadUsesRememberedBackendFamily() { - WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, true)); + WorldLifecycleService service = new WorldLifecycleService(CapabilitySnapshotFixtures.forTesting(ServerFamily.PURPUR, false, false, true)); service.rememberBackend(NamespacedKey.minecraft("studio"), "paper_like_runtime"); assertEquals("paper_like_runtime", service.selectUnloadBackend("minecraft:studio").backendName()); @@ -104,7 +104,7 @@ public class WorldLifecycleSelectionTest { CountingBackend selected = new CountingBackend("selected", true); CountingBackend inactive = new CountingBackend("inactive", false); WorldLifecycleService service = new WorldLifecycleService( - CapabilitySnapshot.forTesting(ServerFamily.PAPER, false, false, true), + CapabilitySnapshotFixtures.forTesting(ServerFamily.PAPER, false, false, true), selected, inactive, inactive); diff --git a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleUnloadAsyncTest.java b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleUnloadAsyncTest.java index f3af9776a..45c433f33 100644 --- a/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleUnloadAsyncTest.java +++ b/core/src/test/java/art/arcane/iris/core/lifecycle/WorldLifecycleUnloadAsyncTest.java @@ -279,7 +279,7 @@ public class WorldLifecycleUnloadAsyncTest { ) { ControlledBackend inactive = new ControlledBackend("inactive"); return new WorldLifecycleService( - CapabilitySnapshot.forTesting(ServerFamily.PURPUR, false, false, false), + CapabilitySnapshotFixtures.forTesting(ServerFamily.PURPUR, false, false, false), inactive, remembered, fallback diff --git a/core/src/test/java/art/arcane/iris/core/localization/IrisDownloadValidationTest.java b/core/src/test/java/art/arcane/iris/core/localization/IrisDownloadValidationTest.java new file mode 100644 index 000000000..84251d6a7 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/localization/IrisDownloadValidationTest.java @@ -0,0 +1,158 @@ +package art.arcane.iris.core.localization; + +import art.arcane.volmlib.util.localization.LocaleOverlay; +import art.arcane.volmlib.util.localization.LocalizationValidator; +import art.arcane.volmlib.util.localization.RemoteLanguageCatalog; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class IrisDownloadValidationTest { + private static final String LOCALE = "de_DE"; + private static final String MISSING_KEY = "iris.director.commanddeveloper.director.stage_world_generation_update"; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void incompleteCacheDownloadsCompleteCatalogAndReusesIt() throws Exception { + String complete = completeCatalog(); + AtomicInteger requests = new AtomicInteger(); + HttpServer server = server(complete, requests); + try (URLClassLoader resources = resources(); + RemoteLanguageCatalog catalog = catalog(server, resources)) { + Path cache = catalog.cacheFile(LOCALE); + Files.createDirectories(cache.getParent()); + Files.writeString(cache, incompleteCatalog(complete)); + + RemoteLanguageCatalog.CacheResult initial = catalog.read(LOCALE, IrisLanguage::validateDownload); + assertEquals(RemoteLanguageCatalog.CacheState.INVALID, initial.state()); + assertTrue(initial.failure().getMessage().contains(MISSING_KEY)); + assertEquals(0, requests.get()); + + assertEquals(complete, catalog.readOrDownload(LOCALE, IrisLanguage::validateDownload)); + assertEquals(complete, Files.readString(cache)); + assertEquals(RemoteLanguageCatalog.CacheState.VALID, + catalog.read(LOCALE, IrisLanguage::validateDownload).state()); + assertEquals(1, requests.get()); + + assertEquals(complete, catalog.readOrDownload(LOCALE, IrisLanguage::validateDownload)); + assertEquals(1, requests.get()); + } finally { + server.stop(0); + } + } + + @Test + public void incompleteUpstreamDoesNotReplaceAnInvalidCache() throws Exception { + String incomplete = incompleteCatalog(completeCatalog()); + AtomicInteger requests = new AtomicInteger(); + HttpServer server = server(incomplete, requests); + try (URLClassLoader resources = resources(); + RemoteLanguageCatalog catalog = catalog(server, resources)) { + Path cache = catalog.cacheFile(LOCALE); + Files.createDirectories(cache.getParent()); + String cached = "{\"locale\":\"de_DE\",\"messages\":{}}"; + Files.writeString(cache, cached); + + IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, + () -> catalog.readOrDownload(LOCALE, IrisLanguage::validateDownload)); + assertTrue(failure.getMessage().contains(MISSING_KEY)); + assertEquals(cached, Files.readString(cache)); + assertEquals(1, requests.get()); + } finally { + server.stop(0); + } + } + + @Test + public void incompleteUpstreamDoesNotCreateACacheFile() throws Exception { + AtomicInteger requests = new AtomicInteger(); + HttpServer server = server(incompleteCatalog(completeCatalog()), requests); + try (URLClassLoader resources = resources(); + RemoteLanguageCatalog catalog = catalog(server, resources)) { + assertThrows(IllegalArgumentException.class, + () -> catalog.readOrDownload(LOCALE, IrisLanguage::validateDownload)); + assertFalse(Files.exists(catalog.cacheFile(LOCALE))); + assertEquals(1, requests.get()); + } finally { + server.stop(0); + } + } + + @Test + public void partialOperatorOverridesRemainValid() { + LocaleOverlay overlay = IrisLanguage.parseOverlay("override", LOCALE, """ + {"locale":"de_DE","messages":{"iris.command.unknown":"Unbekannter Iris-Befehl"}} + """); + + assertEquals(1, overlay.values().size()); + assertTrue(LocalizationValidator.validate(IrisLanguage.catalog(), List.of(overlay)).errors().isEmpty()); + } + + private String completeCatalog() throws IOException { + return Files.readString(Path.of("src/main/resources/languages", LOCALE + ".json")); + } + + private String incompleteCatalog(String complete) { + JsonObject document = JsonParser.parseString(complete).getAsJsonObject(); + assertTrue(document.getAsJsonObject("messages").remove(MISSING_KEY) != null); + return document.toString(); + } + + private HttpServer server(String content, AtomicInteger requests) throws IOException { + byte[] response = content.getBytes(StandardCharsets.UTF_8); + HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/master/languages/de_DE.json", exchange -> respond(exchange, response, requests)); + server.start(); + return server; + } + + private void respond(HttpExchange exchange, byte[] response, AtomicInteger requests) throws IOException { + requests.incrementAndGet(); + try { + exchange.sendResponseHeaders(200, response.length); + exchange.getResponseBody().write(response); + } finally { + exchange.close(); + } + } + + private URLClassLoader resources() throws IOException { + Path resources = temporaryFolder.newFolder().toPath(); + Files.writeString(resources.resolve("source.properties"), "revision=master\nlocales=de_DE\n"); + return new URLClassLoader(new URL[]{resources.toUri().toURL()}, null); + } + + private RemoteLanguageCatalog catalog(HttpServer server, ClassLoader resources) throws IOException { + return RemoteLanguageCatalog.load(new RemoteLanguageCatalog.Options( + "Iris", + URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/"), + "languages", + ".json", + "source.properties", + temporaryFolder.newFolder().toPath(), + resources + )); + } +} diff --git a/core/src/test/java/art/arcane/iris/core/localization/IrisLanguageTest.java b/core/src/test/java/art/arcane/iris/core/localization/IrisLanguageTest.java index ba8062d86..79425b4c2 100644 --- a/core/src/test/java/art/arcane/iris/core/localization/IrisLanguageTest.java +++ b/core/src/test/java/art/arcane/iris/core/localization/IrisLanguageTest.java @@ -128,7 +128,32 @@ public class IrisLanguageTest { private LocaleOverlay loadSourceOverlay(String locale) throws Exception { Path source = Path.of("src/main/resources/languages", locale + ".json"); - return IrisLanguage.parseOverlay(source.toString(), locale, Files.readString(source)); + return IrisLanguage.parseDownloadedOverlay(source.toString(), locale, Files.readString(source)); + } + + @Test + public void downloadedCatalogSelectsBukkitKeysWithoutAcceptingUnknownOverrides() { + String raw = """ + {"locale":"de_DE","messages":{ + "iris.command.unknown":"Unbekannter Iris-Befehl", + "iris.modded.help.entry.command.version":"Version anzeigen" + }} + """; + LocaleOverlay downloaded = IrisLanguage.parseDownloadedOverlay("download", "de_DE", raw); + assertEquals(Set.of(IrisMessages.COMMAND_UNKNOWN.id()), downloaded.values().keySet()); + assertTrue(LocalizationValidator.validate(IrisLanguage.catalog(), List.of(downloaded)).errors().isEmpty()); + LocaleOverlay override = IrisLanguage.parseOverlay("override", "de_DE", raw); + assertFalse(LocalizationValidator.validate(IrisLanguage.catalog(), List.of(override)).errors().isEmpty()); + assertEquals("A pregeneration task is already running. Stop it first with /iris pregen stop.", + IrisLanguage.plain(IrisMessages.PREGEN_ALREADY_RUNNING)); + } + + @Test + public void downloadedCatalogStillRejectsInvalidActivePlaceholders() { + LocaleOverlay downloaded = IrisLanguage.parseDownloadedOverlay("download", "de_DE", """ + {"locale":"de_DE","messages":{"iris.command.permission_denied":"Keine Erlaubnis"}} + """); + assertFalse(LocalizationValidator.validate(IrisLanguage.catalog(), List.of(downloaded)).errors().isEmpty()); } @Test @@ -231,7 +256,8 @@ public class IrisLanguageTest { } assertValueIntegrity(locale, key.id(), key.englishValue(), overlay.value(key.id())); } - assertTrue(locale + " contains too many English placeholder values", translated >= 1050); + assertTrue(locale + " contains too many English placeholder values", + translated * 10 >= IrisLanguage.catalog().keys().size() * 7); } } diff --git a/gradle/volmlib-resolution.settings.gradle b/gradle/volmlib-resolution.settings.gradle index 3ad9c67dc..adc6a88e9 100644 --- a/gradle/volmlib-resolution.settings.gradle +++ b/gradle/volmlib-resolution.settings.gradle @@ -92,6 +92,7 @@ if (irisUseLocalVolmLib && irisLocalVolmLibDirectory != null) { dependencySubstitution { substitute(module('com.github.VolmitSoftware:VolmLib')).using(project(':shared')) substitute(module('com.github.VolmitSoftware.VolmLib:shared')).using(project(':shared')) + substitute(module('com.github.VolmitSoftware.VolmLib:packaging')).using(project(':packaging')) substitute(module('com.github.VolmitSoftware.VolmLib:volmlib-shared')).using(project(':shared')) } }