Dicing things up a little

- Trying to slim this beauty down to a size 6.
- Shes a size 7.1
This commit is contained in:
Brian Neumann-Fopiano
2026-09-05 16:45:31 -04:00
parent d10d70a894
commit f2887632fe
50 changed files with 668 additions and 491 deletions
+5 -3
View File
@@ -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.
@@ -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;
}
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
+23
View File
@@ -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/']
}
}
}
+23
View File
@@ -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/']
}
}
}
@@ -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;
@@ -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;
@@ -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 {
@@ -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<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
@@ -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;
@@ -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 {
@@ -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;
@@ -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 {
@@ -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<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
@@ -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());
@@ -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;
@@ -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<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
@@ -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<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
@@ -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;
@@ -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 {
@@ -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;
@@ -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}" + " <dimension>; 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,
@@ -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;
@@ -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<MessageKey> keys() {
List<MessageKey> keys = new ArrayList<>();
keys.addAll(ModdedCommandMessages.keys());
keys.addAll(ModdedHelpMessages.keys());
keys.addAll(ClientUiMessages.keys());
return List.copyOf(keys);
}
}
@@ -0,0 +1 @@
art.arcane.iris.modded.localization.ModdedMessageContributor
@@ -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());
}
}
}
+23
View File
@@ -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/']
}
}
}
+27 -4
View File
@@ -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/']
}
}
}
+52 -4
View File
@@ -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<Integer> classNameIndexes = new ArrayList<>();
List<Integer> 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<String> 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;
-121
View File
@@ -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<String> names = new HashSet<>();
Enumeration<? extends ZipEntry> 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);
}
}
}
@@ -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<String> 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<String> 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<String> 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;
}
}
@@ -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<String, EntryIdentity> 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<ZipArchiveEntry> 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<String, EntryIdentity> identity(File artifact) throws Exception {
Map<String, EntryIdentity> entries = new TreeMap<>();
try (JarFile jar = new JarFile(artifact)) {
Enumeration<JarEntry> 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) {
}
}
+4 -4
View File
@@ -415,10 +415,10 @@ List<String> 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',
@@ -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;
}
}
}
@@ -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<String, MessageValue> 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()) {
@@ -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<MessageKey> keys();
}
@@ -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<MessageKey> 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();
}
}
@@ -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));
@@ -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;
}
}
}
@@ -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();
@@ -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"),
@@ -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);
@@ -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
@@ -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
));
}
}
@@ -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);
}
}
@@ -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'))
}
}