From f482b8ef7efa9277599b81d4c4cb8f1a1d51e2d4 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Fri, 12 Jun 2026 15:52:01 -0400 Subject: [PATCH] laDEDA --- adapters/fabric/build.gradle | 10 +- .../iris/fabric/IrisFabricBootstrap.java | 1 - .../fabric/src/main/resources/fabric.mod.json | 2 +- adapters/forge/build.gradle | 18 +- .../arcane/iris/forge/IrisForgeBootstrap.java | 1 - .../src/main/resources/META-INF/mods.toml | 2 +- .../iris/modded/IrisModdedChunkGenerator.java | 2 +- .../arcane/iris/modded/ModdedBiomeWriter.java | 42 +++- .../iris/modded/ModdedPackInstaller.java | 17 -- .../arcane/iris/modded/ModdedPlatform.java | 2 +- .../arcane/iris/modded/ModdedWorldCheck.java | 54 +++-- .../modded/command/IrisModdedCommands.java | 113 ++++++--- .../modded/command/ModdedCommandFeedback.java | 97 ++++++++ .../modded/command/ModdedCommandHelp.java | 228 ++++++++++++++++++ .../command/ModdedDatapackCommands.java | 16 +- .../iris/modded/command/ModdedGoldenHash.java | 7 +- .../modded/command/ModdedObjectCommands.java | 16 +- .../modded/command/ModdedPackCommands.java | 17 +- .../iris/modded/command/ModdedRegen.java | 7 +- .../command/ModdedStructureCommands.java | 18 +- .../modded/command/ModdedStudioCommands.java | 21 +- .../modded/command/ModdedWorldCommands.java | 187 ++++++++++++++ .../command/ModdedWorldDatapackWriter.java | 217 +++++++++++++++++ .../data/minecraft/dimension/overworld.json | 11 - adapters/neoforge/build.gradle | 10 +- .../iris/neoforge/IrisNeoForgeBootstrap.java | 1 - .../resources/META-INF/neoforge.mods.toml | 2 +- build.gradle | 81 ++++++- .../nms/datapack/v1217/DataFixerV1217.java | 23 +- .../engine/actuator/IrisBiomeActuator.java | 15 +- .../iris/engine/object/IrisBiomeCustom.java | 2 +- gradle.properties | 3 + 32 files changed, 1115 insertions(+), 128 deletions(-) create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java create mode 100644 adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldDatapackWriter.java delete mode 100644 adapters/modded-common/src/main/resources/data/minecraft/dimension/overworld.json diff --git a/adapters/fabric/build.gradle b/adapters/fabric/build.gradle index 42f61703b..f5618e644 100644 --- a/adapters/fabric/build.gradle +++ b/adapters/fabric/build.gradle @@ -30,7 +30,10 @@ Properties rootProperties = new Properties() file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.1')) String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.1.2')) -String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse('0.19.3') +String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse(rootProperties.getProperty('fabricLoaderVersion', '0.19.3')) +Closure irisArtifactName = { String platform, String targetVersion -> + return "Iris v${project.version} [${platform}] ${targetVersion}.jar" +} group = 'art.arcane' version = irisVersion @@ -177,7 +180,10 @@ processResources { } tasks.named('shadowJar', ShadowJar).configure { - archiveFileName.set("Iris-${project.version}+mc${minecraftVersion}-fabric.jar") + doFirst { + delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-fabric.jar")) + } + archiveFileName.set(irisArtifactName('Fabric', "${minecraftVersion}+${fabricLoaderVersion}")) configurations = [project.configurations.named('bundle').get()] mergeServiceFiles() exclude('META-INF/maven/**') diff --git a/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java index 293e36ae4..ba34383d2 100644 --- a/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java +++ b/adapters/fabric/src/main/java/art/arcane/iris/fabric/IrisFabricBootstrap.java @@ -58,7 +58,6 @@ public final class IrisFabricBootstrap implements ModInitializer { @Override public void onInitialize() { ModdedEngineBootstrap.initialize(new FabricModdedLoader()); - art.arcane.iris.modded.ModdedPackInstaller.ensureDefaultPack(ModdedEngineBootstrap.loader().configDir()); FabricLoader loader = FabricLoader.getInstance(); String modVersion = loader.getModContainer("irisworldgen") .map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString()) diff --git a/adapters/fabric/src/main/resources/fabric.mod.json b/adapters/fabric/src/main/resources/fabric.mod.json index 3c272e961..8e4f739d2 100644 --- a/adapters/fabric/src/main/resources/fabric.mod.json +++ b/adapters/fabric/src/main/resources/fabric.mod.json @@ -3,7 +3,7 @@ "id": "irisworldgen", "version": "${version}", "name": "Iris", - "description": "Iris World Generation Engine (Fabric adapter - native chunk generator, overworld override datapack, engine lifecycle)", + "description": "Iris World Generation Engine (Fabric adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)", "authors": ["Arcane Arts (Volmit Software)"], "contact": { "sources": "https://github.com/VolmitSoftware/Iris" diff --git a/adapters/forge/build.gradle b/adapters/forge/build.gradle index afa31030a..56767749d 100644 --- a/adapters/forge/build.gradle +++ b/adapters/forge/build.gradle @@ -30,7 +30,18 @@ Properties rootProperties = new Properties() file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.1')) String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.1.2')) -String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.1.2-64.0.9') +String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse(rootProperties.getProperty('forgeVersion', '26.1.2-64.0.9')) +Closure loaderDisplayVersion = { String loaderVersion -> + String coordinatePrefix = "${minecraftVersion}-" + if (loaderVersion.startsWith(coordinatePrefix)) { + return loaderVersion.substring(coordinatePrefix.length()) + } + + return loaderVersion +} +Closure irisArtifactName = { String platform, String targetVersion -> + return "Iris v${project.version} [${platform}] ${targetVersion}.jar" +} group = 'art.arcane' version = irisVersion @@ -198,7 +209,10 @@ processResources { } tasks.named('shadowJar', ShadowJar).configure { - archiveFileName.set("Iris-${project.version}+mc${minecraftVersion}-forge.jar") + doFirst { + delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-forge.jar")) + } + archiveFileName.set(irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}")) configurations = [project.configurations.named('bundle').get()] mergeServiceFiles() exclude('META-INF/maven/**') diff --git a/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java index 963647b53..fbf382e67 100644 --- a/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java +++ b/adapters/forge/src/main/java/art/arcane/iris/forge/IrisForgeBootstrap.java @@ -51,7 +51,6 @@ public final class IrisForgeBootstrap { public IrisForgeBootstrap(FMLJavaModLoadingContext context) { ModdedEngineBootstrap.initialize(new ForgeModdedLoader()); - art.arcane.iris.modded.ModdedPackInstaller.ensureDefaultPack(ModdedEngineBootstrap.loader().configDir()); String modVersion = ModList.getModContainerById("irisworldgen") .map((ModContainer container) -> container.getModInfo().getVersion().toString()) .orElse("unknown"); diff --git a/adapters/forge/src/main/resources/META-INF/mods.toml b/adapters/forge/src/main/resources/META-INF/mods.toml index 4c81dfc79..71a190e47 100644 --- a/adapters/forge/src/main/resources/META-INF/mods.toml +++ b/adapters/forge/src/main/resources/META-INF/mods.toml @@ -6,7 +6,7 @@ license = "GPL-3.0" modId = "irisworldgen" version = "${version}" displayName = "Iris" -description = "Iris World Generation Engine (Forge adapter - native chunk generator, overworld override datapack, engine lifecycle)" +description = "Iris World Generation Engine (Forge adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)" authors = "Arcane Arts (Volmit Software)" [[dependencies.irisworldgen]] diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java index e88bc42e3..e52748406 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/IrisModdedChunkGenerator.java @@ -72,7 +72,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator { private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); public static final MapCodec CODEC = RecordCodecBuilder.mapCodec((RecordCodecBuilder.Instance instance) -> instance.group( BiomeSource.CODEC.fieldOf("biome_source").forGetter((IrisModdedChunkGenerator generator) -> generator.biomeSource), - Codec.STRING.optionalFieldOf("dimension", "overworld").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey) + Codec.STRING.fieldOf("dimension").forGetter((IrisModdedChunkGenerator generator) -> generator.dimensionKey) ).apply(instance, IrisModdedChunkGenerator::new)); private final String dimensionKey; diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomeWriter.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomeWriter.java index 8160c4807..b18926ef1 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomeWriter.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedBiomeWriter.java @@ -20,17 +20,55 @@ package art.arcane.iris.modded; import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBiomeWriter; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.biome.Biome; +import java.util.ArrayList; import java.util.List; +import java.util.function.Supplier; public final class ModdedBiomeWriter implements PlatformBiomeWriter { + private final Supplier server; + + public ModdedBiomeWriter(Supplier server) { + this.server = server; + } + @Override public int biomeIdFor(String key) { - return 0; + Registry registry = biomeRegistry(); + Identifier identifier = Identifier.tryParse(key); + if (registry == null || identifier == null) { + return 0; + } + Biome biome = registry.getValue(identifier); + return biome == null ? 0 : registry.getId(biome); } @Override public List allBiomes() { - return List.of(); + Registry registry = biomeRegistry(); + List biomes = new ArrayList<>(); + if (registry == null) { + return biomes; + } + for (Identifier identifier : registry.keySet()) { + Biome biome = registry.getValue(identifier); + if (biome != null) { + biomes.add(ModdedBiome.of(biome, identifier.toString())); + } + } + return biomes; + } + + private Registry biomeRegistry() { + MinecraftServer instance = server.get(); + if (instance == null) { + return null; + } + return instance.registryAccess().lookupOrThrow(Registries.BIOME); } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPackInstaller.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPackInstaller.java index a8ae8e2d1..1808536dc 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPackInstaller.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPackInstaller.java @@ -40,29 +40,12 @@ import java.util.stream.Stream; public final class ModdedPackInstaller { private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); - private static final String DEFAULT_PACK = "overworld"; - private static final String DEFAULT_BRANCH = "master"; private static final Pattern PACK_NAME = Pattern.compile("[a-z0-9_-]+"); private static final Pattern BRANCH_NAME = Pattern.compile("[A-Za-z0-9._-]+"); private ModdedPackInstaller() { } - public static void ensureDefaultPack(Path configDir) { - Path target = configDir.resolve("irisworldgen").resolve("packs").resolve(DEFAULT_PACK); - - if (Files.isDirectory(target.resolve("dimensions"))) { - return; - } - - LOGGER.info("Iris default pack missing; downloading IrisDimensions/{} (latest on {})", DEFAULT_PACK, DEFAULT_BRANCH); - boolean installed = install(configDir, DEFAULT_PACK, DEFAULT_BRANCH, LOGGER::info); - - if (!installed) { - LOGGER.error("Iris failed to download the default '{}' pack; install it manually at {} (source: https://github.com/IrisDimensions/overworld)", DEFAULT_PACK, target); - } - } - public static boolean install(Path configDir, String pack, String branch, Consumer feedback) { if (pack == null || !PACK_NAME.matcher(pack).matches()) { feedback.accept("Invalid pack name '" + pack + "' (allowed: a-z, 0-9, _ and -)"); diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java index 1200cb7bb..c7600abd7 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedPlatform.java @@ -49,7 +49,7 @@ public final class ModdedPlatform implements IrisPlatform { this.scheduler = new ModdedScheduler(); this.capabilities = new ModdedCapabilities(); this.structureHooks = new ModdedStructureHooks(loader::currentServer); - this.biomeWriter = new ModdedBiomeWriter(); + this.biomeWriter = new ModdedBiomeWriter(loader::currentServer); } public static void errorSink(Consumer sink) { diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java index 70a9b2d2b..92e63653b 100644 --- a/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/ModdedWorldCheck.java @@ -86,16 +86,22 @@ public final class ModdedWorldCheck { } private static boolean run(MinecraftServer server) { - ServerLevel overworld = server.overworld(); - String generatorClass = overworld.getChunkSource().getGenerator().getClass().getName(); - LOGGER.info("[worldcheck] overworld generator: {}", generatorClass); - boolean irisGenerator = overworld.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator; - if (!irisGenerator) { - LOGGER.error("[worldcheck] overworld is NOT using IrisModdedChunkGenerator"); + ServerLevel level = targetLevel(server); + if (level == null) { + LOGGER.error("[worldcheck] no Iris dimension is loaded"); + return false; } - BlockPos spawn = overworld.getRespawnData().pos(); - LOGGER.info("[worldcheck] spawn: {} {} {} (minY={} height={})", spawn.getX(), spawn.getY(), spawn.getZ(), overworld.getMinY(), overworld.getHeight()); + String levelId = level.dimension().identifier().toString(); + String generatorClass = level.getChunkSource().getGenerator().getClass().getName(); + LOGGER.info("[worldcheck] {} generator: {}", levelId, generatorClass); + boolean irisGenerator = level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator; + if (!irisGenerator) { + LOGGER.error("[worldcheck] {} is NOT using IrisModdedChunkGenerator", levelId); + } + + BlockPos spawn = level.getRespawnData().pos(); + LOGGER.info("[worldcheck] spawn: {} {} {} (minY={} height={})", spawn.getX(), spawn.getY(), spawn.getZ(), level.getMinY(), level.getHeight()); MessageDigest digest = sha256(); List samples = new ArrayList<>(); @@ -104,9 +110,9 @@ public final class ModdedWorldCheck { for (int dz = 0; dz < 4; dz++) { int x = spawn.getX() + (dx - 2) * 16 + 8; int z = spawn.getZ() + (dz - 2) * 16 + 8; - overworld.getChunk(x >> 4, z >> 4); - int y = overworld.getHeight(Heightmap.Types.WORLD_SURFACE, x, z); - BlockState surface = overworld.getBlockState(new BlockPos(x, y - 1, z)); + level.getChunk(x >> 4, z >> 4); + int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z); + BlockState surface = level.getBlockState(new BlockPos(x, y - 1, z)); String key = BuiltInRegistries.BLOCK.getKey(surface.getBlock()).toString(); String line = x + " " + (y - 1) + " " + z + " " + key; samples.add(line); @@ -121,7 +127,7 @@ public final class ModdedWorldCheck { LOGGER.info("[worldcheck] surface digest: {} ({} columns, {} distinct surface blocks: {})", HexFormat.of().formatHex(digest.digest()).substring(0, 12), samples.size(), surfaceKeys.size(), surfaceKeys); - ChunkAccess zeroChunk = overworld.getChunk(0, 0); + ChunkAccess zeroChunk = level.getChunk(0, 0); int nonEmptySections = 0; for (LevelChunkSection section : zeroChunk.getSections()) { if (!section.hasOnlyAir()) { @@ -129,8 +135,8 @@ public final class ModdedWorldCheck { } } Set columnKeys = new LinkedHashSet<>(); - int surfaceY = overworld.getHeight(Heightmap.Types.WORLD_SURFACE, 8, 8); - for (int y = overworld.getMinY(); y < surfaceY; y += 16) { + int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, 8, 8); + for (int y = level.getMinY(); y < surfaceY; y += 16) { BlockState state = zeroChunk.getBlockState(new BlockPos(8, y, 8)); if (!state.isAir()) { columnKeys.add(BuiltInRegistries.BLOCK.getKey(state.getBlock()).toString()); @@ -153,6 +159,26 @@ public final class ModdedWorldCheck { return pass; } + private static ServerLevel targetLevel(MinecraftServer server) { + String target = System.getProperty("iris.worldcheck.dimension"); + if (target != null && !target.isBlank()) { + for (ServerLevel level : server.getAllLevels()) { + if (level.dimension().identifier().toString().equals(target.trim())) { + return level; + } + } + LOGGER.error("[worldcheck] requested dimension '{}' is not loaded", target); + return null; + } + + for (ServerLevel level : server.getAllLevels()) { + if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) { + return level; + } + } + return null; + } + private static MessageDigest sha256() { try { return MessageDigest.getInstance("SHA-256"); 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 37770436f..0a77a6801 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 @@ -38,12 +38,12 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.suggestion.SuggestionProvider; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.LiteralCommandNode; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.SharedSuggestionProvider; import net.minecraft.core.BlockPos; import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -74,15 +74,25 @@ public final class IrisModdedCommands { private static final SuggestionProvider OBJECT_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder); private static final SuggestionProvider STRUCTURE_KEYS = (CommandContext context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder); private static final SuggestionProvider POI_TYPES = (CommandContext context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder); - static final SuggestionProvider PACK_NAMES = (CommandContext context, SuggestionsBuilder builder) -> suggestPackNames(builder); + static final SuggestionProvider PACK_NAMES = (CommandContext context, SuggestionsBuilder builder) -> suggestPackNames(context, builder); private static final SuggestionProvider DIMENSION_NAMES = (CommandContext context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder); private IrisModdedCommands() { } public static void register(CommandDispatcher dispatcher) { + LiteralCommandNode root = dispatcher.register(rootTree()); + dispatcher.register(Commands.literal("ir").redirect(root)); + dispatcher.register(Commands.literal("irs").redirect(root)); + LOGGER.info("Iris /iris command tree registered"); + } + + private static LiteralArgumentBuilder rootTree() { LiteralArgumentBuilder root = Commands.literal("iris"); + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "")); + root.then(helpTree()); + root.then(Commands.literal("version") .executes((CommandContext context) -> version(context.getSource()))); @@ -100,40 +110,74 @@ public final class IrisModdedCommands { root.then(Commands.literal("seed").requires(GATE) .executes((CommandContext context) -> seed(context.getSource()))); - root.then(goldenhashTree()); + root.then(goldenhashTree("goldenhash")); + root.then(goldenhashTree("gold")); - root.then(Commands.literal("download").requires(GATE) - .then(Commands.argument("pack", StringArgumentType.word()).suggests(PACK_NAMES) - .executes((CommandContext context) -> download(context.getSource(), StringArgumentType.getString(context, "pack"), "master")) - .then(Commands.argument("branch", StringArgumentType.word()) - .executes((CommandContext context) -> download(context.getSource(), StringArgumentType.getString(context, "pack"), StringArgumentType.getString(context, "branch")))))); + root.then(downloadTree("download")); + root.then(downloadTree("dl")); - root.then(Commands.literal("metrics").requires(GATE) - .executes((CommandContext context) -> metrics(context.getSource()))); + root.then(metricsTree("metrics")); + root.then(metricsTree("measure")); - root.then(Commands.literal("regen").requires(GATE) - .executes((CommandContext context) -> regen(context.getSource(), 0)) - .then(Commands.argument("radius", IntegerArgumentType.integer(0, 64)) - .executes((CommandContext context) -> regen(context.getSource(), IntegerArgumentType.getInteger(context, "radius"))))); + root.then(regenTree("regen")); + root.then(regenTree("rg")); - root.then(pregenTree()); + root.then(pregenTree("pregen")); + root.then(pregenTree("pregenerate")); root.then(Commands.literal("wand").requires(GATE) .executes((CommandContext context) -> ModdedObjectCommands.giveWand(context.getSource()))); - root.then(ModdedObjectCommands.tree()); + root.then(ModdedObjectCommands.tree("object")); + root.then(ModdedObjectCommands.tree("o")); root.then(editTree()); - root.then(ModdedStudioCommands.tree()); - root.then(ModdedPackCommands.tree()); - root.then(ModdedDatapackCommands.tree()); - root.then(ModdedStructureCommands.tree()); + root.then(ModdedStudioCommands.tree("studio")); + root.then(ModdedStudioCommands.tree("std")); + root.then(ModdedStudioCommands.tree("s")); + root.then(ModdedPackCommands.tree("pack")); + root.then(ModdedPackCommands.tree("pk")); + root.then(ModdedWorldCommands.tree("world")); + root.then(ModdedWorldCommands.tree("w")); + root.then(ModdedDatapackCommands.tree("datapack")); + root.then(ModdedDatapackCommands.tree("datapacks")); + root.then(ModdedDatapackCommands.tree("dp")); + root.then(ModdedStructureCommands.tree("structure")); + root.then(ModdedStructureCommands.tree("struct")); + root.then(ModdedStructureCommands.tree("str")); - dispatcher.register(root); - LOGGER.info("Iris /iris command tree registered"); + return root; + } + + private static LiteralArgumentBuilder helpTree() { + return Commands.literal("help") + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), "")) + .then(Commands.argument("section", StringArgumentType.greedyString()) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), StringArgumentType.getString(context, "section")))); + } + + private static LiteralArgumentBuilder downloadTree(String name) { + return Commands.literal(name).requires(GATE) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(PACK_NAMES) + .executes((CommandContext context) -> download(context.getSource(), StringArgumentType.getString(context, "pack"), "stable")) + .then(Commands.argument("branch", StringArgumentType.word()) + .executes((CommandContext context) -> download(context.getSource(), StringArgumentType.getString(context, "pack"), StringArgumentType.getString(context, "branch"))))); + } + + private static LiteralArgumentBuilder metricsTree(String name) { + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> metrics(context.getSource())); + } + + private static LiteralArgumentBuilder regenTree(String name) { + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> regen(context.getSource(), 0)) + .then(Commands.argument("radius", IntegerArgumentType.integer(0, 64)) + .executes((CommandContext context) -> regen(context.getSource(), IntegerArgumentType.getInteger(context, "radius")))); } private static LiteralArgumentBuilder gotoTree(String name) { return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)) .then(Commands.literal("biome") .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(BIOME_KEYS) .executes((CommandContext context) -> gotoBiome(context.getSource(), StringArgumentType.getString(context, "key"))))) @@ -151,8 +195,9 @@ public final class IrisModdedCommands { .executes((CommandContext context) -> gotoPoi(context.getSource(), StringArgumentType.getString(context, "type"))))); } - private static LiteralArgumentBuilder pregenTree() { - return Commands.literal("pregen").requires(GATE) + private static LiteralArgumentBuilder pregenTree(String name) { + return Commands.literal(name).requires(GATE) + .executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)) .then(Commands.literal("start") .then(Commands.argument("radius", IntegerArgumentType.integer(1, 100000)) .executes((CommandContext context) -> pregenStart(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 0, 0)) @@ -164,14 +209,18 @@ public final class IrisModdedCommands { IntegerArgumentType.getInteger(context, "z"))))))) .then(Commands.literal("stop") .executes((CommandContext context) -> pregenStop(context.getSource()))) + .then(Commands.literal("x") + .executes((CommandContext context) -> pregenStop(context.getSource()))) .then(Commands.literal("pause") .executes((CommandContext context) -> pregenPause(context.getSource()))) + .then(Commands.literal("resume") + .executes((CommandContext context) -> pregenPause(context.getSource()))) .then(Commands.literal("status") .executes((CommandContext context) -> pregenStatus(context.getSource()))); } - private static LiteralArgumentBuilder goldenhashTree() { - LiteralArgumentBuilder radiusAndThreads = Commands.literal("goldenhash").requires(GATE) + private static LiteralArgumentBuilder goldenhashTree(String name) { + LiteralArgumentBuilder radiusAndThreads = Commands.literal(name).requires(GATE) .executes((CommandContext context) -> goldenhash(context.getSource(), 8, 8, ModdedGoldenHash.Mode.AUTO)); attachModes(radiusAndThreads, (CommandContext context) -> 8, (CommandContext context) -> 8); @@ -617,6 +666,7 @@ public final class IrisModdedCommands { } private static CompletableFuture suggestBiomeKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); try { Engine engine = engineFor(context.getSource().getLevel()); if (engine != null) { @@ -628,6 +678,7 @@ public final class IrisModdedCommands { } private static CompletableFuture suggestRegionKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); try { Engine engine = engineFor(context.getSource().getLevel()); if (engine != null) { @@ -639,6 +690,7 @@ public final class IrisModdedCommands { } private static CompletableFuture suggestObjectKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); try { Engine engine = engineFor(context.getSource().getLevel()); if (engine != null) { @@ -650,6 +702,7 @@ public final class IrisModdedCommands { } private static CompletableFuture suggestStructureKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); try { Engine engine = engineFor(context.getSource().getLevel()); if (engine != null) { @@ -660,7 +713,8 @@ public final class IrisModdedCommands { return builder.buildFuture(); } - private static CompletableFuture suggestPackNames(SuggestionsBuilder builder) { + private static CompletableFuture suggestPackNames(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); List names = new ArrayList<>(); names.add("overworld"); try { @@ -679,6 +733,7 @@ public final class IrisModdedCommands { } private static CompletableFuture suggestDimensionNames(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); List names = new ArrayList<>(); for (ServerLevel level : context.getSource().getServer().getAllLevels()) { if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) { @@ -689,10 +744,10 @@ public final class IrisModdedCommands { } static void ok(CommandSourceStack source, String message) { - source.sendSuccess(() -> Component.literal(message), false); + ModdedCommandFeedback.ok(source, message); } static void fail(CommandSourceStack source, String message) { - source.sendFailure(Component.literal(message)); + ModdedCommandFeedback.fail(source, message); } } diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java new file mode 100644 index 000000000..c5a39cad1 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandFeedback.java @@ -0,0 +1,97 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +final class ModdedCommandFeedback { + private static final long MESSAGE_SOUND_COOLDOWN_MS = 650L; + private static final long TAB_SOUND_COOLDOWN_MS = 175L; + private static final Map MESSAGE_SOUNDS = new ConcurrentHashMap<>(); + private static final Map TAB_SOUNDS = new ConcurrentHashMap<>(); + + private ModdedCommandFeedback() { + } + + static void ok(CommandSourceStack source, String message) { + source.sendSuccess(() -> Component.literal(message).withStyle(ChatFormatting.GREEN), false); + playSuccess(source); + } + + static void fail(CommandSourceStack source, String message) { + source.sendFailure(Component.literal(message).withStyle(ChatFormatting.RED)); + playFailure(source); + } + + static void send(CommandSourceStack source, Component component) { + source.sendSuccess(() -> component, false); + } + + static void tab(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null || !claim(TAB_SOUNDS, player.getUUID(), TAB_SOUND_COOLDOWN_MS)) { + return; + } + + player.level().playSound(null, player.blockPosition(), SoundEvents.ITEM_FRAME_ROTATE_ITEM, SoundSource.PLAYERS, 0.25F, 1.7F); + } + + private static void playSuccess(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null || !claim(MESSAGE_SOUNDS, player.getUUID(), MESSAGE_SOUND_COOLDOWN_MS)) { + return; + } + + ServerLevel level = player.level(); + level.playSound(null, player.blockPosition(), SoundEvents.AMETHYST_CLUSTER_BREAK, SoundSource.PLAYERS, 0.77F, 1.65F); + level.playSound(null, player.blockPosition(), SoundEvents.RESPAWN_ANCHOR_CHARGE, SoundSource.PLAYERS, 0.125F, 2.99F); + } + + private static void playFailure(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null || !claim(MESSAGE_SOUNDS, player.getUUID(), MESSAGE_SOUND_COOLDOWN_MS)) { + return; + } + + ServerLevel level = player.level(); + level.playSound(null, player.blockPosition(), SoundEvents.AMETHYST_CLUSTER_BREAK, SoundSource.PLAYERS, 0.77F, 0.25F); + level.playSound(null, player.blockPosition(), SoundEvents.BEACON_DEACTIVATE, SoundSource.PLAYERS, 0.2F, 0.45F); + } + + private static boolean claim(Map sounds, UUID uuid, long cooldownMs) { + long now = System.currentTimeMillis(); + Long previous = sounds.get(uuid); + if (previous != null && now - previous.longValue() < cooldownMs) { + return false; + } + + sounds.put(uuid, now); + return true; + } +} 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 new file mode 100644 index 000000000..9915e765c --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedCommandHelp.java @@ -0,0 +1,228 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class ModdedCommandHelp { + private static final Map> SECTIONS = new LinkedHashMap<>(); + + static { + SECTIONS.put("", List.of( + Entry.command("version", "", "Print version information"), + Entry.command("info", "[dimension]", "List loaded Iris dimensions and pack details"), + Entry.command("what", "", "Inspect the Iris biome, region, cave biome, surface and chunk at your position"), + Entry.group("find", "Find and teleport to Iris biomes, regions, objects, structures and points of interest", "goto"), + Entry.command("seed", "", "Print world and engine seed information"), + Entry.command("download", " [branch]", "Download a pack project", "dl"), + Entry.command("metrics", "", "Print generation metrics for your current Iris dimension", "measure"), + Entry.command("regen", "[radius]", "Delete and regenerate nearby chunks in place", "rg"), + Entry.group("pregen", "Pregenerate an Iris dimension", "pregenerate"), + Entry.command("wand", "", "Get an Iris object wand"), + Entry.group("object", "Object wand, save, paste, analyze and undo tools", "o"), + Entry.group("studio", "Pack project creation, packaging and reports", "std", "s"), + Entry.group("pack", "Pack validation and maintenance", "pk"), + Entry.group("world", "Explicit Iris dimension enablement and removal", "w"), + Entry.group("datapack", "World datapack install and status helpers", "datapacks", "dp"), + Entry.group("structure", "Iris structure index, info and placement tools", "struct", "str"), + Entry.command("goldenhash", "[radius] [threads] [capture|verify]", "Generate deterministic block hashes for parity testing", "gold") + )); + SECTIONS.put("find", List.of( + Entry.command("biome", "", "Find an Iris biome"), + Entry.command("region", "", "Find an Iris region"), + Entry.command("object", "", "Find an object placement"), + Entry.command("structure", "", "Find an Iris-placed structure"), + Entry.command("poi", "", "Find a supported point of interest") + )); + SECTIONS.put("goto", SECTIONS.get("find")); + SECTIONS.put("pregen", List.of( + Entry.command("start", " [x] [z]", "Start pregeneration"), + Entry.command("stop", "", "Stop the active pregeneration task", "x"), + Entry.command("pause", "", "Pause or resume pregeneration", "resume"), + Entry.command("status", "", "Show pregeneration status") + )); + SECTIONS.put("pregenerate", SECTIONS.get("pregen")); + SECTIONS.put("object", List.of( + Entry.command("wand", "", "Get an Iris object wand"), + Entry.command("dust", "", "Get dust that reveals object placements", "d"), + Entry.command("save", "", "Save the selected wand volume as an object"), + Entry.command("paste", "", "Paste an object at your position"), + Entry.command("expand", "[amount]", "Expand the wand selection in your looking direction"), + Entry.command("contract", "[amount]", "Contract the wand selection in your looking direction", "-"), + Entry.command("shift", "[amount]", "Shift the wand selection in your looking direction"), + Entry.command("position1", "[look]", "Set selection point 1", "p1"), + Entry.command("position2", "[look]", "Set selection point 2", "p2"), + Entry.command("x+y", "", "Autoselect up and out"), + Entry.command("x&y", "", "Autoselect up, down and out"), + Entry.command("analyze", "", "Show object composition"), + Entry.command("shrink", "", "Shrink an object to its minimum size"), + Entry.command("undo", "[amount]", "Undo pasted objects", "u") + )); + SECTIONS.put("o", SECTIONS.get("object")); + SECTIONS.put("studio", List.of( + Entry.command("create", " [template]", "Create a new pack project", "+"), + Entry.command("package", "[pack]", "Package a dimension into a compressed format"), + Entry.command("version", "[pack]", "Print a pack version"), + Entry.command("regions", "[radius]", "Calculate nearby region distribution"), + Entry.command("open", "", "Explain modded studio workflow"), + Entry.command("close", "", "Explain modded studio workflow"), + Entry.command("vscode", "", "Explain editor workflow"), + Entry.command("update", "", "Explain workspace regeneration workflow"), + Entry.command("importvanilla", "", "Explain vanilla import workflow", "importv", "iv") + )); + SECTIONS.put("std", SECTIONS.get("studio")); + SECTIONS.put("s", SECTIONS.get("studio")); + SECTIONS.put("pack", List.of( + Entry.command("validate", "[pack]", "Validate a pack or every pack", "v"), + Entry.command("restore", "", "Restore the latest trashed files for a pack", "r"), + Entry.command("status", "[pack]", "Show cached validation status", "s") + )); + SECTIONS.put("pk", SECTIONS.get("pack")); + SECTIONS.put("world", List.of( + Entry.command("enable", " [packDimension]", "Create an Iris dimension in world/datapacks/iris", "create"), + Entry.command("replace-overworld", " [packDimension]", "Explicitly make minecraft:overworld use an Iris pack"), + Entry.command("disable", "", "Remove an Iris dimension definition from the world datapack", "remove", "rm"), + Entry.command("list", "", "List Iris dimensions staged in the world datapack", "ls"), + Entry.command("status", "", "Show staged and currently loaded Iris dimensions") + )); + SECTIONS.put("w", SECTIONS.get("world")); + SECTIONS.put("datapack", List.of( + Entry.command("status", "", "Check loaded Iris dimension type overrides"), + Entry.command("install", "", "Install dimension type overrides for loaded Iris dimensions"), + Entry.command("list", "", "List configured and installed datapacks", "ls"), + Entry.command("ingest", "", "Explain Bukkit datapack ingest workflow", "pull"), + Entry.command("remove", "", "Explain datapack removal workflow", "rm") + )); + SECTIONS.put("datapacks", SECTIONS.get("datapack")); + SECTIONS.put("dp", SECTIONS.get("datapack")); + SECTIONS.put("structure", List.of( + Entry.command("list", "", "Regenerate structure-index.json", "ls"), + Entry.command("info", "", "Resolve an Iris structure graph and report bounds"), + Entry.command("place", "", "Assemble and place an Iris structure at your location", "p"), + Entry.command("import", "", "Explain Bukkit structure import workflow", "import-all", "reimport", "imp", "all"), + Entry.command("capture", "", "Explain Bukkit structure capture workflow", "cap"), + Entry.command("verify", "", "Explain modded structure locate behavior", "locateall") + )); + SECTIONS.put("struct", SECTIONS.get("structure")); + SECTIONS.put("str", SECTIONS.get("structure")); + } + + private ModdedCommandHelp() { + } + + static int send(CommandSourceStack source, String path) { + String normalized = normalize(path); + List entries = SECTIONS.get(normalized); + if (entries == null) { + ModdedCommandFeedback.fail(source, "Unknown Iris help section: " + normalized); + return 0; + } + + sendHeader(source, normalized.isEmpty() ? "Iris Commands" : "/iris " + normalized); + if (!normalized.isEmpty()) { + ModdedCommandFeedback.send(source, clickable(" < Back", "/iris", "/iris", "Back to Iris command groups", true)); + } + for (Entry entry : entries) { + ModdedCommandFeedback.send(source, line(normalized, entry)); + } + ModdedCommandFeedback.ok(source, "Click a group to open it, or click a command to place it in chat."); + return 1; + } + + private static void sendHeader(CommandSourceStack source, String title) { + MutableComponent header = Component.literal("========== ").withStyle(ChatFormatting.DARK_GREEN, ChatFormatting.STRIKETHROUGH) + .append(Component.literal(" " + title + " ").withStyle(ChatFormatting.GREEN, ChatFormatting.BOLD)) + .append(Component.literal(" ==========").withStyle(ChatFormatting.DARK_GREEN, ChatFormatting.STRIKETHROUGH)); + ModdedCommandFeedback.send(source, header); + } + + private static MutableComponent line(String path, Entry entry) { + String basePath = path.isEmpty() ? "" : " " + path; + String command = "/iris" + basePath + " " + entry.name(); + String suggestion = entry.usage().isBlank() ? command : command + " " + entry.usage(); + String hover = entry.description() + (entry.usage().isBlank() ? "" : "\nUsage: " + suggestion); + MutableComponent row = Component.literal(" "); + row.append(clickable(entry.name(), command, suggestion, hover, entry.group())); + if (entry.aliases().length > 0) { + row.append(Component.literal(" " + String.join(", ", entry.aliases())).withStyle(ChatFormatting.DARK_GREEN)); + } + row.append(Component.literal(" - ").withStyle(ChatFormatting.DARK_GRAY)); + row.append(Component.literal(entry.description()).withStyle(ChatFormatting.GRAY)); + return row; + } + + private static MutableComponent clickable(String label, String command, String suggestion, String hover, boolean run) { + Component hoverText = Component.literal(hover).withStyle(ChatFormatting.GREEN); + if (run) { + return Component.literal(label).withStyle((style) -> style + .withColor(ChatFormatting.AQUA) + .withBold(true) + .withClickEvent(new ClickEvent.RunCommand(command)) + .withHoverEvent(new HoverEvent.ShowText(hoverText))); + } + + return Component.literal(label).withStyle((style) -> style + .withColor(ChatFormatting.GREEN) + .withClickEvent(new ClickEvent.SuggestCommand(suggestion)) + .withHoverEvent(new HoverEvent.ShowText(hoverText))); + } + + private static String normalize(String path) { + if (path == null) { + return ""; + } + + String normalized = path.trim().toLowerCase(); + if (normalized.startsWith("/iris ")) { + normalized = normalized.substring(6).trim(); + } else if (normalized.equals("/iris")) { + normalized = ""; + } + + if (normalized.startsWith("help ")) { + normalized = normalized.substring(5).trim(); + } + + int space = normalized.indexOf(' '); + if (space >= 0) { + normalized = normalized.substring(0, space); + } + + return normalized; + } + + private record Entry(String name, String usage, String description, boolean group, String... aliases) { + static Entry command(String name, String usage, String description, String... aliases) { + return new Entry(name, usage, description, false, aliases); + } + + static Entry group(String name, String description, String... aliases) { + return new Entry(name, "", description, true, aliases); + } + } +} 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 b865cf55f..1e15e6b34 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 @@ -48,13 +48,15 @@ import java.util.function.Predicate; public final class ModdedDatapackCommands { private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); - private static final String WORLD_PACK_NAME = "iris"; + private static final String WORLD_PACK_NAME = ModdedWorldDatapackWriter.WORLD_PACK_NAME; private ModdedDatapackCommands() { } - public static LiteralArgumentBuilder tree() { - LiteralArgumentBuilder root = Commands.literal("datapack").requires(GATE); + public static LiteralArgumentBuilder tree(String name) { + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); root.then(Commands.literal("status") .executes((CommandContext context) -> status(context.getSource()))); @@ -64,10 +66,14 @@ public final class ModdedDatapackCommands { root.then(Commands.literal("list") .executes((CommandContext context) -> list(context.getSource()))); + root.then(Commands.literal("ls") + .executes((CommandContext context) -> list(context.getSource()))); root.then(message("ingest", "Modrinth datapack ingest requires the Bukkit plugin: its post-restart structure import into editable Iris resources uses Bukkit registries, and Iris modded dimensions do not run vanilla structure placement, so ingested structure datapacks would not generate. Drop datapacks into world/datapacks manually for non-structure content.")); + root.then(message("pull", "Modrinth datapack ingest requires the Bukkit plugin: its post-restart structure import into editable Iris resources uses Bukkit registries, and Iris modded dimensions do not run vanilla structure placement, so ingested structure datapacks would not generate. Drop datapacks into world/datapacks manually for non-structure content.")); root.then(message("remove", "Datapack removal manages the Bukkit ingest manifest. On modded servers delete the datapack folder from world/datapacks and restart.")); + root.then(message("rm", "Datapack removal manages the Bukkit ingest manifest. On modded servers delete the datapack folder from world/datapacks and restart.")); return root; } @@ -130,7 +136,7 @@ public final class ModdedDatapackCommands { if (!matches) { mismatches++; IrisModdedCommands.fail(source, " WARNING: the active dimension type does not match the pack. Terrain outside " - + activeMin + ".." + activeMax + " will be clipped. Run /iris datapack install and restart the server."); + + activeMin + ".." + activeMax + " will be clipped. Run /iris world enable for new worlds, or /iris datapack install for already-loaded Iris dimensions, then restart."); } } if (irisLevels == 0) { @@ -202,7 +208,7 @@ public final class ModdedDatapackCommands { for (String path : written) { IrisModdedCommands.ok(source, "Wrote " + path); } - IrisModdedCommands.ok(source, "World datapack '" + WORLD_PACK_NAME + "' installed. Restart the server for the dimension types to apply (world datapacks override mod-provided data)."); + IrisModdedCommands.ok(source, "World datapack '" + WORLD_PACK_NAME + "' dimension type overrides installed. Restart the server for the dimension types to apply."); return 1; } 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 833793191..92e001fd3 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 @@ -27,7 +27,6 @@ import art.arcane.iris.spi.PlatformBlockState; import art.arcane.iris.util.common.parallel.MultiBurst; import art.arcane.iris.util.project.hunk.Hunk; import net.minecraft.commands.CommandSourceStack; -import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import org.slf4j.Logger; @@ -91,7 +90,7 @@ public final class ModdedGoldenHash { public static void start(CommandSourceStack source, ServerLevel level, Engine engine, int radius, int threads, Mode mode) { if (!ACTIVE.compareAndSet(false, true)) { - source.sendFailure(Component.literal("A goldenhash scan is already running.")); + IrisModdedCommands.fail(source, "A goldenhash scan is already running."); return; } ModdedGoldenHash scan = new ModdedGoldenHash(source, level, engine, radius, threads, mode); @@ -375,11 +374,11 @@ public final class ModdedGoldenHash { } private void ok(String message) { - server.execute(() -> source.sendSuccess(() -> Component.literal(message), false)); + server.execute(() -> IrisModdedCommands.ok(source, message)); } private void fail(String message) { - server.execute(() -> source.sendFailure(Component.literal(message))); + server.execute(() -> IrisModdedCommands.fail(source, message)); } private static String shortHash(String hex) { 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 7c094e7c4..c5ec8dade 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 @@ -66,6 +66,7 @@ public final class ModdedObjectCommands { private static final double TARGET_RANGE = 256.0D; private static final SuggestionProvider OBJECT_KEYS = (CommandContext context, SuggestionsBuilder builder) -> { + ModdedCommandFeedback.tab(context.getSource()); try { Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); if (engine != null) { @@ -88,14 +89,18 @@ public final class ModdedObjectCommands { SHIFT } - public static LiteralArgumentBuilder tree() { + public static LiteralArgumentBuilder tree(String name) { ModdedObjectUndo.init(); - LiteralArgumentBuilder root = Commands.literal("object").requires(GATE); + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); root.then(Commands.literal("wand") .executes((CommandContext context) -> giveWand(context.getSource()))); root.then(Commands.literal("dust") .executes((CommandContext context) -> giveDust(context.getSource()))); + root.then(Commands.literal("d") + .executes((CommandContext context) -> giveDust(context.getSource()))); root.then(Commands.literal("save") .then(Commands.literal("overwrite") @@ -108,6 +113,7 @@ public final class ModdedObjectCommands { root.then(resizeTree("expand", ResizeOp.EXPAND)); root.then(resizeTree("contract", ResizeOp.CONTRACT)); + root.then(resizeTree("-", ResizeOp.CONTRACT)); root.then(resizeTree("shift", ResizeOp.SHIFT)); root.then(Commands.literal("xpy") @@ -120,7 +126,9 @@ public final class ModdedObjectCommands { .executes((CommandContext context) -> autoSelect(context.getSource(), true))); root.then(positionTree("position1", true)); + root.then(positionTree("p1", true)); root.then(positionTree("position2", false)); + root.then(positionTree("p2", false)); root.then(Commands.literal("analyze") .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS) @@ -134,6 +142,10 @@ public final class ModdedObjectCommands { .executes((CommandContext context) -> undo(context.getSource(), 1)) .then(Commands.argument("amount", IntegerArgumentType.integer(1, 32)) .executes((CommandContext context) -> undo(context.getSource(), IntegerArgumentType.getInteger(context, "amount"))))); + root.then(Commands.literal("u") + .executes((CommandContext context) -> undo(context.getSource(), 1)) + .then(Commands.argument("amount", IntegerArgumentType.integer(1, 32)) + .executes((CommandContext context) -> undo(context.getSource(), IntegerArgumentType.getInteger(context, "amount"))))); root.then(bukkitOnly("we", "WorldEdit selection import requires the Bukkit plugin with WorldEdit installed.")); root.then(bukkitOnly("studio", "The object studio world requires the Bukkit studio toolchain; it is not available on modded servers.")); 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 dcd3973d0..781d35e37 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 @@ -44,22 +44,35 @@ public final class ModdedPackCommands { private ModdedPackCommands() { } - public static LiteralArgumentBuilder tree() { - LiteralArgumentBuilder root = Commands.literal("pack").requires(GATE); + public static LiteralArgumentBuilder tree(String name) { + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); root.then(Commands.literal("validate") .executes((CommandContext context) -> validate(context.getSource(), null)) .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) .executes((CommandContext context) -> validate(context.getSource(), StringArgumentType.getString(context, "pack"))))); + root.then(Commands.literal("v") + .executes((CommandContext context) -> validate(context.getSource(), null)) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> validate(context.getSource(), StringArgumentType.getString(context, "pack"))))); root.then(Commands.literal("restore") .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) .executes((CommandContext context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"))))); + root.then(Commands.literal("r") + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack"))))); root.then(Commands.literal("status") .executes((CommandContext context) -> status(context.getSource(), null)) .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) .executes((CommandContext context) -> status(context.getSource(), StringArgumentType.getString(context, "pack"))))); + root.then(Commands.literal("s") + .executes((CommandContext context) -> status(context.getSource(), null)) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> status(context.getSource(), StringArgumentType.getString(context, "pack"))))); return root; } 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 f1de40d19..95e950332 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 @@ -34,7 +34,6 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Registry; import net.minecraft.core.SectionPos; import net.minecraft.core.registries.Registries; -import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; @@ -89,7 +88,7 @@ public final class ModdedRegen { public static void start(CommandSourceStack source, ServerLevel level, IrisModdedChunkGenerator generator, Engine engine, ServerPlayer player, int radius) { if (!ACTIVE.compareAndSet(false, true)) { - source.sendFailure(Component.literal("A regen is already running.")); + IrisModdedCommands.fail(source, "A regen is already running."); return; } int centerX = player.blockPosition().getX() >> 4; @@ -276,10 +275,10 @@ public final class ModdedRegen { } private void ok(String message) { - server.execute(() -> source.sendSuccess(() -> Component.literal(message), false)); + server.execute(() -> IrisModdedCommands.ok(source, message)); } private void fail(String message) { - server.execute(() -> source.sendFailure(Component.literal(message))); + server.execute(() -> IrisModdedCommands.fail(source, message)); } } 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 eb7835b03..6d70c554b 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 @@ -56,11 +56,15 @@ public final class ModdedStructureCommands { private ModdedStructureCommands() { } - public static LiteralArgumentBuilder tree() { - LiteralArgumentBuilder root = Commands.literal("structure").requires(GATE); + public static LiteralArgumentBuilder tree(String name) { + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); root.then(Commands.literal("list") .executes((CommandContext context) -> list(context.getSource()))); + root.then(Commands.literal("ls") + .executes((CommandContext context) -> list(context.getSource()))); root.then(Commands.literal("info") .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(IRIS_STRUCTURE_KEYS) @@ -69,10 +73,19 @@ public final class ModdedStructureCommands { root.then(Commands.literal("place") .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(IRIS_STRUCTURE_KEYS) .executes((CommandContext context) -> place(context.getSource(), StringArgumentType.getString(context, "key"))))); + root.then(Commands.literal("p") + .then(Commands.argument("key", StringArgumentType.greedyString()).suggests(IRIS_STRUCTURE_KEYS) + .executes((CommandContext context) -> place(context.getSource(), StringArgumentType.getString(context, "key"))))); root.then(message("import", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("import-all", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("reimport", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("imp", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("all", "Structure import rebuilds vanilla & datapack structures as editable Iris resources through Bukkit/NMS template managers; run /iris structure import on a Bukkit server against this pack, then copy the pack folder over.")); root.then(message("capture", "Structure capture generates each structure in a throwaway Bukkit scratch world to read its blocks; it requires the Bukkit plugin (v26 NMS binding).")); + root.then(message("cap", "Structure capture generates each structure in a throwaway Bukkit scratch world to read its blocks; it requires the Bukkit plugin (v26 NMS binding).")); root.then(message("verify", "Vanilla structure locate is meaningless here: Iris modded dimensions do not run vanilla structure placement (Iris places structures itself). Use /iris goto structure to locate Iris-placed structures.")); + root.then(message("locateall", "Vanilla structure locate is meaningless here: Iris modded dimensions do not run vanilla structure placement (Iris places structures itself). Use /iris goto structure to locate Iris-placed structures.")); return root; } @@ -194,6 +207,7 @@ public final class ModdedStructureCommands { } private static CompletableFuture suggestIrisStructureKeys(CommandContext context, SuggestionsBuilder builder) { + ModdedCommandFeedback.tab(context.getSource()); try { Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel()); if (engine != null && engine.getData().getStructureLoader() != null) { 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 e01061a89..2fd0124f8 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 @@ -71,14 +71,21 @@ public final class ModdedStudioCommands { private ModdedStudioCommands() { } - public static LiteralArgumentBuilder tree() { - LiteralArgumentBuilder root = Commands.literal("studio").requires(GATE); + public static LiteralArgumentBuilder tree(String name) { + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); root.then(Commands.literal("create") .then(Commands.argument("name", StringArgumentType.word()) .executes((CommandContext context) -> create(context.getSource(), StringArgumentType.getString(context, "name"), DEFAULT_TEMPLATE)) .then(Commands.argument("template", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) .executes((CommandContext context) -> create(context.getSource(), StringArgumentType.getString(context, "name"), StringArgumentType.getString(context, "template")))))); + root.then(Commands.literal("+") + .then(Commands.argument("name", StringArgumentType.word()) + .executes((CommandContext context) -> create(context.getSource(), StringArgumentType.getString(context, "name"), DEFAULT_TEMPLATE)) + .then(Commands.argument("template", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> create(context.getSource(), StringArgumentType.getString(context, "name"), StringArgumentType.getString(context, "template")))))); root.then(Commands.literal("package") .executes((CommandContext context) -> pkg(context.getSource(), null)) @@ -96,17 +103,27 @@ public final class ModdedStudioCommands { .executes((CommandContext context) -> regions(context.getSource(), IntegerArgumentType.getInteger(context, "radius"))))); root.then(message("open", "Studio worlds are temporary Bukkit worlds opened by the Bukkit studio toolchain; modded servers cannot open ad-hoc runtime dimensions. Edit the pack under config/irisworldgen/packs/ and create a fresh world (or /iris regen) to see changes.")); + root.then(message("o", "Studio worlds are temporary Bukkit worlds opened by the Bukkit studio toolchain; modded servers cannot open ad-hoc runtime dimensions. Edit the pack under config/irisworldgen/packs/ and create a fresh world (or /iris regen) to see changes.")); root.then(message("close", "There are no studio worlds on modded servers (/iris studio open is Bukkit-only), so there is nothing to close.")); + root.then(message("x", "There are no studio worlds on modded servers (/iris studio open is Bukkit-only), so there is nothing to close.")); root.then(message("tpstudio", "There are no studio worlds on modded servers to teleport to; /iris studio open is Bukkit-only.")); + root.then(message("stp", "There are no studio worlds on modded servers to teleport to; /iris studio open is Bukkit-only.")); root.then(message("vscode", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/ directly in your editor.")); + root.then(message("vsc", "VSCode launch and workspace generation are desktop features of the Bukkit studio toolchain; edit config/irisworldgen/packs/ directly in your editor.")); root.then(message("update", "Workspace regeneration (.code-workspace + JSON schemas) reads Bukkit registries (SchemaBuilder); run /iris studio update on a Bukkit server against this pack.")); root.then(message("importvanilla", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("importv", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over.")); + root.then(message("iv", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over.")); root.then(message("noise", "The noise explorer is a desktop GUI launched from the Bukkit plugin.")); + root.then(message("nmap", "The noise explorer is a desktop GUI launched from the Bukkit plugin.")); root.then(message("map", "The world map renderer is a desktop GUI launched from the Bukkit plugin.")); + root.then(message("render", "The world map renderer is a desktop GUI launched from the Bukkit plugin.")); root.then(message("loot", "Loot simulation opens a Bukkit chest inventory GUI; it is not available on modded servers.")); root.then(message("profile", "Pack performance profiling is part of the Bukkit studio toolchain and is not ported to modded servers.")); root.then(message("spawn", "Iris entity spawning uses the Bukkit entity pipeline and is not ported to modded servers.")); + root.then(message("summon", "Iris entity spawning uses the Bukkit entity pipeline and is not ported to modded servers.")); root.then(message("objects", "The chunk object report reads Bukkit chunk data and is not ported to modded servers.")); + root.then(message("find-objects", "The chunk object report reads Bukkit chunk data and is not ported to modded servers.")); return root; } 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 new file mode 100644 index 000000000..3c8231ce5 --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldCommands.java @@ -0,0 +1,187 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.modded.IrisModdedChunkGenerator; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +public final class ModdedWorldCommands { + private static final Logger LOGGER = LoggerFactory.getLogger("Iris"); + private static final Predicate GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS); + private static final SuggestionProvider ENABLED_DIMENSIONS = (CommandContext context, SuggestionsBuilder builder) -> suggestEnabledDimensions(context, builder); + + private ModdedWorldCommands() { + } + + public static LiteralArgumentBuilder tree(String name) { + LiteralArgumentBuilder root = Commands.literal(name).requires(GATE); + + root.executes((CommandContext context) -> ModdedCommandHelp.send(context.getSource(), name)); + + root.then(Commands.literal("status") + .executes((CommandContext context) -> status(context.getSource()))); + root.then(Commands.literal("list") + .executes((CommandContext context) -> list(context.getSource()))); + root.then(Commands.literal("ls") + .executes((CommandContext context) -> list(context.getSource()))); + + root.then(enableTree("enable")); + root.then(enableTree("create")); + root.then(replaceOverworldTree()); + + root.then(Commands.literal("disable") + .then(Commands.argument("dimension", StringArgumentType.word()).suggests(ENABLED_DIMENSIONS) + .executes((CommandContext context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension"))))); + root.then(Commands.literal("remove") + .then(Commands.argument("dimension", StringArgumentType.word()).suggests(ENABLED_DIMENSIONS) + .executes((CommandContext context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension"))))); + root.then(Commands.literal("rm") + .then(Commands.argument("dimension", StringArgumentType.word()).suggests(ENABLED_DIMENSIONS) + .executes((CommandContext context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension"))))); + + return root; + } + + private static LiteralArgumentBuilder enableTree(String name) { + return Commands.literal(name) + .then(Commands.argument("dimension", StringArgumentType.word()) + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> enable(context.getSource(), + StringArgumentType.getString(context, "dimension"), + StringArgumentType.getString(context, "pack"), + StringArgumentType.getString(context, "pack"))) + .then(Commands.argument("packDimension", StringArgumentType.word()) + .executes((CommandContext context) -> enable(context.getSource(), + StringArgumentType.getString(context, "dimension"), + StringArgumentType.getString(context, "pack"), + StringArgumentType.getString(context, "packDimension")))))); + } + + private static LiteralArgumentBuilder replaceOverworldTree() { + return Commands.literal("replace-overworld") + .then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES) + .executes((CommandContext context) -> enable(context.getSource(), + "minecraft:overworld", + StringArgumentType.getString(context, "pack"), + StringArgumentType.getString(context, "pack"))) + .then(Commands.argument("packDimension", StringArgumentType.word()) + .executes((CommandContext context) -> enable(context.getSource(), + "minecraft:overworld", + StringArgumentType.getString(context, "pack"), + StringArgumentType.getString(context, "packDimension"))))); + } + + private static int enable(CommandSourceStack source, String targetDimension, String packName, String packDimension) { + try { + ModdedWorldDatapackWriter.WriteResult result = ModdedWorldDatapackWriter.enable(source.getServer(), targetDimension, packName, packDimension); + IrisModdedCommands.ok(source, "Enabled Iris world " + result.targetDimension() + " using pack '" + result.packName() + "' dimension '" + result.packDimension() + "'."); + IrisModdedCommands.ok(source, "Wrote " + result.dimensionFile().getPath()); + IrisModdedCommands.ok(source, "Wrote " + result.typeFile().getPath()); + IrisModdedCommands.ok(source, "Wrote " + result.packMetaFile().getPath()); + IrisModdedCommands.ok(source, "Restart the server, then use /execute in " + result.targetDimension() + " run tp 0 100 0 or a portal/modded dimension tool."); + if ("minecraft:overworld".equals(result.targetDimension())) { + IrisModdedCommands.ok(source, "minecraft:overworld replacement was explicitly requested."); + } + return 1; + } catch (IOException e) { + LOGGER.error("Iris world datapack write failed for {}", targetDimension, e); + IrisModdedCommands.fail(source, "Failed to write Iris world datapack: " + e.getMessage()); + return 0; + } catch (IllegalArgumentException e) { + IrisModdedCommands.fail(source, e.getMessage()); + return 0; + } + } + + private static int disable(CommandSourceStack source, String targetDimension) { + try { + File removed = ModdedWorldDatapackWriter.disable(source.getServer(), targetDimension); + IrisModdedCommands.ok(source, "Removed " + removed.getPath()); + IrisModdedCommands.ok(source, "Restart the server for the dimension removal to apply."); + return 1; + } catch (IOException e) { + LOGGER.error("Iris world datapack removal failed for {}", targetDimension, e); + IrisModdedCommands.fail(source, "Failed to remove Iris world datapack entry: " + e.getMessage()); + return 0; + } catch (IllegalArgumentException e) { + IrisModdedCommands.fail(source, e.getMessage()); + return 0; + } + } + + private static int status(CommandSourceStack source) { + list(source); + int loaded = 0; + MinecraftServer server = source.getServer(); + for (ServerLevel level : server.getAllLevels()) { + if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) { + loaded++; + IrisModdedCommands.ok(source, "Loaded Iris level: " + level.dimension().identifier() + " -> pack dimension '" + generator.dimensionKey() + "'"); + } + } + if (loaded == 0) { + IrisModdedCommands.fail(source, "No Iris dimensions are currently loaded. Enabled dimensions require a server restart before Minecraft loads them."); + } + return loaded > 0 ? 1 : 0; + } + + private static int list(CommandSourceStack source) { + try { + List dimensions = ModdedWorldDatapackWriter.enabledDimensions(source.getServer()); + IrisModdedCommands.ok(source, "Enabled Iris world datapack dimensions: " + dimensions.size()); + for (String dimension : dimensions) { + IrisModdedCommands.ok(source, " - " + dimension); + } + if (dimensions.isEmpty()) { + IrisModdedCommands.ok(source, "Use /iris world enable to create one without replacing the main world."); + } + return 1; + } catch (IOException e) { + LOGGER.error("Iris world datapack listing failed", e); + IrisModdedCommands.fail(source, "Failed to list Iris world datapack dimensions: " + e.getMessage()); + return 0; + } + } + + private static CompletableFuture suggestEnabledDimensions(CommandContext context, SuggestionsBuilder builder) { + try { + return SharedSuggestionProvider.suggest(ModdedWorldDatapackWriter.enabledDimensions(context.getSource().getServer()), builder); + } catch (IOException e) { + return Suggestions.empty(); + } + } +} diff --git a/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldDatapackWriter.java b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldDatapackWriter.java new file mode 100644 index 000000000..c37795d2b --- /dev/null +++ b/adapters/modded-common/src/main/java/art/arcane/iris/modded/command/ModdedWorldDatapackWriter.java @@ -0,0 +1,217 @@ +/* + * Iris is a World Generator for Minecraft Servers + * Copyright (c) 2026 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.modded.command; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.engine.object.IrisDimension; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.storage.LevelResource; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.stream.Stream; + +final class ModdedWorldDatapackWriter { + static final String WORLD_PACK_NAME = "iris"; + private static final String DEFAULT_NAMESPACE = "irisworldgen"; + private static final String TYPE_NAMESPACE = "irisworldgen"; + + private ModdedWorldDatapackWriter() { + } + + static WriteResult enable(MinecraftServer server, String targetDimension, String packName, String packDimension) throws IOException { + ResourceTarget target = ResourceTarget.parse(targetDimension); + String cleanPackName = cleanPackSegment(packName, "pack"); + String cleanPackDimension = cleanPackSegment(packDimension, "pack dimension"); + File packFolder = new File(ModdedPackCommands.packsRoot(), cleanPackName); + if (!packFolder.isDirectory()) { + throw new IllegalArgumentException("Pack '" + cleanPackName + "' was not found under " + ModdedPackCommands.packsRoot().getAbsolutePath()); + } + + IrisData data = IrisData.get(packFolder); + IrisDimension dimension = data.getDimensionLoader().load(cleanPackDimension); + if (dimension == null) { + throw new IllegalArgumentException("Pack '" + cleanPackName + "' does not contain dimensions/" + cleanPackDimension + ".json"); + } + + String typeKey = IrisDimension.sanitizeDimensionTypeKeyValue(cleanPackDimension); + File typeFile = dimensionTypeFile(server, typeKey); + File dimensionFile = dimensionFile(server, target); + File mcmeta = packMetaFile(server); + + writeParented(typeFile, dimension.getDimensionType().toJson(DataVersion.getLatest().get())); + writeParented(dimensionFile, dimensionJson(cleanPackDimension, typeKey)); + writePackMeta(mcmeta); + + return new WriteResult(target.id(), cleanPackName, cleanPackDimension, dimensionFile, typeFile, mcmeta); + } + + static File disable(MinecraftServer server, String targetDimension) throws IOException { + ResourceTarget target = ResourceTarget.parse(targetDimension); + File dimensionFile = dimensionFile(server, target); + if (!dimensionFile.isFile()) { + throw new IllegalArgumentException("No Iris world datapack dimension exists for " + target.id()); + } + Files.delete(dimensionFile.toPath()); + return dimensionFile; + } + + static List enabledDimensions(MinecraftServer server) throws IOException { + File dataRoot = new File(irisPackFolder(server), "data"); + if (!dataRoot.isDirectory()) { + return List.of(); + } + + List dimensions = new ArrayList<>(); + File[] namespaces = dataRoot.listFiles(File::isDirectory); + if (namespaces == null) { + return List.of(); + } + + for (File namespace : namespaces) { + File dimensionRoot = new File(namespace, "dimension"); + if (!dimensionRoot.isDirectory()) { + continue; + } + collectDimensionFiles(namespace.getName(), dimensionRoot, dimensions); + } + Collections.sort(dimensions); + return dimensions; + } + + static File packMetaFile(MinecraftServer server) { + return new File(irisPackFolder(server), "pack.mcmeta"); + } + + private static void collectDimensionFiles(String namespace, File dimensionRoot, List dimensions) throws IOException { + Path root = dimensionRoot.toPath(); + try (Stream stream = Files.walk(root)) { + stream.filter(Files::isRegularFile) + .filter((Path path) -> path.getFileName().toString().endsWith(".json")) + .forEach((Path path) -> { + String relative = root.relativize(path).toString().replace(File.separatorChar, '/'); + String idPath = relative.substring(0, relative.length() - ".json".length()); + dimensions.add(namespace + ":" + idPath); + }); + } + } + + private static File worldDatapacksFolder(MinecraftServer server) { + return server.getWorldPath(LevelResource.DATAPACK_DIR).toFile(); + } + + private static File irisPackFolder(MinecraftServer server) { + return new File(worldDatapacksFolder(server), WORLD_PACK_NAME); + } + + private static File dimensionTypeFile(MinecraftServer server, String typeKey) { + return new File(irisPackFolder(server), "data/" + TYPE_NAMESPACE + "/dimension_type/" + typeKey + ".json"); + } + + private static File dimensionFile(MinecraftServer server, ResourceTarget target) { + return new File(new File(irisPackFolder(server), "data/" + target.namespace() + "/dimension"), target.path() + ".json"); + } + + private static void writePackMeta(File output) throws IOException { + int packFormat = DataVersion.getLatest().getPackFormat(); + String json = "{\n" + + " \"pack\": {\n" + + " \"description\": \"Iris world and dimension type definitions.\",\n" + + " \"pack_format\": " + packFormat + ",\n" + + " \"min_format\": " + packFormat + ",\n" + + " \"max_format\": " + packFormat + "\n" + + " }\n" + + "}\n"; + writeParented(output, json); + } + + private static void writeParented(File output, String text) throws IOException { + File parent = output.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + Files.writeString(output.toPath(), text, StandardCharsets.UTF_8); + } + + private static String dimensionJson(String packDimension, String typeKey) { + return "{\n" + + " \"type\": \"" + TYPE_NAMESPACE + ":" + typeKey + "\",\n" + + " \"generator\": {\n" + + " \"type\": \"irisworldgen:iris\",\n" + + " \"dimension\": \"" + escape(packDimension) + "\",\n" + + " \"biome_source\": {\n" + + " \"type\": \"minecraft:fixed\",\n" + + " \"biome\": \"minecraft:plains\"\n" + + " }\n" + + " }\n" + + "}\n"; + } + + private static String cleanPackSegment(String value, String label) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("Missing " + label + "."); + } + String clean = value.trim(); + if (clean.contains("/") || clean.contains("..") || clean.contains("\\") || clean.contains(":")) { + throw new IllegalArgumentException("Invalid " + label + " '" + value + "'."); + } + return clean; + } + + private static String escape(String value) { + return value.replace("\\", "\\\\").replace("\"", "\\\""); + } + + record WriteResult(String targetDimension, String packName, String packDimension, File dimensionFile, File typeFile, File packMetaFile) { + } + + private record ResourceTarget(String namespace, String path) { + static ResourceTarget parse(String value) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException("Missing dimension id."); + } + + String normalized = value.trim().toLowerCase(Locale.ROOT); + String namespace = DEFAULT_NAMESPACE; + String path = normalized; + int colon = normalized.indexOf(':'); + if (colon >= 0) { + namespace = normalized.substring(0, colon); + path = normalized.substring(colon + 1); + } + + if (!namespace.matches("[a-z0-9_.-]+") || !path.matches("[a-z0-9_./-]+") || path.startsWith("/") || path.endsWith("/") || path.contains("..")) { + throw new IllegalArgumentException("Invalid dimension id '" + value + "'. Use name or namespace:path."); + } + return new ResourceTarget(namespace, path); + } + + String id() { + return namespace + ":" + path; + } + } +} diff --git a/adapters/modded-common/src/main/resources/data/minecraft/dimension/overworld.json b/adapters/modded-common/src/main/resources/data/minecraft/dimension/overworld.json deleted file mode 100644 index 3a77a41d3..000000000 --- a/adapters/modded-common/src/main/resources/data/minecraft/dimension/overworld.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "type": "irisworldgen:overworld", - "generator": { - "type": "irisworldgen:iris", - "dimension": "overworld", - "biome_source": { - "type": "minecraft:fixed", - "biome": "minecraft:plains" - } - } -} diff --git a/adapters/neoforge/build.gradle b/adapters/neoforge/build.gradle index 6db8e0a92..9137c89e2 100644 --- a/adapters/neoforge/build.gradle +++ b/adapters/neoforge/build.gradle @@ -30,7 +30,10 @@ Properties rootProperties = new Properties() file('../../gradle.properties').withInputStream { InputStream stream -> rootProperties.load(stream) } String irisVersion = providers.gradleProperty('irisVersion').getOrElse(rootProperties.getProperty('irisVersion', '4.0.0-26.1')) String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse(rootProperties.getProperty('minecraftVersion', '26.1.2')) -String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.1.2.75') +String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse(rootProperties.getProperty('neoForgeVersion', '26.1.2.75')) +Closure irisArtifactName = { String platform, String targetVersion -> + return "Iris v${project.version} [${platform}] ${targetVersion}.jar" +} group = 'art.arcane' version = irisVersion @@ -168,7 +171,10 @@ processResources { } tasks.named('shadowJar', ShadowJar).configure { - archiveFileName.set("Iris-${project.version}+mc${minecraftVersion}-neoforge.jar") + doFirst { + delete(layout.buildDirectory.file("libs/Iris-${project.version}+mc${minecraftVersion}-neoforge.jar")) + } + archiveFileName.set(irisArtifactName('NeoForge', "${minecraftVersion}+${neoForgeVersion}")) configurations = [project.configurations.named('bundle').get()] mergeServiceFiles() exclude('META-INF/maven/**') diff --git a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java index cef45f1d5..458b4f6b7 100644 --- a/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java +++ b/adapters/neoforge/src/main/java/art/arcane/iris/neoforge/IrisNeoForgeBootstrap.java @@ -51,7 +51,6 @@ public final class IrisNeoForgeBootstrap { public IrisNeoForgeBootstrap(IEventBus modBus) { ModdedEngineBootstrap.initialize(new NeoForgeModdedLoader()); - art.arcane.iris.modded.ModdedPackInstaller.ensureDefaultPack(ModdedEngineBootstrap.loader().configDir()); String modVersion = ModList.get().getModContainerById("irisworldgen") .map((ModContainer container) -> container.getModInfo().getVersion().toString()) .orElse("unknown"); diff --git a/adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml index 618c88169..dfcc36f8c 100644 --- a/adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/adapters/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -6,7 +6,7 @@ license = "GPL-3.0" modId = "irisworldgen" version = "${version}" displayName = "Iris" -description = "Iris World Generation Engine (NeoForge adapter - native chunk generator, overworld override datapack, engine lifecycle)" +description = "Iris World Generation Engine (NeoForge adapter - native chunk generator, explicit Iris world datapack workflow, engine lifecycle)" authors = "Arcane Arts (Volmit Software)" [[dependencies.irisworldgen]] diff --git a/build.gradle b/build.gradle index f70e49a1f..0c14210b1 100644 --- a/build.gradle +++ b/build.gradle @@ -48,9 +48,28 @@ project(':core') { version = rootProject.version } String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.1.2') +String fabricLoaderVersion = providers.gradleProperty('fabricLoaderVersion').getOrElse('0.19.3') +String forgeVersion = providers.gradleProperty('forgeVersion').getOrElse('26.1.2-64.0.9') +String neoForgeVersion = providers.gradleProperty('neoForgeVersion').getOrElse('26.1.2.75') String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate') .orElse('com.github.VolmitSoftware:VolmLib:master-SNAPSHOT') .get() +Closure loaderDisplayVersion = { String loaderVersion -> + String coordinatePrefix = "${minecraftVersion}-" + if (loaderVersion.startsWith(coordinatePrefix)) { + return loaderVersion.substring(coordinatePrefix.length()) + } + + return loaderVersion +} +Closure irisArtifactName = { String platform, String targetVersion -> + return "Iris v${project.version} [${platform}] ${targetVersion}.jar" +} +String bukkitArtifactName = irisArtifactName('CraftBukkit', minecraftVersion) +String fabricArtifactName = irisArtifactName('Fabric', "${minecraftVersion}+${loaderDisplayVersion(fabricLoaderVersion)}") +String forgeArtifactName = irisArtifactName('Forge', "${minecraftVersion}+${loaderDisplayVersion(forgeVersion)}") +String neoForgeArtifactName = irisArtifactName('NeoForge', "${minecraftVersion}+${loaderDisplayVersion(neoForgeVersion)}") +extensions.extraProperties.set('bukkitArtifactName', bukkitArtifactName) apply plugin: ApiGenerator @@ -115,64 +134,102 @@ tasks.named('jar', Jar).configure { inputs.files(included) duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(jarJar, provider { included.resolve().collect { zipTree(it) } }) - archiveFileName.set("Iris-${project.version}.jar") + doFirst { + delete(layout.buildDirectory.file("libs/Iris-${project.version}.jar")) + } + archiveFileName.set(bukkitArtifactName) } tasks.register('iris', Copy) { group = 'iris' dependsOn('jar') - from(layout.buildDirectory.file("libs/Iris-${project.version}.jar")) + from(layout.buildDirectory.file("libs/${bukkitArtifactName}")) into(layout.buildDirectory) + doFirst { + delete(layout.buildDirectory.file("Iris-${project.version}.jar")) + } } tasks.register('buildBukkit', Copy) { group = 'iris' dependsOn('jar') - from(layout.buildDirectory.file("libs/Iris-${project.version}.jar")) - rename { "Iris-${project.version}+mc${minecraftVersion}.jar" } + from(layout.buildDirectory.file("libs/${bukkitArtifactName}")) into(layout.projectDirectory.dir('dist')) + doFirst { + delete(layout.projectDirectory.file("dist/Iris-${project.version}+mc${minecraftVersion}.jar")) + } } tasks.register('fabricJar', Exec) { group = 'iris' workingDir = layout.projectDirectory.dir('adapters/fabric').asFile String wrapperScript = System.getProperty('os.name').toLowerCase().contains('windows') ? 'gradlew.bat' : 'gradlew' - commandLine(layout.projectDirectory.file(wrapperScript).asFile.absolutePath, 'shadowJar', '--console=plain') + commandLine( + layout.projectDirectory.file(wrapperScript).asFile.absolutePath, + 'shadowJar', + '--console=plain', + "-PirisVersion=${project.version}", + "-PminecraftVersion=${minecraftVersion}", + "-PfabricLoaderVersion=${fabricLoaderVersion}" + ) } tasks.register('buildFabric', Copy) { group = 'iris' dependsOn('fabricJar') - from(layout.projectDirectory.file("adapters/fabric/build/libs/Iris-${project.version}+mc${minecraftVersion}-fabric.jar")) + from(layout.projectDirectory.file("adapters/fabric/build/libs/${fabricArtifactName}")) into(layout.projectDirectory.dir('dist')) + doFirst { + delete(layout.projectDirectory.file("dist/Iris-${project.version}+mc${minecraftVersion}-fabric.jar")) + } } tasks.register('forgeJar', Exec) { group = 'iris' workingDir = layout.projectDirectory.dir('adapters/forge').asFile String wrapperScript = System.getProperty('os.name').toLowerCase().contains('windows') ? 'gradlew.bat' : 'gradlew' - commandLine(layout.projectDirectory.file(wrapperScript).asFile.absolutePath, 'shadowJar', '--console=plain') + commandLine( + layout.projectDirectory.file(wrapperScript).asFile.absolutePath, + 'shadowJar', + '--console=plain', + "-PirisVersion=${project.version}", + "-PminecraftVersion=${minecraftVersion}", + "-PforgeVersion=${forgeVersion}" + ) } tasks.register('buildForge', Copy) { group = 'iris' dependsOn('forgeJar') - from(layout.projectDirectory.file("adapters/forge/build/libs/Iris-${project.version}+mc${minecraftVersion}-forge.jar")) + from(layout.projectDirectory.file("adapters/forge/build/libs/${forgeArtifactName}")) into(layout.projectDirectory.dir('dist')) + doFirst { + delete(layout.projectDirectory.file("dist/Iris-${project.version}+mc${minecraftVersion}-forge.jar")) + } } tasks.register('neoforgeJar', Exec) { group = 'iris' workingDir = layout.projectDirectory.dir('adapters/neoforge').asFile String wrapperScript = System.getProperty('os.name').toLowerCase().contains('windows') ? 'gradlew.bat' : 'gradlew' - commandLine(layout.projectDirectory.file(wrapperScript).asFile.absolutePath, 'shadowJar', '--console=plain') + commandLine( + layout.projectDirectory.file(wrapperScript).asFile.absolutePath, + 'shadowJar', + '--console=plain', + "-PirisVersion=${project.version}", + "-PminecraftVersion=${minecraftVersion}", + "-PneoForgeVersion=${neoForgeVersion}" + ) } tasks.register('buildNeoforge', Copy) { group = 'iris' dependsOn('neoforgeJar') - from(layout.projectDirectory.file("adapters/neoforge/build/libs/Iris-${project.version}+mc${minecraftVersion}-neoforge.jar")) + from(layout.projectDirectory.file("adapters/neoforge/build/libs/${neoForgeArtifactName}")) into(layout.projectDirectory.dir('dist')) + doFirst { + delete(layout.projectDirectory.file("dist/Iris-${project.version}+mc${minecraftVersion}-neoforge.jar")) + } } tasks.register('buildAll') { @@ -322,7 +379,7 @@ void registerCustomOutputTask(String name, String path) { group = 'development' outputs.upToDateWhen { false } dependsOn('iris') - from(layout.buildDirectory.file("Iris-${project.version}.jar")) + from(layout.buildDirectory.file((String) project.ext.bukkitArtifactName)) into(file(path)) rename { String ignored -> 'Iris.jar' } } @@ -337,7 +394,7 @@ void registerCustomOutputTaskUnix(String name, String path) { group = 'development' outputs.upToDateWhen { false } dependsOn('iris') - from(layout.buildDirectory.file("Iris-${project.version}.jar")) + from(layout.buildDirectory.file((String) project.ext.bukkitArtifactName)) into(file(path)) rename { String ignored -> 'Iris.jar' } } diff --git a/core/src/main/java/art/arcane/iris/core/nms/datapack/v1217/DataFixerV1217.java b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1217/DataFixerV1217.java index d7b239388..6ad102b94 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/datapack/v1217/DataFixerV1217.java +++ b/core/src/main/java/art/arcane/iris/core/nms/datapack/v1217/DataFixerV1217.java @@ -90,12 +90,21 @@ public class DataFixerV1217 extends DataFixerV1213 { @Override public JSONObject fixCustomBiome(IrisBiomeCustom biome, JSONObject json) { json = super.fixCustomBiome(biome, json); - var effects = json.getJSONObject("effects"); - var attributes = new JSONObject(); + JSONObject effects = json.getJSONObject("effects"); + JSONObject attributes = new JSONObject(); attributes.put("minecraft:visual/fog_color", effects.remove("fog_color")); attributes.put("minecraft:visual/sky_color", effects.remove("sky_color")); + attributes.put("minecraft:visual/water_color", effects.remove("water_color")); attributes.put("minecraft:visual/water_fog_color", effects.remove("water_fog_color")); + Object grassColor = effects.remove("grass_color"); + if (grassColor != null) { + attributes.put("minecraft:visual/grass_color", grassColor); + } + Object foliageColor = effects.remove("foliage_color"); + if (foliageColor != null) { + attributes.put("minecraft:visual/foliage_color", foliageColor); + } JSONObject particle = (JSONObject) effects.remove("particle"); if (particle != null) { @@ -112,7 +121,7 @@ public class DataFixerV1217 extends DataFixerV1213 { public void fixDimension(Dimension dimension, JSONObject json) { super.fixDimension(dimension, json); - var attributes = new JSONObject(); + JSONObject attributes = new JSONObject(); if ((Boolean) json.remove("ultrawarm")) { attributes.put("minecraft:gameplay/water_evaporates", true); attributes.put("minecraft:gameplay/fast_lava", true); @@ -138,8 +147,10 @@ public class DataFixerV1217 extends DataFixerV1213 { attributes.put("minecraft:gameplay/piglins_zombify", !(Boolean) json.remove("piglin_safe")); attributes.put("minecraft:gameplay/can_start_raid", json.remove("has_raids")); - var cloud_height = json.remove("cloud_height"); - if (cloud_height != null) attributes.put("minecraft:visual/cloud_height", cloud_height); + Object cloudHeight = json.remove("cloud_height"); + if (cloudHeight != null) { + attributes.put("minecraft:visual/cloud_height", cloudHeight); + } boolean natural = (Boolean) json.remove("natural"); attributes.put("minecraft:gameplay/nether_portal_spawns_piglin", natural); @@ -152,7 +163,7 @@ public class DataFixerV1217 extends DataFixerV1213 { json.put("attributes", attributes); json.remove("effects"); - var defaults = new JSONObject(DIMENSIONS.get(dimension)); + JSONObject defaults = new JSONObject(DIMENSIONS.get(dimension)); merge(json, defaults); } diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java index 1e304510d..73b1d5616 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisBiomeActuator.java @@ -22,6 +22,7 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineAssignedActuator; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisBiomeCustom; +import art.arcane.iris.platform.bukkit.BukkitBiome; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.iris.util.project.hunk.Hunk; @@ -53,15 +54,27 @@ public class IrisBiomeActuator extends EngineAssignedActuator { for (int zf = 0; zf < h.getDepth(); zf++) { ib = context.getBiome().get(xf, zf); MatterBiomeInject matter; + PlatformBiome biome; if (ib.isCustom()) { IrisBiomeCustom custom = ib.getCustomBiome(rng, x, 0, z); - matter = BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(getDimension().getLoadKey() + ":" + custom.getId())); + String key = getDimension().getLoadKey() + ":" + custom.getId(); + biome = IrisPlatforms.get().registries().biome(key); + matter = BiomeInjectMatter.get(IrisPlatforms.get().biomeWriter().biomeIdFor(key)); } else { Biome v = ib.getSkyBiome(rng, x, 0, z); + PlatformBiome fallback = BukkitBiome.of(v); + PlatformBiome resolved = IrisPlatforms.get().registries().biome(fallback.key()); + biome = resolved == null ? fallback : resolved; matter = BiomeInjectMatter.get(v); } + if (biome != null) { + for (int yf = 0; yf < h.getHeight(); yf++) { + h.set(xf, yf, zf, biome); + } + } + getEngine().getMantle().getMantle().set(x + xf, 0, z + zf, matter); } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java index cbdd4df3a..6456c7396 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisBiomeCustom.java @@ -180,7 +180,7 @@ public class IrisBiomeCustom { private int parseColor(String c) { String v = (c.startsWith("#") ? c : "#" + c).trim(); try { - return Color.decode(v).getRGB(); + return Color.decode(v).getRGB() & 0x00FFFFFF; } catch (Throwable e) { IrisLogging.reportError(e); IrisLogging.error("Error Parsing '''color''', (" + c + ")"); diff --git a/gradle.properties b/gradle.properties index 851b86f52..145facb7d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,3 +25,6 @@ nmsTools.repo-url=https://repo.codemc.org/repository/nms/ nmsTools.specialSourceVersion=1.11.4 irisVersion=4.0.0-26.1 minecraftVersion=26.1.2 +fabricLoaderVersion=0.19.3 +forgeVersion=26.1.2-64.0.9 +neoForgeVersion=26.1.2.75