This commit is contained in:
Brian Neumann-Fopiano
2026-06-12 06:58:32 -04:00
parent b98a341dc7
commit e2f6061f98
29 changed files with 4405 additions and 20 deletions
+18
View File
@@ -90,13 +90,25 @@ configurations.named('bundle').configure {
configurations.compileClasspath.extendsFrom(configurations.devBundle)
configurations.runtimeClasspath.extendsFrom(configurations.devBundle)
configurations.create('jij') {
transitive = true
}
dependencies {
minecraft("com.mojang:minecraft:${minecraftVersion}")
implementation("net.fabricmc:fabric-loader:${fabricLoaderVersion}")
jij('net.fabricmc.fabric-api:fabric-api-base:2.0.3+ece063234c')
jij('net.fabricmc.fabric-api:fabric-registry-sync-v0:7.1.0+2fa62b4e4c')
jij('net.fabricmc.fabric-api:fabric-resource-loader-v1:2.0.10+7c44c7324c')
jij('net.fabricmc.fabric-api:fabric-lifecycle-events-v1:4.1.0+6d50a0854c')
jij('net.fabricmc.fabric-api:fabric-command-api-v2:3.0.5+e2bdee784c')
jij('net.fabricmc.fabric-api:fabric-events-interaction-v0:5.2.2+07b380be4c')
implementation('net.fabricmc.fabric-api:fabric-api-base:2.0.3+ece063234c')
implementation('net.fabricmc.fabric-api:fabric-registry-sync-v0:7.1.0+2fa62b4e4c')
implementation('net.fabricmc.fabric-api:fabric-resource-loader-v1:2.0.10+7c44c7324c')
implementation('net.fabricmc.fabric-api:fabric-lifecycle-events-v1:4.1.0+6d50a0854c')
implementation('net.fabricmc.fabric-api:fabric-command-api-v2:3.0.5+e2bdee784c')
implementation('net.fabricmc.fabric-api:fabric-events-interaction-v0:5.2.2+07b380be4c')
compileOnly('org.slf4j:slf4j-api:2.0.17')
compileOnly(libs.spigot) {
transitive = false
@@ -174,6 +186,12 @@ tasks.named('shadowJar', ShadowJar).configure {
exclude('META-INF/*.DSA')
exclude('META-INF/*.RSA')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
exclude('oshi.properties')
from(project.configurations.named('jij')) {
into('META-INF/jars')
rename { String fileName -> fileName.replaceAll(/-[0-9][^-]*\.jar$/, '.jar') }
}
}
tasks.named('assemble').configure {
@@ -39,6 +39,13 @@ public final class FabricModdedLoader implements ModdedLoader {
.orElse("unknown");
}
@Override
public String modVersion() {
return FabricLoader.getInstance().getModContainer("irisworldgen")
.map((ModContainer container) -> container.getMetadata().getVersion().getFriendlyString())
.orElse("unknown");
}
@Override
public MinecraftServer currentServer() {
Object instance = FabricLoader.getInstance().getGameInstance();
@@ -23,14 +23,32 @@ import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedParityProbe;
import art.arcane.iris.modded.ModdedWorldCheck;
import art.arcane.iris.modded.ModdedWorldEngines;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedObjectUndo;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.brigadier.CommandDispatcher;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.BlockHitResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -40,6 +58,7 @@ 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())
@@ -53,7 +72,17 @@ public final class IrisFabricBootstrap implements ModInitializer {
ModdedEngineBootstrap.bind();
Registry.register(BuiltInRegistries.CHUNK_GENERATOR, Identifier.fromNamespaceAndPath("irisworldgen", "iris"), IrisModdedChunkGenerator.CODEC);
LOGGER.info("Iris chunk generator registered as irisworldgen:iris");
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> ModdedWorldEngines.shutdown());
ServerLifecycleEvents.SERVER_STOPPING.register((MinecraftServer server) -> {
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedWorldEngines.shutdown();
});
CommandRegistrationCallback.EVENT.register((CommandDispatcher<CommandSourceStack> dispatcher, CommandBuildContext buildContext, Commands.CommandSelection selection) -> IrisModdedCommands.register(dispatcher));
AttackBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockPos pos, Direction direction) ->
ModdedWandService.attackBlock(player, level, hand, pos) ? InteractionResult.SUCCESS : InteractionResult.PASS);
UseBlockCallback.EVENT.register((Player player, Level level, InteractionHand hand, BlockHitResult hit) ->
ModdedWandService.useBlock(player, level, hand, hit.getBlockPos()) ? InteractionResult.SUCCESS : InteractionResult.PASS);
ServerTickEvents.END_SERVER_TICK.register((MinecraftServer server) -> ModdedWandService.serverTick(server));
String parity = System.getProperty("iris.parity");
if (parity != null) {
@@ -13,11 +13,17 @@
"entrypoints": {
"main": ["art.arcane.iris.fabric.IrisFabricBootstrap"]
},
"jars": [
{ "file": "META-INF/jars/fabric-api-base.jar" },
{ "file": "META-INF/jars/fabric-registry-sync-v0.jar" },
{ "file": "META-INF/jars/fabric-resource-loader-v1.jar" },
{ "file": "META-INF/jars/fabric-lifecycle-events-v1.jar" },
{ "file": "META-INF/jars/fabric-networking-api-v1.jar" },
{ "file": "META-INF/jars/fabric-command-api-v2.jar" },
{ "file": "META-INF/jars/fabric-events-interaction-v0.jar" }
],
"depends": {
"fabricloader": ">=0.19.0",
"fabric-registry-sync-v0": "*",
"fabric-resource-loader-v1": "*",
"fabric-lifecycle-events-v1": "*",
"minecraft": "~${minecraftVersion}",
"java": ">=25"
}
+2
View File
@@ -208,6 +208,8 @@ tasks.named('shadowJar', ShadowJar).configure {
exclude('META-INF/*.RSA')
exclude('org/apache/commons/lang/enum/**')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
exclude('oshi.properties')
}
tasks.named('assemble').configure {
@@ -40,6 +40,13 @@ public final class ForgeModdedLoader implements ModdedLoader {
return FMLLoader.versionInfo().mcVersion();
}
@Override
public String modVersion() {
return ModList.getModContainerById("irisworldgen")
.map((net.minecraftforge.fml.ModContainer container) -> container.getModInfo().getVersion().toString())
.orElse("unknown");
}
@Override
public MinecraftServer currentServer() {
return ServerLifecycleHooks.getCurrentServer();
@@ -23,9 +23,15 @@ import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedParityProbe;
import art.arcane.iris.modded.ModdedWorldCheck;
import art.arcane.iris.modded.ModdedWorldEngines;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedObjectUndo;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraftforge.event.RegisterCommandsEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
import net.minecraftforge.event.server.ServerStoppingEvent;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
@@ -34,6 +40,8 @@ import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.fml.loading.FMLLoader;
import net.minecraftforge.fml.loading.VersionInfo;
import net.minecraftforge.registries.DeferredRegister;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,6 +51,7 @@ 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");
@@ -57,7 +66,17 @@ public final class IrisForgeBootstrap {
chunkGenerators.register(context.getModBusGroup());
LOGGER.info("Iris chunk generator registered as irisworldgen:iris");
ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> ModdedWorldEngines.shutdown());
ServerStoppingEvent.BUS.addListener((ServerStoppingEvent event) -> {
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedWorldEngines.shutdown();
});
RegisterCommandsEvent.BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
PlayerInteractEvent.LeftClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.LeftClickBlock>) (PlayerInteractEvent.LeftClickBlock event) ->
ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
PlayerInteractEvent.RightClickBlock.BUS.addListener((Predicate<PlayerInteractEvent.RightClickBlock>) (PlayerInteractEvent.RightClickBlock event) ->
ModdedWandService.useBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos()));
TickEvent.ServerTickEvent.Post.BUS.addListener((TickEvent.ServerTickEvent.Post event) -> ModdedWandService.serverTick(event.server()));
String parity = System.getProperty("iris.parity");
if (parity != null) {
@@ -134,6 +134,18 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
}
}
public String dimensionKey() {
return dimensionKey;
}
public Engine engineIfBound() {
return engine;
}
public Engine commandEngine() {
return engine();
}
@Override
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) {
Engine generationEngine = engine();
@@ -192,6 +204,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
return raced != null ? raced : resolved;
}
public BiomeResolver regenBiomeResolver(Registry<Biome> registry, Hunk<PlatformBiome> biomes, ChunkPos pos) {
Engine current = engine();
int dimMinY = current.getMinHeight();
int height = current.getMaxHeight() - dimMinY;
return new HunkBiomeResolver(this, biomes, registry, pos, dimMinY, height);
}
private void writeBlocks(ChunkAccess chunk, ModdedBlockBuffer blocks, int dimMinY, int height) {
int chunkMinY = chunk.getMinY();
int chunkMaxY = chunkMinY + chunk.getHeight();
@@ -28,6 +28,8 @@ public interface ModdedLoader {
String minecraftVersion();
String modVersion();
MinecraftServer currentServer();
Path configDir();
@@ -0,0 +1,159 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zeroturnaround.zip.ZipUtil;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern;
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<String> feedback) {
if (pack == null || !PACK_NAME.matcher(pack).matches()) {
feedback.accept("Invalid pack name '" + pack + "' (allowed: a-z, 0-9, _ and -)");
return false;
}
if (branch == null || !BRANCH_NAME.matcher(branch).matches()) {
feedback.accept("Invalid branch name '" + branch + "' (allowed: letters, digits, . _ and -)");
return false;
}
Path target = configDir.resolve("irisworldgen").resolve("packs").resolve(pack);
String url = "https://codeload.github.com/IrisDimensions/" + pack + "/zip/refs/heads/" + branch;
try {
Path work = Files.createTempDirectory("iris-pack-dl");
Path zip = work.resolve(pack + ".zip");
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(30))
.build();
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofMinutes(5))
.GET()
.build();
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() != 200) {
feedback.accept("Pack download failed: HTTP " + response.statusCode() + " from " + url);
return false;
}
try (InputStream in = response.body()) {
Files.copy(in, zip, StandardCopyOption.REPLACE_EXISTING);
}
Path extracted = work.resolve("extracted");
ZipUtil.unpack(zip.toFile(), extracted.toFile());
Path root = singleRoot(extracted);
Files.createDirectories(target.getParent());
deleteRecursively(target);
copyRecursively(root, target);
deleteRecursively(work);
feedback.accept("Iris installed pack '" + pack + "' (branch " + branch + ") into " + target);
return true;
} catch (IOException | InterruptedException error) {
LOGGER.error("Iris pack download failed for IrisDimensions/{} ({})", pack, branch, error);
feedback.accept("Pack download failed: " + error.getClass().getSimpleName() + (error.getMessage() == null ? "" : " - " + error.getMessage()));
if (error instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
return false;
}
}
private static Path singleRoot(Path extracted) throws IOException {
try (Stream<Path> entries = Files.list(extracted)) {
List<Path> children = entries.toList();
if (children.size() == 1 && Files.isDirectory(children.get(0))) {
return children.get(0);
}
return extracted;
}
}
private static void copyRecursively(Path source, Path target) throws IOException {
try (Stream<Path> walk = Files.walk(source)) {
for (Path path : walk.toList()) {
Path destination = target.resolve(source.relativize(path).toString());
if (Files.isDirectory(path)) {
Files.createDirectories(destination);
} else {
Files.createDirectories(destination.getParent());
Files.copy(path, destination, StandardCopyOption.REPLACE_EXISTING);
}
}
}
}
private static void deleteRecursively(Path root) throws IOException {
if (!Files.exists(root)) {
return;
}
try (Stream<Path> walk = Files.walk(root)) {
for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
Files.delete(path);
}
}
}
}
@@ -0,0 +1,698 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.IrisStructureLocator;
import art.arcane.iris.engine.framework.Locator;
import art.arcane.iris.engine.framework.WrongEngineBroException;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedLoader;
import art.arcane.iris.modded.ModdedPackInstaller;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.Position2;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.IntegerArgumentType;
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.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;
import net.minecraft.world.entity.Relative;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.Heightmap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
public final class IrisModdedCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private static final long LOCATE_TIMEOUT_MS = 120000L;
private static final SuggestionProvider<CommandSourceStack> BIOME_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestBiomeKeys(context, builder);
private static final SuggestionProvider<CommandSourceStack> REGION_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestRegionKeys(context, builder);
private static final SuggestionProvider<CommandSourceStack> OBJECT_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestObjectKeys(context, builder);
private static final SuggestionProvider<CommandSourceStack> STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestStructureKeys(context, builder);
private static final SuggestionProvider<CommandSourceStack> POI_TYPES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(List.of("buried_treasure"), builder);
static final SuggestionProvider<CommandSourceStack> PACK_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestPackNames(builder);
private static final SuggestionProvider<CommandSourceStack> DIMENSION_NAMES = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestDimensionNames(context, builder);
private IrisModdedCommands() {
}
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("iris");
root.then(Commands.literal("version")
.executes((CommandContext<CommandSourceStack> context) -> version(context.getSource())));
root.then(Commands.literal("info").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> info(context.getSource(), null))
.then(Commands.argument("dimension", StringArgumentType.greedyString()).suggests(DIMENSION_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> info(context.getSource(), StringArgumentType.getString(context, "dimension")))));
root.then(Commands.literal("what").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> what(context.getSource())));
root.then(gotoTree("goto"));
root.then(gotoTree("find"));
root.then(Commands.literal("seed").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> seed(context.getSource())));
root.then(goldenhashTree());
root.then(Commands.literal("download").requires(GATE)
.then(Commands.argument("pack", StringArgumentType.word()).suggests(PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> download(context.getSource(), StringArgumentType.getString(context, "pack"), "master"))
.then(Commands.argument("branch", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) -> download(context.getSource(), StringArgumentType.getString(context, "pack"), StringArgumentType.getString(context, "branch"))))));
root.then(Commands.literal("metrics").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> metrics(context.getSource())));
root.then(Commands.literal("regen").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> regen(context.getSource(), 0))
.then(Commands.argument("radius", IntegerArgumentType.integer(0, 64))
.executes((CommandContext<CommandSourceStack> context) -> regen(context.getSource(), IntegerArgumentType.getInteger(context, "radius")))));
root.then(pregenTree());
root.then(Commands.literal("wand").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> ModdedObjectCommands.giveWand(context.getSource())));
root.then(ModdedObjectCommands.tree());
root.then(editTree());
root.then(ModdedStudioCommands.tree());
root.then(ModdedPackCommands.tree());
root.then(ModdedDatapackCommands.tree());
root.then(ModdedStructureCommands.tree());
dispatcher.register(root);
LOGGER.info("Iris /iris command tree registered");
}
private static LiteralArgumentBuilder<CommandSourceStack> gotoTree(String name) {
return Commands.literal(name).requires(GATE)
.then(Commands.literal("biome")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(BIOME_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> gotoBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("region")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(REGION_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> gotoRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("object")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> gotoObject(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("structure")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(STRUCTURE_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> gotoStructure(context.getSource(), StringArgumentType.getString(context, "key")))))
.then(Commands.literal("poi")
.then(Commands.argument("type", StringArgumentType.greedyString()).suggests(POI_TYPES)
.executes((CommandContext<CommandSourceStack> context) -> gotoPoi(context.getSource(), StringArgumentType.getString(context, "type")))));
}
private static LiteralArgumentBuilder<CommandSourceStack> pregenTree() {
return Commands.literal("pregen").requires(GATE)
.then(Commands.literal("start")
.then(Commands.argument("radius", IntegerArgumentType.integer(1, 100000))
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 0, 0))
.then(Commands.argument("x", IntegerArgumentType.integer())
.then(Commands.argument("z", IntegerArgumentType.integer())
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(),
IntegerArgumentType.getInteger(context, "radius"),
IntegerArgumentType.getInteger(context, "x"),
IntegerArgumentType.getInteger(context, "z")))))))
.then(Commands.literal("stop")
.executes((CommandContext<CommandSourceStack> context) -> pregenStop(context.getSource())))
.then(Commands.literal("pause")
.executes((CommandContext<CommandSourceStack> context) -> pregenPause(context.getSource())))
.then(Commands.literal("status")
.executes((CommandContext<CommandSourceStack> context) -> pregenStatus(context.getSource())));
}
private static LiteralArgumentBuilder<CommandSourceStack> goldenhashTree() {
LiteralArgumentBuilder<CommandSourceStack> radiusAndThreads = Commands.literal("goldenhash").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> goldenhash(context.getSource(), 8, 8, ModdedGoldenHash.Mode.AUTO));
attachModes(radiusAndThreads, (CommandContext<CommandSourceStack> context) -> 8, (CommandContext<CommandSourceStack> context) -> 8);
com.mojang.brigadier.builder.RequiredArgumentBuilder<CommandSourceStack, Integer> radius = Commands.argument("radius", IntegerArgumentType.integer(0, 256))
.executes((CommandContext<CommandSourceStack> context) -> goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), 8, ModdedGoldenHash.Mode.AUTO));
attachModes(radius, (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext<CommandSourceStack> context) -> 8);
com.mojang.brigadier.builder.RequiredArgumentBuilder<CommandSourceStack, Integer> threads = Commands.argument("threads", IntegerArgumentType.integer(1, 64))
.executes((CommandContext<CommandSourceStack> context) -> goldenhash(context.getSource(), IntegerArgumentType.getInteger(context, "radius"), IntegerArgumentType.getInteger(context, "threads"), ModdedGoldenHash.Mode.AUTO));
attachModes(threads, (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "radius"), (CommandContext<CommandSourceStack> context) -> IntegerArgumentType.getInteger(context, "threads"));
radius.then(threads);
radiusAndThreads.then(radius);
return radiusAndThreads;
}
private interface IntExtractor {
int extract(CommandContext<CommandSourceStack> context);
}
private static void attachModes(com.mojang.brigadier.builder.ArgumentBuilder<CommandSourceStack, ?> node, IntExtractor radius, IntExtractor threads) {
node.then(Commands.literal("capture")
.executes((CommandContext<CommandSourceStack> context) -> goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.CAPTURE)));
node.then(Commands.literal("verify")
.executes((CommandContext<CommandSourceStack> context) -> goldenhash(context.getSource(), radius.extract(context), threads.extract(context), ModdedGoldenHash.Mode.VERIFY)));
}
private static LiteralArgumentBuilder<CommandSourceStack> editTree() {
String message = "/iris edit opens pack JSON files in a desktop editor through the Bukkit studio toolchain; "
+ "on modded servers edit the pack files directly under config/irisworldgen/packs/<pack>/.";
LiteralArgumentBuilder<CommandSourceStack> node = Commands.literal("edit").requires(GATE)
.executes((CommandContext<CommandSourceStack> context) -> {
fail(context.getSource(), message);
return 0;
});
for (String child : new String[]{"biome", "region", "dimension"}) {
node.then(Commands.literal(child)
.executes((CommandContext<CommandSourceStack> context) -> {
fail(context.getSource(), message);
return 0;
}));
}
return node;
}
private static int regen(CommandSourceStack source, int radius) {
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "This command can only be used by players.");
return 0;
}
ServerLevel level = source.getLevel();
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator irisGenerator)) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
ModdedRegen.start(source, level, irisGenerator, engine, player, radius);
return 1;
}
private static int pregenStart(CommandSourceStack source, int radius, int centerX, int centerZ) {
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
if (!ModdedPregenJob.start(level, engine, radius, centerX, centerZ)) {
fail(source, "A pregeneration task is already running. Stop it first with /iris pregen stop.");
return 0;
}
ok(source, "Pregen started in " + level.dimension().identifier() + " of " + (radius * 2) + " by " + (radius * 2)
+ " blocks from " + centerX + "," + centerZ + ". Progress logs to console; see /iris pregen status.");
return 1;
}
private static int pregenStop(CommandSourceStack source) {
if (ModdedPregenJob.stop()) {
ok(source, "Stopping pregeneration; finishing up the current region...");
return 1;
}
fail(source, "No active pregeneration task to stop.");
return 0;
}
private static int pregenPause(CommandSourceStack source) {
Boolean paused = ModdedPregenJob.pauseResume();
if (paused == null) {
fail(source, "No active pregeneration task to pause/resume.");
return 0;
}
ok(source, "Pregeneration is now " + (paused.booleanValue() ? "paused" : "running") + ".");
return 1;
}
private static int pregenStatus(CommandSourceStack source) {
String status = ModdedPregenJob.status();
if (status == null) {
fail(source, "No active pregeneration task.");
return 0;
}
ok(source, status);
return 1;
}
private static int version(CommandSourceStack source) {
ModdedLoader loader = ModdedEngineBootstrap.loader();
int engines = engineCount(source.getServer());
ok(source, "Iris " + loader.modVersion() + " by Volmit Software on " + loader.platformName()
+ " (Minecraft " + loader.minecraftVersion() + "), " + engines + " Iris dimension(s)");
return 1;
}
private static int info(CommandSourceStack source, String filter) {
MinecraftServer server = source.getServer();
List<String> lines = new ArrayList<>();
int total = 0;
int iris = 0;
for (ServerLevel level : server.getAllLevels()) {
total++;
ChunkGenerator generator = level.getChunkSource().getGenerator();
if (!(generator instanceof IrisModdedChunkGenerator irisGenerator)) {
continue;
}
iris++;
String dimensionId = level.dimension().identifier().toString();
if (filter != null && !dimensionId.contains(filter) && !irisGenerator.dimensionKey().contains(filter)) {
continue;
}
Engine engine = irisGenerator.engineIfBound();
if (engine == null) {
lines.add(dimensionId + ": pack=" + irisGenerator.dimensionKey() + " (engine not started yet)");
continue;
}
lines.add(dimensionId + ": pack=" + engine.getDimension().getLoadKey()
+ " seed=" + level.getSeed()
+ " height=" + engine.getMinHeight() + ".." + engine.getMaxHeight()
+ " generated=" + engine.getGenerated()
+ " data=" + engine.getData().getDataFolder().getAbsolutePath());
}
ok(source, "Loaded dimensions: " + total + " (" + iris + " Iris)");
if (lines.isEmpty()) {
ok(source, filter == null ? "No Iris dimensions are loaded." : "No Iris dimension matches '" + filter + "'.");
return 0;
}
for (String line : lines) {
ok(source, line);
}
return 1;
}
private static int what(CommandSourceStack source) {
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "This command can only be used by players.");
return 0;
}
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
BlockPos pos = player.blockPosition();
int relativeY = pos.getY() - engine.getMinHeight();
try {
IrisBiome biome = engine.getBiome(pos.getX(), relativeY, pos.getZ());
ok(source, "Biome: " + biome.getLoadKey() + " (" + biome.getName() + ")");
} catch (Throwable e) {
fail(source, "Biome lookup failed: " + e.getClass().getSimpleName());
}
try {
IrisRegion region = engine.getRegion(pos.getX(), pos.getZ());
ok(source, "Region: " + region.getLoadKey() + " (" + region.getName() + ")");
} catch (Throwable e) {
fail(source, "Region lookup failed: " + e.getClass().getSimpleName());
}
try {
IrisBiome cave = engine.getCaveBiome(pos.getX(), relativeY, pos.getZ());
ok(source, "Cave biome: " + (cave == null ? "none" : cave.getLoadKey()));
} catch (Throwable e) {
fail(source, "Cave biome lookup failed: " + e.getClass().getSimpleName());
}
int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, pos.getX(), pos.getZ());
BlockState surface = level.getBlockState(new BlockPos(pos.getX(), surfaceY - 1, pos.getZ()));
ok(source, "Surface block: " + BuiltInRegistries.BLOCK.getKey(surface.getBlock()) + " (y=" + (surfaceY - 1) + ")");
ok(source, "Position: " + pos.getX() + " " + pos.getY() + " " + pos.getZ() + " (chunk " + (pos.getX() >> 4) + "," + (pos.getZ() >> 4) + ")");
return 1;
}
private static int gotoBiome(CommandSourceStack source, String key) {
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "This command can only be used by players.");
return 0;
}
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
IrisBiome biome = engine.getData().getBiomeLoader().load(key.trim());
if (biome == null) {
fail(source, "Unknown biome: " + key);
return 0;
}
locate(source, level, engine, player, Locator.surfaceBiome(biome.getLoadKey()), "biome " + biome.getLoadKey());
return 1;
}
private static int gotoRegion(CommandSourceStack source, String key) {
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "This command can only be used by players.");
return 0;
}
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
IrisRegion region = engine.getData().getRegionLoader().load(key.trim());
if (region == null) {
fail(source, "Unknown region: " + key);
return 0;
}
if (!engine.getDimension().getRegions().contains(region.getLoadKey())) {
fail(source, region.getLoadKey() + " is not defined in the dimension!");
return 0;
}
locate(source, level, engine, player, Locator.region(region.getLoadKey()), "region " + region.getLoadKey());
return 1;
}
private static int gotoObject(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
String key = keyRaw.trim();
if (!engine.hasObjectPlacement(key)) {
fail(source, key + " is not configured in any region/biome object placements ("
+ engine.getData().getObjectLoader().getPossibleKeys().length + " object keys loaded).");
return 0;
}
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "This command can only be used by players. (object key '" + key + "' resolved against "
+ engine.getData().getObjectLoader().getPossibleKeys().length + " loaded object keys)");
return 0;
}
locate(source, level, engine, player, Locator.object(key), "object " + key);
return 1;
}
private static int gotoStructure(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
String key = keyRaw.trim();
Set<String> placed = IrisStructureLocator.placedKeys(engine);
if (!IrisStructureLocator.isPlaced(engine, key)) {
fail(source, "Structure " + key + " is not placed by this pack. Placed keys (" + placed.size() + "): "
+ String.join(", ", placed.stream().limit(8).toList()) + (placed.size() > 8 ? ", ..." : ""));
return 0;
}
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "This command can only be used by players. (structure key '" + key + "' resolved; "
+ placed.size() + " placed structure keys loaded)");
return 0;
}
MinecraftServer server = source.getServer();
int blockX = player.blockPosition().getX();
int blockZ = player.blockPosition().getZ();
ok(source, "Searching for structure " + key + "...");
Thread thread = new Thread(() -> {
try {
int[] at = IrisStructureLocator.locate(engine, key, blockX, blockZ, 1024);
if (at == null) {
server.execute(() -> fail(source, "Could not find structure " + key + " within 1024 chunks."));
return;
}
int targetX = at[0] + 8;
int targetY = at[1] + 2;
int targetZ = at[2] + 8;
server.execute(() -> {
player.teleportTo(level, targetX + 0.5D, targetY, targetZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
ok(source, "Teleported to structure " + key + " at " + targetX + " " + targetY + " " + targetZ);
});
} catch (Throwable e) {
LOGGER.error("Iris structure locate failed for {}", key, e);
server.execute(() -> fail(source, "Search failed: " + e.getClass().getSimpleName()));
}
}, "Iris Structure Locator");
thread.setDaemon(true);
thread.start();
return 1;
}
private static int gotoPoi(CommandSourceStack source, String typeRaw) {
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
String type = typeRaw.trim();
ServerPlayer player = source.getPlayer();
if (player == null) {
fail(source, "This command can only be used by players. (POI type '" + type + "' accepted)");
return 0;
}
locate(source, level, engine, player, Locator.poi(type), "POI " + type);
return 1;
}
private static void locate(CommandSourceStack source, ServerLevel level, Engine engine, ServerPlayer player, Locator<?> locator, String label) {
MinecraftServer server = source.getServer();
int chunkX = player.blockPosition().getX() >> 4;
int chunkZ = player.blockPosition().getZ() >> 4;
ok(source, "Searching for " + label + "...");
Thread thread = new Thread(() -> {
try {
Position2 at = locator.find(engine, new Position2(chunkX, chunkZ), LOCATE_TIMEOUT_MS, (Integer checks) -> {
}).get();
if (at == null) {
server.execute(() -> fail(source, "Could not find " + label + " within the search timeout."));
return;
}
int blockX = (at.getX() << 4) + 8;
int blockZ = (at.getZ() << 4) + 8;
int blockY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
server.execute(() -> {
player.teleportTo(level, blockX + 0.5D, blockY, blockZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
ok(source, "Teleported to " + label + " at " + blockX + " " + blockY + " " + blockZ);
});
} catch (WrongEngineBroException e) {
server.execute(() -> fail(source, "The engine for this world has been closed; rejoin the dimension and try again."));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
LOGGER.error("Iris locate failed for {}", label, e);
server.execute(() -> fail(source, "Search failed: " + e.getCause()));
}
}, "Iris Locator");
thread.setDaemon(true);
thread.start();
}
private static int seed(CommandSourceStack source) {
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
ok(source, "World seed: " + level.getSeed());
ok(source, "Engine seed: " + engine.getSeedManager().getSeed() + " (mixed: " + engine.getSeedManager().getFullMixedSeed() + ")");
return 1;
}
private static int goldenhash(CommandSourceStack source, int radius, int threads, ModdedGoldenHash.Mode mode) {
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
ModdedGoldenHash.start(source, level, engine, radius, threads, mode);
return 1;
}
private static int download(CommandSourceStack source, String pack, String branch) {
MinecraftServer server = source.getServer();
ok(source, "Downloading IrisDimensions/" + pack + " (branch " + branch + ")...");
Thread thread = new Thread(() -> {
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, branch,
(String message) -> server.execute(() -> ok(source, message)));
if (installed) {
server.execute(() -> ok(source, "Pack '" + pack + "' installed. Restart or create a new world to use it."));
} else {
server.execute(() -> fail(source, "Pack download failed for " + pack + "/" + branch + " (see console)."));
}
}, "Iris Pack Download");
thread.setDaemon(true);
thread.start();
return 1;
}
private static int metrics(CommandSourceStack source) {
ServerLevel level = source.getLevel();
Engine engine = engineFor(level);
if (engine == null) {
fail(source, "This dimension is not generated by Iris.");
return 0;
}
ok(source, "Generated: " + engine.getGenerated() + " chunk(s), " + String.format("%.1f", engine.getGeneratedPerSecond()) + "/s");
KMap<String, Double> pulled = engine.getMetrics().pull();
Map<String, Double> sorted = new TreeMap<>(pulled);
for (Map.Entry<String, Double> entry : sorted.entrySet()) {
if (entry.getValue() == null || entry.getValue() <= 0D) {
continue;
}
ok(source, " " + entry.getKey() + ": " + String.format("%.2f", entry.getValue()) + "ms");
}
return 1;
}
static Engine engineFor(ServerLevel level) {
ChunkGenerator generator = level.getChunkSource().getGenerator();
if (generator instanceof IrisModdedChunkGenerator irisGenerator) {
try {
return irisGenerator.commandEngine();
} catch (Throwable e) {
LOGGER.error("Iris engine lookup failed for {}", level.dimension().identifier(), e);
return null;
}
}
return null;
}
private static int engineCount(MinecraftServer server) {
int count = 0;
for (ServerLevel level : server.getAllLevels()) {
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) {
count++;
}
}
return count;
}
private static CompletableFuture<Suggestions> suggestBiomeKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
try {
Engine engine = engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getBiomeLoader().getPossibleKeys(), builder);
}
} catch (Throwable ignored) {
}
return builder.buildFuture();
}
private static CompletableFuture<Suggestions> suggestRegionKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
try {
Engine engine = engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getDimension().getRegions(), builder);
}
} catch (Throwable ignored) {
}
return builder.buildFuture();
}
private static CompletableFuture<Suggestions> suggestObjectKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
try {
Engine engine = engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder);
}
} catch (Throwable ignored) {
}
return builder.buildFuture();
}
private static CompletableFuture<Suggestions> suggestStructureKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
try {
Engine engine = engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(IrisStructureLocator.placedKeys(engine), builder);
}
} catch (Throwable ignored) {
}
return builder.buildFuture();
}
private static CompletableFuture<Suggestions> suggestPackNames(SuggestionsBuilder builder) {
List<String> names = new ArrayList<>();
names.add("overworld");
try {
File packs = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile();
File[] children = packs.listFiles();
if (children != null) {
for (File child : children) {
if (child.isDirectory() && !names.contains(child.getName())) {
names.add(child.getName());
}
}
}
} catch (Throwable ignored) {
}
return SharedSuggestionProvider.suggest(names, builder);
}
private static CompletableFuture<Suggestions> suggestDimensionNames(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
List<String> names = new ArrayList<>();
for (ServerLevel level : context.getSource().getServer().getAllLevels()) {
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator) {
names.add(level.dimension().identifier().toString());
}
}
return SharedSuggestionProvider.suggest(names, builder);
}
static void ok(CommandSourceStack source, String message) {
source.sendSuccess(() -> Component.literal(message), false);
}
static void fail(CommandSourceStack source, String message) {
source.sendFailure(Component.literal(message));
}
}
@@ -0,0 +1,261 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
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.framework.Engine;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.volmlib.util.collection.KList;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.LevelResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.function.Predicate;
public final class ModdedDatapackCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private static final String WORLD_PACK_NAME = "iris";
private ModdedDatapackCommands() {
}
public static LiteralArgumentBuilder<CommandSourceStack> tree() {
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("datapack").requires(GATE);
root.then(Commands.literal("status")
.executes((CommandContext<CommandSourceStack> context) -> status(context.getSource())));
root.then(Commands.literal("install")
.executes((CommandContext<CommandSourceStack> context) -> install(context.getSource())));
root.then(Commands.literal("list")
.executes((CommandContext<CommandSourceStack> 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("remove", "Datapack removal manages the Bukkit ingest manifest. On modded servers delete the datapack folder from world/datapacks and restart."));
return root;
}
private static LiteralArgumentBuilder<CommandSourceStack> message(String name, String text) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> {
IrisModdedCommands.fail(context.getSource(), text);
return 0;
})
.then(Commands.argument("args", StringArgumentType.greedyString())
.executes((CommandContext<CommandSourceStack> context) -> {
IrisModdedCommands.fail(context.getSource(), text);
return 0;
}));
}
private static File worldDatapacksFolder(MinecraftServer server) {
return server.getWorldPath(LevelResource.DATAPACK_DIR).toFile();
}
private static File overrideFile(MinecraftServer server, String dimensionKey) {
return new File(worldDatapacksFolder(server), WORLD_PACK_NAME + "/data/irisworldgen/dimension_type/" + IrisDimension.sanitizeDimensionTypeKeyValue(dimensionKey) + ".json");
}
private static int status(CommandSourceStack source) {
MinecraftServer server = source.getServer();
int irisLevels = 0;
int mismatches = 0;
for (ServerLevel level : server.getAllLevels()) {
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator irisGenerator)) {
continue;
}
irisLevels++;
String dimensionId = level.dimension().identifier().toString();
String typeKey = level.dimensionTypeRegistration().unwrapKey()
.map((net.minecraft.resources.ResourceKey<DimensionType> key) -> key.identifier().toString())
.orElse("inline");
DimensionType active = level.dimensionType();
int activeMin = active.minY();
int activeMax = active.minY() + active.height();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null || engine.getDimension() == null) {
IrisModdedCommands.ok(source, dimensionId + ": type=" + typeKey + " active=" + activeMin + ".." + activeMax
+ " logical=" + active.logicalHeight() + " (pack=" + irisGenerator.dimensionKey() + ", engine not started; pack heights unknown)");
continue;
}
IrisDimension dimension = engine.getDimension();
int packMin = dimension.getMinHeight();
int packMax = dimension.getMaxHeight();
int packLogical = dimension.getLogicalHeight();
boolean matches = packMin == activeMin && packMax == activeMax && packLogical == active.logicalHeight();
File override = overrideFile(server, irisGenerator.dimensionKey());
IrisModdedCommands.ok(source, dimensionId + ": type=" + typeKey
+ " active=" + activeMin + ".." + activeMax + " logical=" + active.logicalHeight()
+ " | pack '" + dimension.getLoadKey() + "' wants " + packMin + ".." + packMax + " logical=" + packLogical
+ " | " + (matches ? "MATCH" : "MISMATCH")
+ (override.isFile() ? " (world datapack override installed)" : ""));
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.");
}
}
if (irisLevels == 0) {
IrisModdedCommands.fail(source, "No Iris dimensions are loaded.");
return 0;
}
if (mismatches == 0) {
IrisModdedCommands.ok(source, "All " + irisLevels + " Iris dimension(s) match their pack height ranges.");
}
return 1;
}
private static int install(CommandSourceStack source) {
MinecraftServer server = source.getServer();
List<String> written = new ArrayList<>();
for (ServerLevel level : server.getAllLevels()) {
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator irisGenerator)) {
continue;
}
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null || engine.getDimension() == null) {
IrisModdedCommands.fail(source, level.dimension().identifier() + ": engine not started; cannot derive its dimension type yet.");
continue;
}
IrisDimension dimension = engine.getDimension();
String json;
try {
json = dimension.getDimensionType().toJson(DataVersion.getLatest().get());
} catch (Throwable e) {
LOGGER.error("Iris dimension type generation failed for {}", dimension.getLoadKey(), e);
IrisModdedCommands.fail(source, level.dimension().identifier() + ": dimension type generation failed: " + e.getMessage());
continue;
}
File output = overrideFile(server, irisGenerator.dimensionKey());
try {
output.getParentFile().mkdirs();
Files.writeString(output.toPath(), json, StandardCharsets.UTF_8);
written.add(output.getPath());
} catch (IOException e) {
LOGGER.error("Iris dimension type write failed for {}", output, e);
IrisModdedCommands.fail(source, "Failed to write " + output + ": " + e.getMessage());
}
}
if (written.isEmpty()) {
IrisModdedCommands.fail(source, "No Iris dimensions with running engines found; nothing was installed.");
return 0;
}
File mcmeta = new File(worldDatapacksFolder(server), WORLD_PACK_NAME + "/pack.mcmeta");
int packFormat = DataVersion.getLatest().getPackFormat();
String meta = "{\n"
+ " \"pack\": {\n"
+ " \"description\": \"Iris dimension types derived from the installed Iris packs.\",\n"
+ " \"pack_format\": " + packFormat + ",\n"
+ " \"min_format\": " + packFormat + ",\n"
+ " \"max_format\": " + packFormat + "\n"
+ " }\n"
+ "}\n";
try {
mcmeta.getParentFile().mkdirs();
Files.writeString(mcmeta.toPath(), meta, StandardCharsets.UTF_8);
written.add(mcmeta.getPath());
} catch (IOException e) {
LOGGER.error("Iris pack.mcmeta write failed for {}", mcmeta, e);
IrisModdedCommands.fail(source, "Failed to write " + mcmeta + ": " + e.getMessage());
return 0;
}
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).");
return 1;
}
private static int list(CommandSourceStack source) {
MinecraftServer server = source.getServer();
LinkedHashSet<String> configured = new LinkedHashSet<>();
File packsRoot = ModdedPackCommands.packsRoot();
File[] packs = packsRoot.isDirectory() ? packsRoot.listFiles(File::isDirectory) : null;
if (packs != null) {
for (File pack : packs) {
if (!new File(pack, "dimensions").isDirectory()) {
continue;
}
try {
IrisData data = IrisData.get(pack);
for (IrisDimension dimension : data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys())) {
if (dimension == null || dimension.getDatapackImports() == null) {
continue;
}
for (String url : dimension.getDatapackImports()) {
if (url != null && !url.isBlank()) {
configured.add(url.trim());
}
}
}
} catch (Throwable e) {
LOGGER.error("Iris datapack import scan failed for pack {}", pack.getName(), e);
}
}
}
IrisModdedCommands.ok(source, "Configured datapack imports: " + configured.size());
for (String url : configured) {
IrisModdedCommands.ok(source, " - " + url);
}
if (!configured.isEmpty()) {
IrisModdedCommands.ok(source, "Modrinth ingest is Bukkit-only; install these manually into world/datapacks if needed.");
}
File datapacks = worldDatapacksFolder(server);
File[] installed = datapacks.isDirectory() ? datapacks.listFiles(File::isDirectory) : null;
KList<String> names = new KList<>();
if (installed != null) {
for (File folder : installed) {
if (new File(folder, "pack.mcmeta").isFile()) {
names.add(folder.getName());
}
}
}
IrisModdedCommands.ok(source, "Installed world datapacks: " + names.size());
for (String name : names) {
IrisModdedCommands.ok(source, " - " + name);
}
return 1;
}
}
@@ -0,0 +1,183 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.modded.ModdedBlockState;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.DustParticleOptions;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
public final class ModdedDustRevealer {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final int MAX_HITS = 2048;
private static final DustParticleOptions REVEAL_DUST = new DustParticleOptions(0xFFD24A, 1.2F);
private ModdedDustRevealer() {
}
public static void reveal(ServerPlayer player, ServerLevel level, BlockPos pos) {
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
player.sendSystemMessage(Component.literal("This dimension is not generated by Iris."));
return;
}
describe(player, level, engine, pos);
int relativeY = pos.getY() - engine.getMinHeight();
String key = safe(() -> engine.getObjectPlacementKey(pos.getX(), relativeY, pos.getZ()));
if (key == null) {
return;
}
player.sendSystemMessage(Component.literal("Found object " + key));
MinecraftServer server = level.getServer();
BlockPos origin = pos.immutable();
Thread thread = new Thread(() -> {
List<BlockPos> hits = collect(engine, level, origin, key);
server.execute(() -> {
for (BlockPos hit : hits) {
level.sendParticles(player, REVEAL_DUST, true, true,
hit.getX() + 0.5D, hit.getY() + 0.5D, hit.getZ() + 0.5D,
3, 0.25D, 0.25D, 0.25D, 0.0D);
}
player.sendSystemMessage(Component.literal("Revealed " + hits.size() + " block(s) of " + key
+ (hits.size() >= MAX_HITS ? " (capped)" : "")));
});
}, "Iris Dust Revealer");
thread.setDaemon(true);
thread.start();
}
private static List<BlockPos> collect(Engine engine, ServerLevel level, BlockPos origin, String key) {
List<BlockPos> hits = new ArrayList<>();
Set<BlockPos> visited = new HashSet<>();
Deque<BlockPos> frontier = new ArrayDeque<>();
frontier.add(origin);
visited.add(origin);
int minY = level.getMinY();
int maxY = minY + level.getHeight();
try {
while (!frontier.isEmpty() && hits.size() < MAX_HITS) {
BlockPos current = frontier.poll();
hits.add(current);
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
for (int dz = -1; dz <= 1; dz++) {
if (dx == 0 && dy == 0 && dz == 0) {
continue;
}
BlockPos next = current.offset(dx, dy, dz);
if (next.getY() < minY || next.getY() >= maxY || visited.contains(next)) {
continue;
}
visited.add(next);
String nextKey = engine.getObjectPlacementKey(next.getX(), next.getY() - engine.getMinHeight(), next.getZ());
if (key.equals(nextKey)) {
frontier.add(next);
}
}
}
}
}
} catch (Throwable e) {
LOGGER.error("Iris dust reveal BFS failed for {}", key, e);
}
return hits;
}
private static void describe(ServerPlayer player, ServerLevel level, Engine engine, BlockPos pos) {
int x = pos.getX();
int y = pos.getY();
int z = pos.getZ();
int minHeight = engine.getMinHeight();
int relativeY = y - minHeight;
int surfaceRelative = safeInt(() -> engine.getHeight(x, z, true));
int surfaceY = surfaceRelative + minHeight;
int offset = y - surfaceY;
String objectKey = safe(() -> engine.getObjectPlacementKey(x, relativeY, z));
IrisBiome surfaceBiome = safe(() -> engine.getSurfaceBiome(x, z));
IrisBiome biomeHere = safe(() -> engine.getBiome(x, relativeY, z));
IrisBiome caveBiome = safe(() -> engine.getCaveOrMantleBiome(x, relativeY, z));
IrisRegion region = safe(() -> engine.getRegion(x, z));
List<String> lines = new ArrayList<>();
lines.add("--- Iris Dust @ " + x + ", " + y + ", " + z + " ---");
lines.add("Block: " + ModdedBlockState.serialize(level.getBlockState(pos)));
if (offset > 0) {
lines.add("Position: +" + offset + " ABOVE surface (surface Y=" + surfaceY + ")");
} else if (offset < 0) {
lines.add("Position: " + (-offset) + " below surface (surface Y=" + surfaceY + ")");
} else {
lines.add("Position: at surface (Y=" + surfaceY + ")");
}
lines.add("Object @block: " + (objectKey == null ? "none" : objectKey));
if (surfaceBiome != null) {
lines.add("Surface biome: " + surfaceBiome.getLoadKey());
}
if (biomeHere != null && (surfaceBiome == null || !biomeHere.getLoadKey().equals(surfaceBiome.getLoadKey()))) {
lines.add("Biome @Y: " + biomeHere.getLoadKey());
}
if (caveBiome != null && (surfaceBiome == null || !caveBiome.getLoadKey().equals(surfaceBiome.getLoadKey()))) {
lines.add("Cave/Mantle biome: " + caveBiome.getLoadKey());
}
if (region != null) {
lines.add("Region: " + region.getLoadKey() + " (" + region.getName() + ")");
}
Set<String> objects = safe(() -> engine.getObjectsAt(x >> 4, z >> 4));
if (objects != null && !objects.isEmpty()) {
lines.add("Objects in chunk: " + objects);
}
for (String line : lines) {
player.sendSystemMessage(Component.literal(line));
}
}
private static <T> T safe(Supplier<T> supplier) {
try {
return supplier.get();
} catch (Throwable e) {
return null;
}
}
private static int safeInt(Supplier<Integer> supplier) {
try {
Integer value = supplier.get();
return value == null ? 0 : value;
} catch (Throwable e) {
return 0;
}
}
}
@@ -0,0 +1,400 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.modded.ModdedBlockBuffer;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
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;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public final class ModdedGoldenHash {
public enum Mode {
AUTO,
CAPTURE,
VERIFY
}
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String FORMAT = "iris-goldenhash v1";
private static final int BIOME_STEP = 4;
private static final int MAX_REPORTED_MISMATCHES = 10;
private static final AtomicBoolean ACTIVE = new AtomicBoolean(false);
private final CommandSourceStack source;
private final MinecraftServer server;
private final ServerLevel level;
private final Engine engine;
private final int radius;
private final int threads;
private final Mode mode;
private final File goldenFile;
private ModdedGoldenHash(CommandSourceStack source, ServerLevel level, Engine engine, int radius, int threads, Mode mode) {
this.source = source;
this.server = source.getServer();
this.level = level;
this.engine = engine;
this.radius = Math.max(0, radius);
this.threads = Math.max(1, threads);
this.mode = mode;
File goldenDir = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("golden").toFile();
goldenDir.mkdirs();
this.goldenFile = new File(goldenDir, engine.getDimension().getLoadKey()
+ "-s" + level.getSeed()
+ "-c0x0-r" + this.radius + ".hashes");
}
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."));
return;
}
ModdedGoldenHash scan = new ModdedGoldenHash(source, level, engine, radius, threads, mode);
int chunks = (scan.radius * 2 + 1) * (scan.radius * 2 + 1);
scan.ok("GoldenHash started: " + chunks + " chunk(s) around 0,0 in buffers (world untouched), threads=" + scan.threads + " mode=" + mode);
LOGGER.info("goldenhash start: dim={} seed={} radius={} threads={} mode={} file={}",
engine.getDimension().getLoadKey(), level.getSeed(), scan.radius, scan.threads, mode, scan.goldenFile.getName());
Thread thread = new Thread(() -> {
try {
scan.run();
} catch (Throwable e) {
LOGGER.error("goldenhash failed", e);
scan.fail("GoldenHash failed: " + e);
} finally {
ACTIVE.set(false);
}
}, "Iris GoldenHash");
thread.setDaemon(true);
thread.start();
}
private void run() throws Exception {
boolean exists = goldenFile.exists();
if (mode == Mode.VERIFY && !exists) {
fail("No golden capture at " + goldenFile.getAbsolutePath() + "; run '/iris goldenhash " + radius + " " + threads + " capture' first.");
return;
}
resetMantleFull();
List<int[]> targets = orderedTargets(0, 0, radius);
Map<Long, String> lines = scan(targets);
if (lines.size() != targets.size()) {
fail("GoldenHash aborted: " + (targets.size() - lines.size()) + " chunk(s) failed to generate. No golden file written.");
return;
}
if (mode == Mode.CAPTURE || (mode == Mode.AUTO && !exists)) {
capture(lines);
} else {
verify(lines);
}
}
private void resetMantleFull() {
try {
engine.getMantle().getMantle().saveAll();
File folder = engine.getMantle().getMantle().getDataFolder();
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
file.delete();
}
}
}
ok("Mantle reset (" + folder.getAbsolutePath() + ")");
} catch (Throwable e) {
LOGGER.error("goldenhash mantle reset failed", e);
ok("Mantle reset failed (" + e.getClass().getSimpleName() + "); continuing with existing mantle state.");
}
}
private Map<Long, String> scan(List<int[]> targets) throws InterruptedException {
Map<Long, String> lines = new ConcurrentHashMap<>();
Semaphore inFlight = new Semaphore(threads);
CountDownLatch done = new CountDownLatch(targets.size());
AtomicInteger completed = new AtomicInteger();
int total = targets.size();
int stride = total <= 64 ? 1 : 32;
int height = engine.getMaxHeight() - engine.getMinHeight();
PlatformBlockState air = IrisPlatforms.get().registries().air();
for (int[] target : targets) {
int chunkX = target[0];
int chunkZ = target[1];
inFlight.acquire();
MultiBurst.burst.lazy(() -> {
try {
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
engine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false);
lines.put(chunkKey(chunkX, chunkZ), hashChunk(chunkX, chunkZ, blocks, biomes, height));
int doneCount = completed.incrementAndGet();
if (doneCount % stride == 0 || doneCount == total) {
ok("[" + doneCount + "/" + total + "] chunk " + chunkX + "," + chunkZ + " hashed");
}
} catch (Throwable e) {
LOGGER.error("goldenhash chunk {},{} failed", chunkX, chunkZ, e);
fail("Chunk " + chunkX + "," + chunkZ + " FAILED: " + e.getClass().getSimpleName());
} finally {
inFlight.release();
done.countDown();
}
});
}
done.await();
return lines;
}
private String hashChunk(int chunkX, int chunkZ, ModdedBlockBuffer blocks, Hunk<PlatformBiome> biomes, int height) {
MessageDigest blockDigest = sha256();
MessageDigest biomeDigest = sha256();
Map<PlatformBlockState, byte[]> blockCache = new HashMap<>();
Map<PlatformBiome, byte[]> biomeCache = new HashMap<>();
byte[] plains = "minecraft:plains\n".getBytes(StandardCharsets.UTF_8);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < height; y++) {
PlatformBlockState state = blocks.get(x, y, z);
byte[] bytes = blockCache.computeIfAbsent(state, (PlatformBlockState s) -> (s.key() + "\n").getBytes(StandardCharsets.UTF_8));
blockDigest.update(bytes);
}
}
}
for (int x = 0; x < 16; x += BIOME_STEP) {
for (int z = 0; z < 16; z += BIOME_STEP) {
for (int y = 0; y < height; y += BIOME_STEP) {
PlatformBiome biome = biomes.get(x, y, z);
byte[] bytes = biome == null
? plains
: biomeCache.computeIfAbsent(biome, (PlatformBiome b) -> (b.key() + "\n").getBytes(StandardCharsets.UTF_8));
biomeDigest.update(bytes);
}
}
}
return chunkX + " " + chunkZ + " "
+ HexFormat.of().formatHex(blockDigest.digest()) + " "
+ HexFormat.of().formatHex(biomeDigest.digest());
}
private void capture(Map<Long, String> lines) throws IOException {
List<String> body = orderedBody(lines);
String combined = combinedHash(body);
List<String> out = new ArrayList<>();
out.add("#" + FORMAT);
out.add("#world=" + engine.getWorld().name());
out.add("#dim=" + engine.getDimension().getLoadKey());
out.add("#seed=" + level.getSeed());
out.add("#mc=" + ModdedEngineBootstrap.loader().minecraftVersion());
out.add("#minY=" + engine.getMinHeight() + " maxY=" + engine.getMaxHeight());
out.add("#center=0,0");
out.add("#radius=" + radius);
out.addAll(body);
out.add("#combined=" + combined);
Files.write(goldenFile.toPath(), out, StandardCharsets.UTF_8);
ok("Golden captured: " + body.size() + " chunks combined=" + shortHash(combined));
ok(goldenFile.getAbsolutePath());
LOGGER.info("goldenhash captured: {} combined={}", goldenFile.getAbsolutePath(), combined);
}
private void verify(Map<Long, String> lines) throws IOException {
List<String> existing = Files.readAllLines(goldenFile.toPath(), StandardCharsets.UTF_8);
Map<String, String> meta = new HashMap<>();
Map<String, String> goldenChunks = new HashMap<>();
for (String line : existing) {
if (line.startsWith("#")) {
int eq = line.indexOf('=');
if (eq > 0) {
meta.put(line.substring(1, eq), line.substring(eq + 1));
}
} else if (!line.isBlank()) {
int second = line.indexOf(' ', line.indexOf(' ') + 1);
goldenChunks.put(line.substring(0, second), line);
}
}
String expectedSeed = String.valueOf(level.getSeed());
String expectedDim = engine.getDimension().getLoadKey();
if (!expectedSeed.equals(meta.get("seed")) || !expectedDim.equals(meta.get("dim"))) {
fail("Golden file is for dim=" + meta.get("dim") + " seed=" + meta.get("seed")
+ " but this world is dim=" + expectedDim + " seed=" + expectedSeed + ". Aborting.");
return;
}
String mc = ModdedEngineBootstrap.loader().minecraftVersion();
if (!mc.equals(meta.get("mc"))) {
ok("Golden was captured on mc=" + meta.get("mc") + ", running mc=" + mc + ". Diffs may be version-induced.");
}
List<String> body = orderedBody(lines);
List<String> mismatches = new ArrayList<>();
for (String line : body) {
int second = line.indexOf(' ', line.indexOf(' ') + 1);
String key = line.substring(0, second);
String golden = goldenChunks.get(key);
if (!line.equals(golden)) {
mismatches.add(key + (golden == null ? " (missing in golden)" : ""));
}
}
String combined = combinedHash(body);
if (mismatches.isEmpty()) {
ok("GOLDEN MATCH: " + body.size() + "/" + goldenChunks.size() + " chunks, combined=" + shortHash(combined));
LOGGER.info("goldenhash MATCH: {} combined={}", goldenFile.getName(), combined);
return;
}
fail("GOLDEN MISMATCH: " + mismatches.size() + "/" + body.size() + " chunks differ.");
for (int i = 0; i < Math.min(MAX_REPORTED_MISMATCHES, mismatches.size()); i++) {
fail(" chunk " + mismatches.get(i));
}
if (mismatches.size() > MAX_REPORTED_MISMATCHES) {
fail(" ... and " + (mismatches.size() - MAX_REPORTED_MISMATCHES) + " more");
}
File current = new File(goldenFile.getParentFile(), goldenFile.getName() + ".new");
List<String> out = new ArrayList<>(body);
out.add("#combined=" + combined);
Files.write(current.toPath(), out, StandardCharsets.UTF_8);
ok("Current hashes written to " + current.getName());
LOGGER.info("goldenhash MISMATCH: {}/{} -> {}", mismatches.size(), body.size(), current.getAbsolutePath());
diagnose(mismatches.getFirst());
}
private void diagnose(String mismatchKey) {
try {
String[] parts = mismatchKey.trim().split(" ");
int chunkX = Integer.parseInt(parts[0]);
int chunkZ = Integer.parseInt(parts[1]);
int height = engine.getMaxHeight() - engine.getMinHeight();
PlatformBlockState air = IrisPlatforms.get().registries().air();
ModdedBlockBuffer first = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> firstBiomes = Hunk.newArrayHunk(16, height, 16);
engine.generate(chunkX << 4, chunkZ << 4, first, firstBiomes, false);
ModdedBlockBuffer second = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> secondBiomes = Hunk.newArrayHunk(16, height, 16);
engine.generate(chunkX << 4, chunkZ << 4, second, secondBiomes, false);
int diffs = 0;
for (int x = 0; x < 16 && diffs < 50; x++) {
for (int z = 0; z < 16 && diffs < 50; z++) {
for (int y = 0; y < height && diffs < 50; y++) {
if (!first.get(x, y, z).key().equals(second.get(x, y, z).key())) {
diffs++;
}
}
}
}
if (diffs == 0) {
ok("Repeat-gen STABLE for chunk " + chunkX + "," + chunkZ + " (nondeterminism is order/state-dependent, not per-call)");
} else {
fail("Repeat-gen UNSTABLE for chunk " + chunkX + "," + chunkZ + " (" + diffs + "+ block diffs between back-to-back generations)");
}
} catch (Throwable e) {
LOGGER.error("goldenhash diagnosis failed", e);
fail("Diagnosis failed: " + e.getMessage());
}
}
private static List<int[]> orderedTargets(int centerX, int centerZ, int radius) {
List<int[]> targets = new ArrayList<>();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
targets.add(new int[]{centerX + dx, centerZ + dz});
}
}
targets.sort(Comparator.comparingInt((int[] t) -> {
int ox = t[0] - centerX;
int oz = t[1] - centerZ;
return ox * ox + oz * oz;
}));
return targets;
}
private List<String> orderedBody(Map<Long, String> lines) {
Map<Long, String> sorted = new TreeMap<>(lines);
return new ArrayList<>(sorted.values());
}
private String combinedHash(List<String> body) {
MessageDigest digest = sha256();
for (String line : body) {
digest.update((line + "\n").getBytes(StandardCharsets.UTF_8));
}
return HexFormat.of().formatHex(digest.digest());
}
private void ok(String message) {
server.execute(() -> source.sendSuccess(() -> Component.literal(message), false));
}
private void fail(String message) {
server.execute(() -> source.sendFailure(Component.literal(message)));
}
private static String shortHash(String hex) {
return hex.substring(0, 12);
}
private static long chunkKey(int x, int z) {
return (((long) x) << 32) ^ (z & 0xFFFFFFFFL);
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
}
@@ -0,0 +1,610 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisObject;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.volmlib.util.math.RNG;
import com.mojang.brigadier.arguments.IntegerArgumentType;
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.SuggestionsBuilder;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.SharedSuggestionProvider;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.function.Predicate;
public final class ModdedObjectCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private static final long MAX_SAVE_VOLUME = 500000L;
private static final long MAX_AUTOSELECT_VOLUME = 100000L;
private static final double TARGET_RANGE = 256.0D;
private static final SuggestionProvider<CommandSourceStack> OBJECT_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> {
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null) {
return SharedSuggestionProvider.suggest(engine.getData().getObjectLoader().getPossibleKeys(), builder);
}
} catch (Throwable ignored) {
}
return builder.buildFuture();
};
private static final SuggestionProvider<CommandSourceStack> ROTATIONS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) ->
SharedSuggestionProvider.suggest(List.of("0", "90", "180", "270"), builder);
private ModdedObjectCommands() {
}
private enum ResizeOp {
EXPAND,
CONTRACT,
SHIFT
}
public static LiteralArgumentBuilder<CommandSourceStack> tree() {
ModdedObjectUndo.init();
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("object").requires(GATE);
root.then(Commands.literal("wand")
.executes((CommandContext<CommandSourceStack> context) -> giveWand(context.getSource())));
root.then(Commands.literal("dust")
.executes((CommandContext<CommandSourceStack> context) -> giveDust(context.getSource())));
root.then(Commands.literal("save")
.then(Commands.literal("overwrite")
.then(Commands.argument("name", StringArgumentType.greedyString())
.executes((CommandContext<CommandSourceStack> context) -> save(context.getSource(), StringArgumentType.getString(context, "name"), true))))
.then(Commands.argument("name", StringArgumentType.greedyString())
.executes((CommandContext<CommandSourceStack> context) -> save(context.getSource(), StringArgumentType.getString(context, "name"), false))));
root.then(pasteTree());
root.then(resizeTree("expand", ResizeOp.EXPAND));
root.then(resizeTree("contract", ResizeOp.CONTRACT));
root.then(resizeTree("shift", ResizeOp.SHIFT));
root.then(Commands.literal("xpy")
.executes((CommandContext<CommandSourceStack> context) -> autoSelect(context.getSource(), false)));
root.then(Commands.literal("x+y")
.executes((CommandContext<CommandSourceStack> context) -> autoSelect(context.getSource(), false)));
root.then(Commands.literal("xay")
.executes((CommandContext<CommandSourceStack> context) -> autoSelect(context.getSource(), true)));
root.then(Commands.literal("x&y")
.executes((CommandContext<CommandSourceStack> context) -> autoSelect(context.getSource(), true)));
root.then(positionTree("position1", true));
root.then(positionTree("position2", false));
root.then(Commands.literal("analyze")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> analyze(context.getSource(), StringArgumentType.getString(context, "key")))));
root.then(Commands.literal("shrink")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> shrink(context.getSource(), StringArgumentType.getString(context, "key")))));
root.then(Commands.literal("undo")
.executes((CommandContext<CommandSourceStack> context) -> undo(context.getSource(), 1))
.then(Commands.argument("amount", IntegerArgumentType.integer(1, 32))
.executes((CommandContext<CommandSourceStack> 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."));
root.then(bukkitOnly("convert", "Schematic conversion (.schem -> .iob) requires the Bukkit plugin."));
root.then(bukkitOnly("plausibilize", "Tree plausibilization requires the Bukkit plugin (its block-data tooling is not ported to modded yet)."));
return root;
}
private static LiteralArgumentBuilder<CommandSourceStack> pasteTree() {
LiteralArgumentBuilder<CommandSourceStack> paste = Commands.literal("paste");
paste.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> paste(context.getSource(), StringArgumentType.getString(context, "key"), 0, null)));
paste.then(Commands.literal("rotate")
.then(Commands.argument("degrees", IntegerArgumentType.integer(-270, 270)).suggests(ROTATIONS)
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> paste(context.getSource(), StringArgumentType.getString(context, "key"),
IntegerArgumentType.getInteger(context, "degrees"), null)))));
paste.then(Commands.literal("at")
.then(Commands.argument("x", IntegerArgumentType.integer())
.then(Commands.argument("y", IntegerArgumentType.integer())
.then(Commands.argument("z", IntegerArgumentType.integer())
.then(Commands.literal("rotate")
.then(Commands.argument("degrees", IntegerArgumentType.integer(-270, 270)).suggests(ROTATIONS)
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> paste(context.getSource(), StringArgumentType.getString(context, "key"),
IntegerArgumentType.getInteger(context, "degrees"),
new BlockPos(IntegerArgumentType.getInteger(context, "x"), IntegerArgumentType.getInteger(context, "y"), IntegerArgumentType.getInteger(context, "z")))))))
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(OBJECT_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> paste(context.getSource(), StringArgumentType.getString(context, "key"), 0,
new BlockPos(IntegerArgumentType.getInteger(context, "x"), IntegerArgumentType.getInteger(context, "y"), IntegerArgumentType.getInteger(context, "z")))))))));
return paste;
}
private static LiteralArgumentBuilder<CommandSourceStack> resizeTree(String name, ResizeOp op) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> resize(context.getSource(), 1, op))
.then(Commands.argument("amount", IntegerArgumentType.integer(1, 256))
.executes((CommandContext<CommandSourceStack> context) -> resize(context.getSource(), IntegerArgumentType.getInteger(context, "amount"), op)));
}
private static LiteralArgumentBuilder<CommandSourceStack> positionTree(String name, boolean first) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> position(context.getSource(), first, false))
.then(Commands.literal("look")
.executes((CommandContext<CommandSourceStack> context) -> position(context.getSource(), first, true)));
}
private static LiteralArgumentBuilder<CommandSourceStack> bukkitOnly(String name, String message) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> {
IrisModdedCommands.fail(context.getSource(), message);
return 0;
});
}
public static int giveWand(CommandSourceStack source) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by players. (the wand is a physical item)");
return 0;
}
if (!player.getInventory().add(ModdedWandService.createWand())) {
IrisModdedCommands.fail(source, "Your inventory is full.");
return 0;
}
IrisModdedCommands.ok(source, "Poof! Good luck building! (left click = corner 1, right click = corner 2)");
return 1;
}
private static int giveDust(CommandSourceStack source) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by players. (the dust is a physical item)");
return 0;
}
if (!player.getInventory().add(ModdedWandService.createDust())) {
IrisModdedCommands.fail(source, "Your inventory is full.");
return 0;
}
IrisModdedCommands.ok(source, "Right click a block to reveal the object it belongs to.");
return 1;
}
private static int save(CommandSourceStack source, String nameRaw, boolean overwrite) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by players. (saving captures your wand selection)");
return 0;
}
ServerLevel level = player.level();
Engine engine = IrisModdedCommands.engineFor(level);
if (engine == null) {
IrisModdedCommands.fail(source, "This dimension is not generated by Iris; objects save into the dimension's pack.");
return 0;
}
if (!ModdedWandService.isHoldingWand(player)) {
IrisModdedCommands.fail(source, "Hold your Iris wand. (/iris wand)");
return 0;
}
ModdedWandService.Selection selection = ModdedWandService.selection(player);
if (selection == null) {
IrisModdedCommands.fail(source, "No area selected. Left/right click blocks with the wand first.");
return 0;
}
String name = nameRaw.trim().replace('\\', '/');
if (name.isEmpty() || name.contains("..")) {
IrisModdedCommands.fail(source, "Invalid object name: " + nameRaw);
return 0;
}
BlockPos min = selection.min();
BlockPos max = selection.max();
int w = max.getX() - min.getX() + 1;
int h = max.getY() - min.getY() + 1;
int d = max.getZ() - min.getZ() + 1;
long volume = (long) w * h * d;
if (volume > MAX_SAVE_VOLUME) {
IrisModdedCommands.fail(source, "Selection too large: " + volume + " blocks (max " + MAX_SAVE_VOLUME + ").");
return 0;
}
File file = new File(engine.getData().getDataFolder(), "objects" + File.separator + name.replace('/', File.separatorChar) + ".iob");
if (file.exists() && !overwrite) {
IrisModdedCommands.fail(source, "File already exists. Use /iris object save overwrite " + name);
return 0;
}
int[] tilesSkipped = {0};
IrisObject object = capture(level, min, max, w, h, d, tilesSkipped);
File parent = file.getParentFile();
if (parent != null) {
parent.mkdirs();
}
try {
object.write(file);
} catch (IOException e) {
LOGGER.error("Iris object save failed for {}", file.getAbsolutePath(), e);
IrisModdedCommands.fail(source, "Failed to save object: " + e.getMessage());
return 0;
}
String tileNote = tilesSkipped[0] > 0 ? " (" + tilesSkipped[0] + " tile state(s) not captured; tile capture is Bukkit-only for now)" : "";
IrisModdedCommands.ok(source, "Saved " + engine.getData().getDataFolder().getName() + "/objects/" + name + ".iob: "
+ w + "x" + h + "x" + d + ", " + object.getBlocks().size() + " block(s)" + tileNote);
LOGGER.info("Iris object save: {} {}x{}x{} blocks={} tilesSkipped={} -> {}", name, w, h, d, object.getBlocks().size(), tilesSkipped[0], file.getAbsolutePath());
return 1;
}
private static IrisObject capture(ServerLevel level, BlockPos min, BlockPos max, int w, int h, int d, int[] tilesSkipped) {
IrisObject object = new IrisObject(w, h, d);
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
for (int x = min.getX(); x <= max.getX(); x++) {
for (int y = min.getY(); y <= max.getY(); y++) {
for (int z = min.getZ(); z <= max.getZ(); z++) {
BlockState state = level.getBlockState(cursor.set(x, y, z));
if (state.is(Blocks.AIR)) {
continue;
}
if (state.hasBlockEntity()) {
tilesSkipped[0]++;
}
object.setUnsigned(x - min.getX(), y - min.getY(), z - min.getZ(), ModdedBlockState.of(state, null));
}
}
}
return object;
}
private static int paste(CommandSourceStack source, String keyRaw, int rotation, BlockPos at) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
String key = keyRaw.trim();
IrisObject object = null;
try {
object = IrisData.loadAnyObject(key, engine == null ? null : engine.getData());
} catch (Throwable e) {
LOGGER.error("Iris object load failed for {}", key, e);
}
if (object == null || object.getBlocks().size() == 0) {
IrisModdedCommands.fail(source, "Unknown or empty object: " + key);
return 0;
}
ServerPlayer player = source.getPlayer();
BlockPos target = at;
if (target == null) {
if (player == null) {
IrisModdedCommands.fail(source, "Console must specify coordinates: /iris object paste at <x> <y> <z> [rotate <degrees>] " + key);
return 0;
}
HitResult hit = player.pick(TARGET_RANGE, 1.0F, false);
if (hit.getType() != HitResult.Type.BLOCK || !(hit instanceof BlockHitResult blockHit)) {
IrisModdedCommands.fail(source, "You are not looking at a block within " + (int) TARGET_RANGE + " blocks.");
return 0;
}
target = blockHit.getBlockPos().above();
}
IrisObjectPlacement placement = new IrisObjectPlacement();
placement.setRotation(IrisObjectRotation.of(0, rotation, 0));
ModdedObjectPlacer placer = new ModdedObjectPlacer(level);
try {
object.place(target.getX(), target.getY() + object.getCenter().getY(), target.getZ(), placer, placement, new RNG(), null);
} catch (Throwable e) {
LOGGER.error("Iris paste failed for {}", key, e);
ModdedObjectUndo.record(player == null ? ModdedObjectUndo.CONSOLE : player.getUUID(), level, placer.undoSnapshot());
IrisModdedCommands.fail(source, "Paste failed: " + e.getClass().getSimpleName() + " (partial changes recorded for undo)");
return 0;
}
UUID owner = player == null ? ModdedObjectUndo.CONSOLE : player.getUUID();
ModdedObjectUndo.record(owner, level, placer.undoSnapshot());
String tileNote = placer.skippedTiles() > 0 ? ", " + placer.skippedTiles() + " tile state(s) skipped" : "";
IrisModdedCommands.ok(source, "Placed " + key + " at " + target.getX() + " " + target.getY() + " " + target.getZ()
+ " rot=" + rotation + " (" + placer.writes() + " write(s), " + placer.nonAirWrites() + " non-air" + tileNote + ")");
LOGGER.info("Iris paste: {} at {},{},{} rot={} writes={} nonAir={} tilesSkipped={}",
key, target.getX(), target.getY(), target.getZ(), rotation, placer.writes(), placer.nonAirWrites(), placer.skippedTiles());
return placer.writes() > 0 ? 1 : 0;
}
private static int resize(CommandSourceStack source, int amount, ResizeOp op) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by players.");
return 0;
}
if (!ModdedWandService.isHoldingWand(player)) {
IrisModdedCommands.fail(source, "Hold your Iris wand. (/iris wand)");
return 0;
}
ModdedWandService.Selection selection = ModdedWandService.selection(player);
if (selection == null) {
IrisModdedCommands.fail(source, "No area selected.");
return 0;
}
Direction direction = Direction.getApproximateNearest(player.getLookAngle());
int[] mins = {selection.min().getX(), selection.min().getY(), selection.min().getZ()};
int[] maxs = {selection.max().getX(), selection.max().getY(), selection.max().getZ()};
int axis = switch (direction.getAxis()) {
case X -> 0;
case Y -> 1;
case Z -> 2;
};
int step = direction.getAxisDirection().getStep();
switch (op) {
case EXPAND -> {
if (step > 0) {
maxs[axis] += amount;
} else {
mins[axis] -= amount;
}
}
case CONTRACT -> {
if (step > 0) {
maxs[axis] = Math.max(mins[axis], maxs[axis] - amount);
} else {
mins[axis] = Math.min(maxs[axis], mins[axis] + amount);
}
}
case SHIFT -> {
mins[axis] += step * amount;
maxs[axis] += step * amount;
}
}
BlockPos first = new BlockPos(mins[0], mins[1], mins[2]);
BlockPos second = new BlockPos(maxs[0], maxs[1], maxs[2]);
ModdedWandService.setSelection(player, first, second);
IrisModdedCommands.ok(source, op.name().toLowerCase(Locale.ROOT) + " " + amount + " " + direction.getName()
+ ": " + describe(first, second));
return 1;
}
private static int position(CommandSourceStack source, boolean first, boolean look) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by players.");
return 0;
}
if (!ModdedWandService.isHoldingWand(player)) {
IrisModdedCommands.fail(source, "Ready your wand. (/iris wand)");
return 0;
}
BlockPos pos;
if (look) {
HitResult hit = player.pick(TARGET_RANGE, 1.0F, false);
if (hit.getType() != HitResult.Type.BLOCK || !(hit instanceof BlockHitResult blockHit)) {
IrisModdedCommands.fail(source, "You are not looking at a block.");
return 0;
}
pos = blockHit.getBlockPos();
} else {
pos = player.blockPosition().below();
}
ModdedWandService.Selection selection = ModdedWandService.selection(player);
BlockPos other = selection == null ? null : (first ? selection.second() : selection.first());
BlockPos fallback = other == null ? pos : other;
if (first) {
ModdedWandService.setSelection(player, pos, fallback);
} else {
ModdedWandService.setSelection(player, fallback, pos);
}
IrisModdedCommands.ok(source, "Position " + (first ? 1 : 2) + " set to " + pos.getX() + " " + pos.getY() + " " + pos.getZ());
return 1;
}
private static int autoSelect(CommandSourceStack source, boolean down) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by players.");
return 0;
}
if (!ModdedWandService.isHoldingWand(player)) {
IrisModdedCommands.fail(source, "Hold your wand!");
return 0;
}
ModdedWandService.Selection selection = ModdedWandService.selection(player);
if (selection == null) {
IrisModdedCommands.fail(source, "No area selected.");
return 0;
}
ServerLevel level = player.level();
BlockPos min = selection.min();
BlockPos max = selection.max();
long volume = (long) (max.getX() - min.getX() + 1) * (max.getY() - min.getY() + 1) * (max.getZ() - min.getZ() + 1);
if (volume > MAX_AUTOSELECT_VOLUME) {
IrisModdedCommands.fail(source, "Selection too large for auto-select: " + volume + " blocks (max " + MAX_AUTOSELECT_VOLUME + ").");
return 0;
}
int levelMinY = level.getMinY();
int levelMaxY = levelMinY + level.getHeight() - 1;
int topMinY = min.getY();
int topMaxY = max.getY();
while (topMaxY < levelMaxY && !boxOnlyAir(level, min.getX(), topMinY, min.getZ(), max.getX(), topMaxY, max.getZ())) {
topMinY++;
topMaxY++;
}
topMaxY--;
int bottomY = min.getY();
if (down) {
int lowMinY = min.getY();
int lowMaxY = max.getY();
while (lowMinY > levelMinY && !boxOnlyAir(level, min.getX(), lowMinY, min.getZ(), max.getX(), lowMaxY, max.getZ())) {
lowMinY--;
lowMaxY--;
}
bottomY = lowMinY + 1;
}
int minX = min.getX();
int maxX = max.getX();
int minZ = min.getZ();
int maxZ = max.getZ();
while (minX < maxX && boxOnlyAir(level, minX, bottomY, minZ, minX, topMaxY, maxZ)) {
minX++;
}
while (maxX > minX && boxOnlyAir(level, maxX, bottomY, minZ, maxX, topMaxY, maxZ)) {
maxX--;
}
while (minZ < maxZ && boxOnlyAir(level, minX, bottomY, minZ, maxX, topMaxY, minZ)) {
minZ++;
}
while (maxZ > minZ && boxOnlyAir(level, minX, bottomY, maxZ, maxX, topMaxY, maxZ)) {
maxZ--;
}
BlockPos first = new BlockPos(minX, bottomY, minZ);
BlockPos second = new BlockPos(maxX, topMaxY, maxZ);
ModdedWandService.setSelection(player, first, second);
IrisModdedCommands.ok(source, "Auto-select complete: " + describe(first, second));
return 1;
}
private static boolean boxOnlyAir(ServerLevel level, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos();
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
for (int z = minZ; z <= maxZ; z++) {
if (!level.getBlockState(cursor.set(x, y, z)).isAir()) {
return false;
}
}
}
}
return true;
}
private static int analyze(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
String key = keyRaw.trim();
IrisObject object = null;
try {
object = IrisData.loadAnyObject(key, engine == null ? null : engine.getData());
} catch (Throwable e) {
LOGGER.error("Iris object load failed for {}", key, e);
}
if (object == null) {
IrisModdedCommands.fail(source, "Unknown object: " + key);
return 0;
}
IrisModdedCommands.ok(source, "Object Size: " + object.getW() + " * " + object.getH() + " * " + object.getD());
IrisModdedCommands.ok(source, "Blocks Used: " + object.getBlocks().size());
Map<String, Integer> counts = new HashMap<>();
Iterator<PlatformBlockState> values = object.getBlocks().values();
while (values.hasNext()) {
PlatformBlockState state = values.next();
counts.merge(state.key(), 1, Integer::sum);
}
List<Map.Entry<String, Integer>> sorted = new ArrayList<>(counts.entrySet());
sorted.sort(Comparator.comparingInt((Map.Entry<String, Integer> entry) -> entry.getValue()).reversed());
IrisModdedCommands.ok(source, "== Blocks in object ==");
int shown = 0;
for (Map.Entry<String, Integer> entry : sorted) {
IrisModdedCommands.ok(source, " - " + entry.getKey() + " * " + entry.getValue());
shown++;
if (shown >= 10) {
int remaining = sorted.size() - shown;
if (remaining > 0) {
IrisModdedCommands.ok(source, " + " + remaining + " other block state(s)");
}
break;
}
}
return 1;
}
private static int shrink(CommandSourceStack source, String keyRaw) {
ServerLevel level = source.getLevel();
Engine engine = IrisModdedCommands.engineFor(level);
String key = keyRaw.trim();
IrisObject object = null;
try {
object = IrisData.loadAnyObject(key, engine == null ? null : engine.getData());
} catch (Throwable e) {
LOGGER.error("Iris object load failed for {}", key, e);
}
if (object == null) {
IrisModdedCommands.fail(source, "Unknown object: " + key);
return 0;
}
IrisModdedCommands.ok(source, "Current Object Size: " + object.getW() + " * " + object.getH() + " * " + object.getD());
object.shrinkwrap();
IrisModdedCommands.ok(source, "New Object Size: " + object.getW() + " * " + object.getH() + " * " + object.getD());
File file = object.getLoadFile();
if (file == null) {
IrisModdedCommands.fail(source, "Object has no load file; cannot persist the shrink.");
return 0;
}
try {
object.write(file);
} catch (IOException e) {
LOGGER.error("Iris object shrink save failed for {}", file.getAbsolutePath(), e);
IrisModdedCommands.fail(source, "Failed to save object " + file.getName() + ": " + e.getMessage());
return 0;
}
return 1;
}
private static int undo(CommandSourceStack source, int amount) {
ServerPlayer player = source.getPlayer();
UUID owner = player == null ? ModdedObjectUndo.CONSOLE : player.getUUID();
int available = ModdedObjectUndo.size(owner);
if (available == 0) {
IrisModdedCommands.fail(source, "Nothing to undo.");
return 0;
}
int reverted = ModdedObjectUndo.undo(owner, Math.min(amount, available));
IrisModdedCommands.ok(source, "Reverted " + reverted + " paste(s)!");
return 1;
}
private static String describe(BlockPos first, BlockPos second) {
return "(" + first.getX() + "," + first.getY() + "," + first.getZ() + ") -> ("
+ second.getX() + "," + second.getY() + "," + second.getZ() + ")";
}
}
@@ -0,0 +1,147 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IObjectPlacer;
import art.arcane.iris.engine.object.TileData;
import art.arcane.iris.modded.ModdedBlockResolution;
import art.arcane.iris.modded.ModdedBlockState;
import art.arcane.iris.spi.PlatformBlockState;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.levelgen.Heightmap;
import java.util.HashMap;
import java.util.Map;
final class ModdedObjectPlacer implements IObjectPlacer {
private final ServerLevel level;
private final Map<BlockPos, BlockState> undo = new HashMap<>();
private int writes = 0;
private int nonAirWrites = 0;
private int skippedTiles = 0;
ModdedObjectPlacer(ServerLevel level) {
this.level = level;
}
Map<BlockPos, BlockState> undoSnapshot() {
return undo;
}
int writes() {
return writes;
}
int nonAirWrites() {
return nonAirWrites;
}
int skippedTiles() {
return skippedTiles;
}
@Override
public int getHighest(int x, int z, IrisData data) {
return level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
}
@Override
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
return level.getHeight(ignoreFluid ? Heightmap.Types.OCEAN_FLOOR : Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
}
@Override
public void set(int x, int y, int z, PlatformBlockState s) {
if (y <= level.getMinY() || y >= level.getMinY() + level.getHeight()) {
return;
}
BlockPos pos = new BlockPos(x, y, z);
BlockState current = level.getBlockState(pos);
if (current.is(Blocks.BEDROCK)) {
return;
}
BlockState target = (BlockState) s.nativeHandle();
undo.putIfAbsent(pos, current);
level.setBlock(pos, target, Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
writes++;
if (!target.isAir()) {
nonAirWrites++;
}
}
@Override
public PlatformBlockState get(int x, int y, int z) {
return ModdedBlockState.of(level.getBlockState(new BlockPos(x, y, z)), null);
}
@Override
public boolean isPreventingDecay() {
return false;
}
@Override
public boolean isCarved(int x, int y, int z) {
return false;
}
@Override
public boolean isSolid(int x, int y, int z) {
return ModdedBlockResolution.isSolid(level.getBlockState(new BlockPos(x, y, z)));
}
@Override
public boolean isUnderwater(int x, int z) {
return false;
}
@Override
public int getFluidHeight() {
return 63;
}
@Override
public boolean isDebugSmartBore() {
return false;
}
@Override
public void setTile(int xx, int yy, int zz, TileData tile) {
skippedTiles++;
}
@Override
public <T> void setData(int xx, int yy, int zz, T data) {
}
@Override
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
return null;
}
@Override
public Engine getEngine() {
return null;
}
}
@@ -0,0 +1,105 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
public final class ModdedObjectUndo {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final int MAX_ENTRIES_PER_OWNER = 32;
private static final ConcurrentHashMap<UUID, Deque<Entry>> UNDOS = new ConcurrentHashMap<>();
private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);
public static final UUID CONSOLE = new UUID(0L, 0L);
private ModdedObjectUndo() {
}
private record Entry(ServerLevel level, Map<BlockPos, BlockState> blocks) {
}
public static void init() {
if (INITIALIZED.compareAndSet(false, true)) {
LOGGER.info("Iris object undo service ready (bounded to {} paste(s) per player)", MAX_ENTRIES_PER_OWNER);
}
}
public static void record(UUID owner, ServerLevel level, Map<BlockPos, BlockState> oldBlocks) {
if (oldBlocks == null || oldBlocks.isEmpty()) {
return;
}
Deque<Entry> queue = UNDOS.computeIfAbsent(owner, (UUID key) -> new ArrayDeque<>());
synchronized (queue) {
queue.addLast(new Entry(level, oldBlocks));
while (queue.size() > MAX_ENTRIES_PER_OWNER) {
queue.pollFirst();
}
}
}
public static int size(UUID owner) {
Deque<Entry> queue = UNDOS.get(owner);
if (queue == null) {
return 0;
}
synchronized (queue) {
return queue.size();
}
}
public static int undo(UUID owner, int amount) {
Deque<Entry> queue = UNDOS.get(owner);
if (queue == null) {
return 0;
}
int reverted = 0;
while (reverted < amount) {
Entry entry;
synchronized (queue) {
entry = queue.pollLast();
}
if (entry == null) {
break;
}
int writes = 0;
for (Map.Entry<BlockPos, BlockState> block : entry.blocks().entrySet()) {
entry.level().setBlock(block.getKey(), block.getValue(), Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE);
writes++;
}
LOGGER.info("Iris object undo: reverted {} block(s) in {}", writes, entry.level().dimension().identifier());
reverted++;
}
return reverted;
}
public static void clearAll() {
UNDOS.clear();
}
}
@@ -0,0 +1,191 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.core.pack.PackValidator;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
public final class ModdedPackCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private ModdedPackCommands() {
}
public static LiteralArgumentBuilder<CommandSourceStack> tree() {
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("pack").requires(GATE);
root.then(Commands.literal("validate")
.executes((CommandContext<CommandSourceStack> context) -> validate(context.getSource(), null))
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> 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<CommandSourceStack> context) -> restore(context.getSource(), StringArgumentType.getString(context, "pack")))));
root.then(Commands.literal("status")
.executes((CommandContext<CommandSourceStack> context) -> status(context.getSource(), null))
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> status(context.getSource(), StringArgumentType.getString(context, "pack")))));
return root;
}
static File packsRoot() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile();
}
private static int validate(CommandSourceStack source, String pack) {
File packsRoot = packsRoot();
if (!packsRoot.isDirectory()) {
IrisModdedCommands.fail(source, "Packs folder not found: " + packsRoot.getAbsolutePath());
return 0;
}
List<File> targets = new ArrayList<>();
if (pack == null || pack.isBlank()) {
File[] dirs = packsRoot.listFiles(File::isDirectory);
if (dirs == null || dirs.length == 0) {
IrisModdedCommands.fail(source, "No packs to validate under " + packsRoot.getAbsolutePath());
return 0;
}
for (File dir : dirs) {
targets.add(dir);
}
} else {
File target = new File(packsRoot, pack);
if (!target.isDirectory()) {
IrisModdedCommands.fail(source, "Pack '" + pack + "' not found under " + packsRoot.getAbsolutePath());
return 0;
}
targets.add(target);
}
MinecraftServer server = source.getServer();
IrisModdedCommands.ok(source, "Validating " + targets.size() + " pack(s)...");
Thread thread = new Thread(() -> {
int broken = 0;
for (File target : targets) {
try {
PackValidationResult result = PackValidator.validate(target);
PackValidationRegistry.publish(result);
if (!result.isLoadable()) {
broken++;
}
server.execute(() -> report(source, result));
} catch (Throwable e) {
LOGGER.error("Iris pack validation failed for {}", target.getName(), e);
server.execute(() -> IrisModdedCommands.fail(source, "Validation of '" + target.getName() + "' failed: " + e.getMessage()));
broken++;
}
}
int brokenTotal = broken;
server.execute(() -> IrisModdedCommands.ok(source, "Validation complete. Broken packs: " + brokenTotal + "/" + targets.size()));
}, "Iris Pack Validator");
thread.setDaemon(true);
thread.start();
return 1;
}
private static int restore(CommandSourceStack source, String pack) {
File packFolder = new File(packsRoot(), pack);
if (!packFolder.isDirectory()) {
IrisModdedCommands.fail(source, "Pack '" + pack + "' not found under " + packsRoot().getAbsolutePath());
return 0;
}
int restored = PackValidator.restoreTrash(packFolder);
if (restored == 0) {
IrisModdedCommands.fail(source, "Nothing to restore for pack '" + pack + "'.");
return 0;
}
IrisModdedCommands.ok(source, "Restored " + restored + " file(s) from the most recent trash dump for pack '" + pack + "'.");
IrisModdedCommands.ok(source, "Re-run /iris pack validate " + pack + " to re-check.");
return 1;
}
private static int status(CommandSourceStack source, String pack) {
if (pack == null || pack.isBlank()) {
Map<String, PackValidationResult> snapshot = PackValidationRegistry.snapshot();
if (snapshot.isEmpty()) {
IrisModdedCommands.fail(source, "No validation results recorded. Run /iris pack validate first.");
return 0;
}
for (Map.Entry<String, PackValidationResult> entry : snapshot.entrySet()) {
PackValidationResult result = entry.getValue();
String tag = result.isLoadable() ? "OK" : "BROKEN";
IrisModdedCommands.ok(source, tag + " " + entry.getKey()
+ " (blocking=" + result.getBlockingErrors().size()
+ ", warnings=" + result.getWarnings().size()
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
}
return 1;
}
PackValidationResult result = PackValidationRegistry.get(pack);
if (result == null) {
IrisModdedCommands.fail(source, "No validation result for '" + pack + "'. Run /iris pack validate " + pack + ".");
return 0;
}
report(source, result);
return 1;
}
private static void report(CommandSourceStack source, PackValidationResult result) {
if (result.isLoadable()) {
IrisModdedCommands.ok(source, "Pack '" + result.getPackName() + "' is loadable."
+ " (warnings=" + result.getWarnings().size()
+ ", trashed=" + result.getRemovedUnusedFiles().size() + ")");
} else {
IrisModdedCommands.fail(source, "Pack '" + result.getPackName() + "' is BROKEN:");
for (String reason : result.getBlockingErrors()) {
IrisModdedCommands.fail(source, " - " + reason);
}
}
int warningMax = Math.min(10, result.getWarnings().size());
for (int i = 0; i < warningMax; i++) {
IrisModdedCommands.ok(source, " ! " + result.getWarnings().get(i));
}
if (result.getWarnings().size() > warningMax) {
IrisModdedCommands.ok(source, " ... and " + (result.getWarnings().size() - warningMax) + " more warning(s).");
}
int trashMax = Math.min(10, result.getRemovedUnusedFiles().size());
for (int i = 0; i < trashMax; i++) {
IrisModdedCommands.ok(source, " ~ trashed " + result.getRemovedUnusedFiles().get(i));
}
if (result.getRemovedUnusedFiles().size() > trashMax) {
IrisModdedCommands.ok(source, " ... and " + (result.getRemovedUnusedFiles().size() - trashMax) + " more trashed file(s).");
}
}
}
@@ -0,0 +1,183 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.core.pregenerator.IrisPregenerator;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.Position2;
import net.minecraft.server.level.ServerLevel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.atomic.AtomicReference;
public final class ModdedPregenJob implements PregenListener {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final AtomicReference<ModdedPregenJob> ACTIVE = new AtomicReference<>();
private final String dimension;
private final IrisPregenerator pregenerator;
private volatile double chunksPerSecond;
private volatile long generated;
private volatile long totalChunks;
private volatile long eta;
private volatile long elapsed;
private volatile String method = "Modded";
private ModdedPregenJob(ServerLevel level, Engine engine, PregenTask task) {
this.dimension = level.dimension().identifier().toString();
this.pregenerator = new IrisPregenerator(task, new ModdedPregenMethod(level, engine), this);
}
public static boolean start(ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ) {
if (ACTIVE.get() != null) {
return false;
}
PregenTask task = PregenTask.builder()
.gui(false)
.center(new Position2(centerBlockX, centerBlockZ))
.radiusX(radiusBlocks)
.radiusZ(radiusBlocks)
.build();
ModdedPregenJob job = new ModdedPregenJob(level, engine, task);
if (!ACTIVE.compareAndSet(null, job)) {
return false;
}
Thread thread = new Thread(() -> {
try {
job.pregenerator.start();
} catch (Throwable e) {
LOGGER.error("Iris pregen failed for {}", job.dimension, e);
} finally {
ACTIVE.compareAndSet(job, null);
}
}, "Iris Pregen");
thread.setDaemon(true);
thread.start();
return true;
}
public static boolean stop() {
ModdedPregenJob job = ACTIVE.get();
if (job == null) {
return false;
}
job.pregenerator.close();
return true;
}
public static Boolean pauseResume() {
ModdedPregenJob job = ACTIVE.get();
if (job == null) {
return null;
}
if (job.pregenerator.paused()) {
job.pregenerator.resume();
return Boolean.FALSE;
}
job.pregenerator.pause();
return Boolean.TRUE;
}
public static String status() {
ModdedPregenJob job = ACTIVE.get();
if (job == null) {
return null;
}
long total = Math.max(1L, job.totalChunks);
double percent = ((double) job.generated / (double) total) * 100D;
return "Pregen " + job.dimension + ": "
+ Form.f(job.generated) + "/" + Form.f(job.totalChunks)
+ " (" + String.format("%.1f", percent) + "%), "
+ Form.f((int) job.chunksPerSecond) + "/s"
+ ", ETA " + Form.duration(job.eta, 2)
+ ", elapsed " + Form.duration(job.elapsed, 2)
+ ", method " + job.method
+ (job.pregenerator.paused() ? ", PAUSED" : "");
}
@Override
public void onTick(double chunksPerSecond, double chunksPerMinute, double regionsPerMinute, double percent, long generated, long totalChunks, long chunksRemaining, long eta, long elapsed, String method, boolean cached) {
this.chunksPerSecond = chunksPerSecond;
this.generated = generated;
this.totalChunks = totalChunks;
this.eta = eta;
this.elapsed = elapsed;
this.method = method;
}
@Override
public void onChunkGenerating(int x, int z) {
}
@Override
public void onChunkGenerated(int x, int z, boolean cached) {
}
@Override
public void onRegionGenerated(int x, int z) {
}
@Override
public void onRegionGenerating(int x, int z) {
}
@Override
public void onChunkCleaned(int x, int z) {
}
@Override
public void onRegionSkipped(int x, int z) {
}
@Override
public void onNetworkStarted(int x, int z) {
}
@Override
public void onNetworkFailed(int x, int z) {
}
@Override
public void onNetworkReclaim(int revert) {
}
@Override
public void onNetworkGeneratedChunk(int x, int z) {
}
@Override
public void onNetworkDownloaded(int x, int z) {
}
@Override
public void onClose() {
}
@Override
public void onSaving() {
}
@Override
public void onChunkExistsInRegionGen(int x, int z) {
}
}
@@ -0,0 +1,142 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.core.IrisSettings;
import art.arcane.iris.core.pregenerator.PregenListener;
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import net.minecraft.server.level.ChunkResult;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.level.ChunkPos;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public final class ModdedPregenMethod implements PregeneratorMethod {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final TicketType PREGEN_TICKET = new TicketType(TicketType.NO_TIMEOUT, TicketType.FLAG_LOADING | TicketType.FLAG_KEEP_DIMENSION_ACTIVE);
private final ServerLevel level;
private final Engine engine;
private final Semaphore semaphore;
private final int permits;
private final int timeoutSeconds;
public ModdedPregenMethod(ServerLevel level, Engine engine) {
this.level = level;
this.engine = engine;
this.permits = Math.min(96, Math.max(8, Runtime.getRuntime().availableProcessors() * 2));
this.semaphore = new Semaphore(permits, true);
this.timeoutSeconds = Math.max(120, IrisSettings.get().getPregen().getChunkLoadTimeoutSeconds());
}
@Override
public void init() {
LOGGER.info("Iris modded pregen init: dim={} inFlightCap={} timeout={}s",
level.dimension().identifier(), permits, timeoutSeconds);
}
@Override
public void close() {
semaphore.acquireUninterruptibly(permits);
saveLevel();
}
@Override
public void save() {
saveLevel();
}
private void saveLevel() {
level.getServer().execute(() -> level.save(null, false, false));
}
@Override
public boolean supportsRegions(int x, int z, PregenListener listener) {
return false;
}
@Override
public String getMethod(int x, int z) {
return "Modded";
}
@Override
public boolean isAsyncChunkMode() {
return true;
}
@Override
public void generateRegion(int x, int z, PregenListener listener) {
throw new UnsupportedOperationException();
}
@Override
public void generateChunk(int x, int z, PregenListener listener) {
listener.onChunkGenerating(x, z);
try {
semaphore.acquire();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
ChunkPos pos = new ChunkPos(x, z);
CompletableFuture<?> loadFuture = CompletableFuture
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(PREGEN_TICKET, pos, 0), level.getServer())
.thenCompose((CompletableFuture<?> inner) -> inner);
loadFuture.orTimeout(timeoutSeconds, TimeUnit.SECONDS).whenComplete((Object result, Throwable error) -> {
level.getServer().execute(() -> level.getChunkSource().removeTicketWithRadius(PREGEN_TICKET, pos, 0));
try {
if (error != null) {
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, error.toString());
return;
}
if (result instanceof ChunkResult<?> chunkResult && !chunkResult.isSuccess()) {
LOGGER.warn("Iris pregen chunk {},{} returned no chunk: {}", x, z, chunkResult.getError());
return;
}
listener.onChunkGenerated(x, z);
cleanupMantleChunk(x, z);
listener.onChunkCleaned(x, z);
} finally {
semaphore.release();
}
});
}
private void cleanupMantleChunk(int x, int z) {
try {
engine.getMantle().forceCleanupChunk(x, z);
} catch (Throwable ignored) {
}
}
@Override
public Mantle getMantle() {
return engine.getMantle().getMantle();
}
}
@@ -0,0 +1,285 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.mantle.EngineMantle;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.ModdedBlockBuffer;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.PlatformBiome;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.iris.util.project.hunk.Hunk;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.math.M;
import net.minecraft.commands.CommandSourceStack;
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;
import net.minecraft.server.level.ThreadedLevelLightEngine;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.AABB;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public final class ModdedRegen {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final int APPLY_AHEAD = 8;
private static final AtomicBoolean ACTIVE = new AtomicBoolean(false);
private final CommandSourceStack source;
private final MinecraftServer server;
private final ServerLevel level;
private final IrisModdedChunkGenerator generator;
private final Engine engine;
private final int centerX;
private final int centerZ;
private final int radius;
private ModdedRegen(CommandSourceStack source, ServerLevel level, IrisModdedChunkGenerator generator, Engine engine, int centerX, int centerZ, int radius) {
this.source = source;
this.server = source.getServer();
this.level = level;
this.generator = generator;
this.engine = engine;
this.centerX = centerX;
this.centerZ = centerZ;
this.radius = Math.max(0, radius);
}
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."));
return;
}
int centerX = player.blockPosition().getX() >> 4;
int centerZ = player.blockPosition().getZ() >> 4;
ModdedRegen job = new ModdedRegen(source, level, generator, engine, centerX, centerZ, radius);
int chunks = (job.radius * 2 + 1) * (job.radius * 2 + 1);
job.ok("Regen started: " + chunks + " chunk(s) around " + centerX + "," + centerZ + ". Deleting and regenerating in place.");
LOGGER.info("Iris regen start: dim={} center={},{} radius={} chunks={}",
level.dimension().identifier(), centerX, centerZ, job.radius, chunks);
Thread thread = new Thread(job::run, "Iris Regenerate");
thread.setDaemon(true);
thread.start();
}
private void run() {
long startedAt = M.ms();
try {
resetMantleMargin();
List<int[]> targets = orderedTargets(centerX, centerZ, radius);
int applied = regenerate(targets);
ok("Regen finished: " + applied + "/" + targets.size() + " chunk(s) in " + Form.duration(M.ms() - startedAt, 2));
LOGGER.info("Iris regen done: {}/{} chunks in {}ms", applied, targets.size(), M.ms() - startedAt);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Throwable e) {
LOGGER.error("Iris regen failed", e);
fail("Regen failed: " + e);
} finally {
ACTIVE.set(false);
}
}
private void resetMantleMargin() {
EngineMantle engineMantle = engine.getMantle();
int margin = radius + Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1;
for (int dx = -margin; dx <= margin; dx++) {
for (int dz = -margin; dz <= margin; dz++) {
engineMantle.getMantle().deleteChunk(centerX + dx, centerZ + dz);
}
}
}
private int regenerate(List<int[]> targets) throws InterruptedException {
Semaphore inFlight = new Semaphore(APPLY_AHEAD);
CountDownLatch allApplied = new CountDownLatch(targets.size());
AtomicInteger completed = new AtomicInteger();
AtomicInteger applied = new AtomicInteger();
int total = targets.size();
int stride = total <= 64 ? 1 : 32;
int height = engine.getMaxHeight() - engine.getMinHeight();
PlatformBlockState air = IrisPlatforms.get().registries().air();
for (int[] target : targets) {
int chunkX = target[0];
int chunkZ = target[1];
inFlight.acquire();
MultiBurst.burst.lazy(() -> {
long chunkStart = M.ms();
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
try {
engine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false);
} catch (Throwable e) {
LOGGER.error("Iris regen chunk {},{} generation failed", chunkX, chunkZ, e);
fail("Chunk " + chunkX + "," + chunkZ + " generation FAILED: " + e.getClass().getSimpleName());
completed.incrementAndGet();
inFlight.release();
allApplied.countDown();
return;
}
server.execute(() -> {
boolean success = false;
try {
apply(chunkX, chunkZ, blocks, biomes);
success = true;
applied.incrementAndGet();
} catch (Throwable e) {
LOGGER.error("Iris regen chunk {},{} apply failed", chunkX, chunkZ, e);
fail("Chunk " + chunkX + "," + chunkZ + " apply FAILED: " + e.getClass().getSimpleName());
} finally {
int done = completed.incrementAndGet();
if (success && (done % stride == 0 || done == total)) {
ok("Regen [" + done + "/" + total + "] chunk " + chunkX + "," + chunkZ + " in " + (M.ms() - chunkStart) + "ms");
}
inFlight.release();
allApplied.countDown();
}
});
});
}
allApplied.await();
return applied.get();
}
private void apply(int chunkX, int chunkZ, ModdedBlockBuffer blocks, Hunk<PlatformBiome> biomes) {
LevelChunk chunk = level.getChunk(chunkX, chunkZ);
ChunkPos pos = chunk.getPos();
discardEntities(pos);
for (BlockPos blockEntityPos : new ArrayList<>(chunk.getBlockEntities().keySet())) {
chunk.removeBlockEntity(blockEntityPos);
}
int dimMinY = engine.getMinHeight();
int height = engine.getMaxHeight() - dimMinY;
int baseX = pos.getMinBlockX();
int baseZ = pos.getMinBlockZ();
BlockState airState = Blocks.AIR.defaultBlockState();
ThreadedLevelLightEngine lightEngine = (ThreadedLevelLightEngine) level.getChunkSource().getLightEngine();
List<BlockPos> lightChecks = new ArrayList<>();
for (int i = 0; i < chunk.getSectionsCount(); i++) {
LevelChunkSection section = chunk.getSection(i);
int sectionBaseY = chunk.getSectionYFromSectionIndex(i) << 4;
section.acquire();
try {
for (int y = 0; y < 16; y++) {
int worldY = sectionBaseY + y;
int bufferY = worldY - dimMinY;
boolean inRange = bufferY >= 0 && bufferY < height;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
BlockState target = airState;
if (inRange && !blocks.isAir(x, bufferY, z)) {
target = (BlockState) blocks.getRaw(x, bufferY, z).nativeHandle();
}
BlockState previous = section.setBlockState(x, y, z, target, false);
if (previous != target && (previous.getLightEmission() != target.getLightEmission()
|| previous.getLightDampening() != target.getLightDampening()
|| previous.propagatesSkylightDown() != target.propagatesSkylightDown())) {
lightChecks.add(new BlockPos(baseX + x, worldY, baseZ + z));
}
if (target.hasBlockEntity() && target.getBlock() instanceof EntityBlock entityBlock) {
BlockEntity entity = entityBlock.newBlockEntity(new BlockPos(baseX + x, worldY, baseZ + z), target);
if (entity != null) {
chunk.setBlockEntity(entity);
}
}
}
}
}
} finally {
section.release();
}
lightEngine.updateSectionStatus(SectionPos.of(pos, chunk.getSectionYFromSectionIndex(i)), section.hasOnlyAir());
}
Heightmap.primeHeightmaps(chunk, ChunkStatus.FULL.heightmapsAfter());
Registry<Biome> biomeRegistry = level.registryAccess().lookupOrThrow(Registries.BIOME);
chunk.fillBiomesFromNoise(generator.regenBiomeResolver(biomeRegistry, biomes, pos), level.getChunkSource().randomState().sampler());
chunk.markUnsaved();
for (BlockPos check : lightChecks) {
lightEngine.checkBlock(check);
}
for (ServerPlayer tracking : level.getChunkSource().chunkMap.getPlayers(pos, false)) {
tracking.connection.chunkSender.dropChunk(tracking, pos);
tracking.connection.chunkSender.markChunkPendingToSend(chunk);
}
}
private void discardEntities(ChunkPos pos) {
AABB box = new AABB(pos.getMinBlockX(), level.getMinY(), pos.getMinBlockZ(),
pos.getMaxBlockX() + 1, level.getMaxY() + 1, pos.getMaxBlockZ() + 1);
for (Entity entity : level.getEntities((Entity) null, box, (Entity e) -> !(e instanceof ServerPlayer))) {
entity.discard();
}
}
private static List<int[]> orderedTargets(int centerX, int centerZ, int radius) {
List<int[]> targets = new ArrayList<>();
for (int dx = -radius; dx <= radius; dx++) {
for (int dz = -radius; dz <= radius; dz++) {
targets.add(new int[]{centerX + dx, centerZ + dz});
}
}
targets.sort(Comparator.comparingInt((int[] t) -> {
int ox = t[0] - centerX;
int oz = t[1] - centerZ;
return ox * ox + oz * oz;
}));
return targets;
}
private void ok(String message) {
server.execute(() -> source.sendSuccess(() -> Component.literal(message), false));
}
private void fail(String message) {
server.execute(() -> source.sendFailure(Component.literal(message)));
}
}
@@ -0,0 +1,206 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.structure.StructureIndexService;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.PlacedStructurePiece;
import art.arcane.iris.engine.framework.StructureAssembler;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisStructure;
import art.arcane.iris.engine.object.ObjectPlaceMode;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
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.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
public final class ModdedStructureCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private static final SuggestionProvider<CommandSourceStack> IRIS_STRUCTURE_KEYS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> suggestIrisStructureKeys(context, builder);
private ModdedStructureCommands() {
}
public static LiteralArgumentBuilder<CommandSourceStack> tree() {
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("structure").requires(GATE);
root.then(Commands.literal("list")
.executes((CommandContext<CommandSourceStack> context) -> list(context.getSource())));
root.then(Commands.literal("info")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(IRIS_STRUCTURE_KEYS)
.executes((CommandContext<CommandSourceStack> context) -> info(context.getSource(), StringArgumentType.getString(context, "key")))));
root.then(Commands.literal("place")
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(IRIS_STRUCTURE_KEYS)
.executes((CommandContext<CommandSourceStack> 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("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("verify", "Vanilla structure locate is meaningless here: Iris modded dimensions do not run vanilla structure placement (Iris places structures itself). Use /iris goto structure <key> to locate Iris-placed structures."));
return root;
}
private static LiteralArgumentBuilder<CommandSourceStack> message(String name, String text) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> {
IrisModdedCommands.fail(context.getSource(), text);
return 0;
})
.then(Commands.argument("args", StringArgumentType.greedyString())
.executes((CommandContext<CommandSourceStack> context) -> {
IrisModdedCommands.fail(context.getSource(), text);
return 0;
}));
}
private static IrisData dataFor(CommandSourceStack source) {
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
if (engine == null) {
IrisModdedCommands.fail(source, "This dimension is not generated by Iris. Run this from an Iris dimension so the pack can be resolved.");
return null;
}
return engine.getData();
}
private static int list(CommandSourceStack source) {
IrisData data = dataFor(source);
if (data == null) {
return 0;
}
File file = StructureIndexService.write(data);
IrisModdedCommands.ok(source, "Wrote structure index: " + file.getPath());
return 1;
}
private static int info(CommandSourceStack source, String keyRaw) {
IrisData data = dataFor(source);
if (data == null) {
return 0;
}
String key = keyRaw.trim();
IrisStructure structure = IrisData.loadAnyStructure(key, data);
if (structure == null) {
IrisModdedCommands.fail(source, "No iris structure '" + key + "' in this pack");
return 0;
}
StructureAssembler assembler = new StructureAssembler(data, structure, 0, 64, 0);
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(1234));
if (pieces == null || pieces.isEmpty()) {
IrisModdedCommands.fail(source, "Structure '" + key + "' assembled 0 pieces (check startPool '" + structure.getStartPool() + "')");
return 0;
}
int minX = Integer.MAX_VALUE;
int minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE;
int maxZ = Integer.MIN_VALUE;
for (PlacedStructurePiece piece : pieces) {
minX = Math.min(minX, piece.getMinX());
minZ = Math.min(minZ, piece.getMinZ());
maxX = Math.max(maxX, piece.getMaxX());
maxZ = Math.max(maxZ, piece.getMaxZ());
}
IrisModdedCommands.ok(source, "Structure '" + key + "': " + pieces.size() + " pieces, footprint "
+ (maxX - minX + 1) + "x" + (maxZ - minZ + 1) + " blocks (sample seed 1234)");
return 1;
}
private static int place(CommandSourceStack source, String keyRaw) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by players (the structure is assembled at your position).");
return 0;
}
ServerLevel level = source.getLevel();
IrisData data = dataFor(source);
if (data == null) {
return 0;
}
String key = keyRaw.trim();
IrisStructure structure = IrisData.loadAnyStructure(key, data);
if (structure == null) {
IrisModdedCommands.fail(source, "No iris structure '" + key + "' in this pack");
return 0;
}
int originX = player.blockPosition().getX();
int originY = player.blockPosition().getY();
int originZ = player.blockPosition().getZ();
StructureAssembler assembler = new StructureAssembler(data, structure, originX, originY, originZ);
RNG rng = new RNG((long) originX * 341873128712L + originZ);
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
if (pieces == null || pieces.isEmpty()) {
IrisModdedCommands.fail(source, "Structure '" + key + "' assembled 0 pieces");
return 0;
}
ModdedObjectPlacer placer = new ModdedObjectPlacer(level);
UUID owner = player.getUUID();
try {
for (PlacedStructurePiece piece : pieces) {
IrisObjectPlacement config = new IrisObjectPlacement();
config.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
config.setRotation(piece.getRotation());
config.getPlace().add(piece.getObject().getLoadKey());
if (!structure.getEdit().isEmpty()) {
config.setEdit(structure.getEdit());
}
piece.getObject().place(piece.getX(), piece.getY(), piece.getZ(), placer, config, rng, null, null, data);
}
} catch (Throwable e) {
LOGGER.error("Iris structure place failed for {}", key, e);
ModdedObjectUndo.record(owner, level, placer.undoSnapshot());
IrisModdedCommands.fail(source, "Place failed: " + e.getClass().getSimpleName() + " (partial changes recorded for undo)");
return 0;
}
ModdedObjectUndo.record(owner, level, placer.undoSnapshot());
String tileNote = placer.skippedTiles() > 0 ? ", " + placer.skippedTiles() + " tile state(s) skipped" : "";
IrisModdedCommands.ok(source, "Placed '" + key + "' (" + pieces.size() + " pieces, " + placer.writes() + " write(s)" + tileNote + ") at your location. /iris object undo to revert.");
return 1;
}
private static CompletableFuture<Suggestions> suggestIrisStructureKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
try {
Engine engine = IrisModdedCommands.engineFor(context.getSource().getLevel());
if (engine != null && engine.getData().getStructureLoader() != null) {
return SharedSuggestionProvider.suggest(engine.getData().getStructureLoader().getPossibleKeys(), builder);
}
} catch (Throwable ignored) {
}
return builder.buildFuture();
}
}
@@ -0,0 +1,412 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.object.IrisBiome;
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.engine.object.IrisEntitySpawn;
import art.arcane.iris.engine.object.IrisObjectPlacement;
import art.arcane.iris.engine.object.IrisRegion;
import art.arcane.iris.engine.object.IrisSpawner;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedPackInstaller;
import art.arcane.iris.util.common.parallel.BurstExecutor;
import art.arcane.iris.util.common.parallel.MultiBurst;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.format.Form;
import art.arcane.volmlib.util.io.IO;
import art.arcane.volmlib.util.json.JSONObject;
import art.arcane.volmlib.util.math.M;
import art.arcane.volmlib.util.math.Spiraler;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zeroturnaround.zip.ZipUtil;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public final class ModdedStudioCommands {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
private static final String DEFAULT_TEMPLATE = "example";
private ModdedStudioCommands() {
}
public static LiteralArgumentBuilder<CommandSourceStack> tree() {
LiteralArgumentBuilder<CommandSourceStack> root = Commands.literal("studio").requires(GATE);
root.then(Commands.literal("create")
.then(Commands.argument("name", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) -> create(context.getSource(), StringArgumentType.getString(context, "name"), DEFAULT_TEMPLATE))
.then(Commands.argument("template", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> create(context.getSource(), StringArgumentType.getString(context, "name"), StringArgumentType.getString(context, "template"))))));
root.then(Commands.literal("package")
.executes((CommandContext<CommandSourceStack> context) -> pkg(context.getSource(), null))
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> pkg(context.getSource(), StringArgumentType.getString(context, "pack")))));
root.then(Commands.literal("version")
.executes((CommandContext<CommandSourceStack> context) -> version(context.getSource(), null))
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> version(context.getSource(), StringArgumentType.getString(context, "pack")))));
root.then(Commands.literal("regions")
.executes((CommandContext<CommandSourceStack> context) -> regions(context.getSource(), 500))
.then(Commands.argument("radius", IntegerArgumentType.integer(8, 1000))
.executes((CommandContext<CommandSourceStack> 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/<pack> 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("tpstudio", "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/<pack> 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("noise", "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("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("objects", "The chunk object report reads Bukkit chunk data and is not ported to modded servers."));
return root;
}
private static LiteralArgumentBuilder<CommandSourceStack> message(String name, String text) {
return Commands.literal(name)
.executes((CommandContext<CommandSourceStack> context) -> {
IrisModdedCommands.fail(context.getSource(), text);
return 0;
})
.then(Commands.argument("args", StringArgumentType.greedyString())
.executes((CommandContext<CommandSourceStack> context) -> {
IrisModdedCommands.fail(context.getSource(), text);
return 0;
}));
}
private static File resolvePack(CommandSourceStack source, String pack) {
String name = pack;
if (name == null || name.isBlank()) {
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
if (engine == null || engine.getDimension() == null) {
IrisModdedCommands.fail(source, "This dimension is not generated by Iris; specify a pack name explicitly.");
return null;
}
name = engine.getDimension().getLoadKey();
}
File folder = new File(ModdedPackCommands.packsRoot(), name);
if (!folder.isDirectory() || !new File(folder, "dimensions").isDirectory()) {
IrisModdedCommands.fail(source, "Pack '" + name + "' not found under " + ModdedPackCommands.packsRoot().getAbsolutePath());
return null;
}
return folder;
}
private static int version(CommandSourceStack source, String pack) {
File folder = resolvePack(source, pack);
if (folder == null) {
return 0;
}
IrisData data = IrisData.get(folder);
IrisDimension dimension = data.getDimensionLoader().load(folder.getName());
if (dimension == null) {
IrisModdedCommands.fail(source, "Pack '" + folder.getName() + "' has no dimensions/" + folder.getName() + ".json");
return 0;
}
IrisModdedCommands.ok(source, "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion());
return 1;
}
private static int create(CommandSourceStack source, String nameRaw, String template) {
String name = nameRaw.toLowerCase(Locale.ROOT);
if (!PROJECT_NAME.matcher(name).matches()) {
IrisModdedCommands.fail(source, "Invalid project name '" + nameRaw + "' (allowed: a-z, 0-9, _ and -)");
return 0;
}
File packsRoot = ModdedPackCommands.packsRoot();
File target = new File(packsRoot, name);
if (target.exists()) {
IrisModdedCommands.fail(source, "Pack '" + name + "' already exists at " + target.getAbsolutePath());
return 0;
}
MinecraftServer server = source.getServer();
IrisModdedCommands.ok(source, "Creating project '" + name + "' from template '" + template + "'...");
Thread thread = new Thread(() -> {
try {
File templateFolder = new File(packsRoot, template);
if (!new File(templateFolder, "dimensions/" + template + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.ok(source, "Template '" + template + "' is not installed; downloading IrisDimensions/" + template + "..."));
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), template, "master",
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
if (!installed || !new File(templateFolder, "dimensions/" + template + ".json").isFile()) {
server.execute(() -> IrisModdedCommands.fail(source, "Template '" + template + "' could not be downloaded; install a pack with dimensions/" + template + ".json first."));
return;
}
}
copyProject(templateFolder, target, template, name);
server.execute(() -> {
IrisModdedCommands.ok(source, "Created project '" + name + "' at " + target.getAbsolutePath());
IrisModdedCommands.ok(source, "Edit dimensions/" + name + ".json and the rest of the pack, then create a world with it. VSCode workspaces are generated by the Bukkit studio toolchain only.");
});
} catch (Throwable e) {
LOGGER.error("Iris studio create failed for {}", name, e);
server.execute(() -> IrisModdedCommands.fail(source, "Project creation failed: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage())));
}
}, "Iris Studio Create");
thread.setDaemon(true);
thread.start();
return 1;
}
private static void copyProject(File templateFolder, File target, String template, String name) throws IOException {
Path source = templateFolder.toPath();
try (Stream<Path> walk = Files.walk(source)) {
for (Path path : walk.sorted(Comparator.naturalOrder()).toList()) {
String relative = source.relativize(path).toString();
if (relative.isEmpty() || relative.equals(".git") || relative.startsWith(".git" + File.separator) || relative.endsWith(".code-workspace")) {
continue;
}
Path destination = target.toPath().resolve(relative);
if (Files.isDirectory(path)) {
Files.createDirectories(destination);
} else {
Files.createDirectories(destination.getParent());
Files.copy(path, destination, StandardCopyOption.REPLACE_EXISTING);
}
}
}
File oldDimension = new File(target, "dimensions/" + template + ".json");
File newDimension = new File(target, "dimensions/" + name + ".json");
if (oldDimension.isFile()) {
Files.copy(oldDimension.toPath(), newDimension.toPath(), StandardCopyOption.REPLACE_EXISTING);
Files.delete(oldDimension.toPath());
}
if (newDimension.isFile()) {
JSONObject json = new JSONObject(IO.readAll(newDimension));
if (json.has("name")) {
json.put("name", Form.capitalizeWords(name.replaceAll("\\Q-\\E", " ")));
IO.writeAll(newDimension, json.toString(4));
}
}
}
private static int pkg(CommandSourceStack source, String pack) {
File folder = resolvePack(source, pack);
if (folder == null) {
return 0;
}
MinecraftServer server = source.getServer();
String dimKey = folder.getName();
IrisModdedCommands.ok(source, "Packaging dimension '" + dimKey + "'...");
Thread thread = new Thread(() -> {
try {
File result = compilePackage(folder, dimKey);
server.execute(() -> IrisModdedCommands.ok(source, "Package compiled: " + result.getAbsolutePath()));
} catch (Throwable e) {
LOGGER.error("Iris package failed for {}", dimKey, e);
server.execute(() -> IrisModdedCommands.fail(source, "Packaging failed: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage())));
}
}, "Iris Studio Package");
thread.setDaemon(true);
thread.start();
return 1;
}
private static File compilePackage(File packFolder, String dimKey) throws IOException {
IrisData dm = IrisData.get(packFolder);
IrisDimension dimension = dm.getDimensionLoader().load(dimKey);
if (dimension == null) {
throw new IOException("Pack '" + dimKey + "' has no dimensions/" + dimKey + ".json");
}
File exports = ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("exports").toFile();
File folder = new File(exports, dimension.getLoadKey());
IO.delete(folder);
folder.mkdirs();
LinkedHashSet<String> regionKeys = new LinkedHashSet<>();
LinkedHashSet<String> biomeKeys = new LinkedHashSet<>();
LinkedHashSet<String> entityKeys = new LinkedHashSet<>();
LinkedHashSet<String> spawnerKeys = new LinkedHashSet<>();
LinkedHashSet<String> generatorKeys = new LinkedHashSet<>();
LinkedHashSet<String> lootKeys = new LinkedHashSet<>();
LinkedHashSet<String> objectKeys = new LinkedHashSet<>();
regionKeys.addAll(dimension.getRegions());
lootKeys.addAll(dimension.getLoot().getTables());
spawnerKeys.addAll(dimension.getEntitySpawners());
for (String regionKey : regionKeys) {
IrisRegion region = dm.getRegionLoader().load(regionKey);
if (region == null) {
continue;
}
region.getAllBiomes(() -> dm).forEach((IrisBiome biome) -> {
if (biome != null && biome.getLoadKey() != null) {
biomeKeys.add(biome.getLoadKey());
}
});
lootKeys.addAll(region.getLoot().getTables());
spawnerKeys.addAll(region.getEntitySpawners());
}
for (String biomeKey : biomeKeys) {
IrisBiome biome = dm.getBiomeLoader().load(biomeKey);
if (biome == null) {
continue;
}
biome.getGenerators().forEach((IrisBiomeGeneratorLink link) -> generatorKeys.add(link.getGenerator()));
lootKeys.addAll(biome.getLoot().getTables());
spawnerKeys.addAll(biome.getEntitySpawners());
for (IrisObjectPlacement placement : biome.getObjects()) {
objectKeys.addAll(placement.getPlace());
}
}
for (String spawnerKey : spawnerKeys) {
IrisSpawner spawner = dm.getSpawnerLoader().load(spawnerKey);
if (spawner == null) {
continue;
}
spawner.getSpawns().forEach((IrisEntitySpawn spawn) -> entityKeys.add(spawn.getEntity()));
}
StringBuilder hashes = new StringBuilder();
for (String objectKey : objectKeys) {
try {
File objectFile = dm.getObjectLoader().findFile(objectKey);
IO.copyFile(objectFile, new File(folder, "objects/" + objectKey + ".iob"));
hashes.append(IO.hash(objectFile));
} catch (Throwable e) {
LOGGER.error("Iris package failed to copy object {}", objectKey, e);
}
}
hashes.append(copyJson(folder, "dimensions", dimension.getLoadKey(), dm.getDimensionLoader().findFile(dimension.getLoadKey())));
for (String key : generatorKeys) {
hashes.append(copyJson(folder, "generators", key, dm.getGeneratorLoader().findFile(key)));
}
for (String key : regionKeys) {
hashes.append(copyJson(folder, "regions", key, dm.getRegionLoader().findFile(key)));
}
for (String key : dm.getBlockLoader().getPossibleKeys()) {
hashes.append(copyJson(folder, "blocks", key, dm.getBlockLoader().findFile(key)));
}
for (String key : biomeKeys) {
hashes.append(copyJson(folder, "biomes", key, dm.getBiomeLoader().findFile(key)));
}
for (String key : entityKeys) {
hashes.append(copyJson(folder, "entities", key, dm.getEntityLoader().findFile(key)));
}
for (String key : lootKeys) {
hashes.append(copyJson(folder, "loot", key, dm.getLootLoader().findFile(key)));
}
JSONObject meta = new JSONObject();
meta.put("hash", IO.hash(hashes.toString()));
meta.put("time", M.ms());
meta.put("version", dimension.getVersion());
IO.writeAll(new File(folder, "package.json"), meta.toString(0));
File output = new File(exports, dimension.getLoadKey() + ".iris");
ZipUtil.pack(folder, output, 9);
IO.delete(folder);
return output;
}
private static String copyJson(File folder, String category, String key, File file) {
if (file == null || !file.isFile()) {
return "";
}
try {
String json = new JSONObject(IO.readAll(file)).toString(0);
IO.writeAll(new File(folder, category + "/" + key + ".json"), json);
return IO.hash(json);
} catch (Throwable e) {
LOGGER.error("Iris package failed to write {}/{}", category, key, e);
return "";
}
}
private static int regions(CommandSourceStack source, int radius) {
ServerPlayer player = source.getPlayer();
if (player == null) {
IrisModdedCommands.fail(source, "This command can only be used by players (sampling is centered on your position).");
return 0;
}
Engine engine = IrisModdedCommands.engineFor(source.getLevel());
if (engine == null) {
IrisModdedCommands.fail(source, "This dimension is not generated by Iris.");
return 0;
}
MinecraftServer server = source.getServer();
int blockX = player.blockPosition().getX();
int blockZ = player.blockPosition().getZ();
IrisModdedCommands.ok(source, "Sampling region distribution in " + (radius * 2) + "x" + (radius * 2) + " chunks around you...");
Thread thread = new Thread(() -> {
try {
int diameter = radius * 2;
int totalTasks = diameter * diameter;
KMap<String, AtomicInteger> counts = new KMap<>();
engine.getDimension().getRegions().forEach((String key) -> counts.put(key, new AtomicInteger(0)));
MultiBurst burst = new MultiBurst("Region Sampler");
BurstExecutor executor = burst.burst(totalTasks);
new Spiraler(diameter, diameter, (int x, int z) -> executor.queue(() -> {
IrisRegion region = engine.getRegion((x << 4) + 8, (z << 4) + 8);
counts.computeIfAbsent(region.getLoadKey(), (String key) -> new AtomicInteger(0)).incrementAndGet();
})).setOffset(blockX, blockZ).drain();
executor.complete();
burst.close();
server.execute(() -> counts.forEach((String key, AtomicInteger count) -> {
IrisRegion region = engine.getData().getRegionLoader().load(key);
String rarity = region == null ? "?" : String.valueOf(region.getRarity());
IrisModdedCommands.ok(source, key + ": rarity=" + rarity + " / " + Form.f((double) count.get() / totalTasks * 100, 2) + "%");
}));
} catch (Throwable e) {
LOGGER.error("Iris region sampling failed", e);
server.execute(() -> IrisModdedCommands.fail(source, "Region sampling failed: " + e.getClass().getSimpleName()));
}
}, "Iris Region Sampler");
thread.setDaemon(true);
thread.start();
return 1;
}
}
@@ -0,0 +1,268 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.modded.command;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.particles.DustParticleOptions;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.util.Unit;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.ItemLore;
import net.minecraft.world.level.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public final class ModdedWandService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final ConcurrentHashMap<UUID, Selection> SELECTIONS = new ConcurrentHashMap<>();
private static final String WAND_TAG = "iris_wand";
private static final String DUST_TAG = "iris_dust";
private static final int DRAW_INTERVAL_TICKS = 5;
private static final double DRAW_STEP = 0.5D;
private static final double DRAW_KEEP_CHANCE = 0.4D;
private static final int DRAW_POINT_CAP = 400;
private static final double DRAW_DISTANCE_SQUARED = 96.0D * 96.0D;
private static int tickCounter = 0;
private ModdedWandService() {
}
public record Selection(ResourceKey<Level> dimension, BlockPos first, BlockPos second) {
public boolean complete() {
return first != null && second != null;
}
public BlockPos min() {
return new BlockPos(Math.min(first.getX(), second.getX()), Math.min(first.getY(), second.getY()), Math.min(first.getZ(), second.getZ()));
}
public BlockPos max() {
return new BlockPos(Math.max(first.getX(), second.getX()), Math.max(first.getY(), second.getY()), Math.max(first.getZ(), second.getZ()));
}
}
public static ItemStack createWand() {
ItemStack stack = new ItemStack(Items.BLAZE_ROD);
stack.set(DataComponents.CUSTOM_NAME, Component.literal("Wand of Iris").withStyle(ChatFormatting.BOLD, ChatFormatting.GOLD));
stack.set(DataComponents.LORE, new ItemLore(List.of(
Component.literal("Left click a block to set the first corner"),
Component.literal("Right click a block to set the second corner"))));
stack.set(DataComponents.UNBREAKABLE, Unit.INSTANCE);
stack.set(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, Boolean.TRUE);
stack.set(DataComponents.CUSTOM_DATA, CustomData.of(flagTag(WAND_TAG)));
return stack;
}
public static ItemStack createDust() {
ItemStack stack = new ItemStack(Items.GLOWSTONE_DUST);
stack.set(DataComponents.CUSTOM_NAME, Component.literal("Dust of Revealing").withStyle(ChatFormatting.BOLD, ChatFormatting.YELLOW));
stack.set(DataComponents.LORE, new ItemLore(List.of(
Component.literal("Right click on a block to reveal it's placement structure!"))));
stack.set(DataComponents.UNBREAKABLE, Unit.INSTANCE);
stack.set(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, Boolean.TRUE);
stack.set(DataComponents.CUSTOM_DATA, CustomData.of(flagTag(DUST_TAG)));
return stack;
}
private static CompoundTag flagTag(String key) {
CompoundTag tag = new CompoundTag();
tag.putBoolean(key, true);
return tag;
}
public static boolean isWand(ItemStack stack) {
return hasFlag(stack, WAND_TAG);
}
public static boolean isDust(ItemStack stack) {
return hasFlag(stack, DUST_TAG);
}
private static boolean hasFlag(ItemStack stack, String key) {
if (stack == null || stack.isEmpty()) {
return false;
}
CustomData data = stack.get(DataComponents.CUSTOM_DATA);
return data != null && data.copyTag().getBooleanOr(key, false);
}
public static boolean isHoldingWand(ServerPlayer player) {
return isWand(player.getMainHandItem());
}
public static boolean attackBlock(Player player, Level level, InteractionHand hand, BlockPos pos) {
if (hand != InteractionHand.MAIN_HAND || level.isClientSide() || !(player instanceof ServerPlayer serverPlayer) || !(level instanceof ServerLevel serverLevel)) {
return false;
}
if (isWand(serverPlayer.getMainHandItem())) {
setCorner(serverPlayer, serverLevel, pos, true);
return true;
}
return false;
}
public static boolean useBlock(Player player, Level level, InteractionHand hand, BlockPos pos) {
if (hand != InteractionHand.MAIN_HAND || level.isClientSide() || !(player instanceof ServerPlayer serverPlayer) || !(level instanceof ServerLevel serverLevel)) {
return false;
}
ItemStack held = serverPlayer.getMainHandItem();
if (isWand(held)) {
setCorner(serverPlayer, serverLevel, pos, false);
return true;
}
if (isDust(held)) {
serverLevel.playSound(null, pos, SoundEvents.AMETHYST_BLOCK_CHIME, SoundSource.PLAYERS, 2.0F, 1.97F);
ModdedDustRevealer.reveal(serverPlayer, serverLevel, pos);
return true;
}
return false;
}
private static void setCorner(ServerPlayer player, ServerLevel level, BlockPos pos, boolean first) {
ResourceKey<Level> dimension = level.dimension();
BlockPos corner = pos.immutable();
SELECTIONS.compute(player.getUUID(), (UUID uuid, Selection existing) -> {
BlockPos other = existing != null && existing.dimension().equals(dimension) ? (first ? existing.second() : existing.first()) : null;
return first ? new Selection(dimension, corner, other) : new Selection(dimension, other, corner);
});
level.playSound(null, pos, SoundEvents.END_PORTAL_FRAME_FILL, SoundSource.PLAYERS, 1.0F, first ? 0.67F : 1.17F);
player.sendOverlayMessage(Component.literal("Position " + (first ? 1 : 2) + " set to " + corner.getX() + ", " + corner.getY() + ", " + corner.getZ()));
}
public static Selection selection(ServerPlayer player) {
Selection selection = SELECTIONS.get(player.getUUID());
if (selection == null || !selection.complete() || !selection.dimension().equals(player.level().dimension())) {
return null;
}
return selection;
}
public static void setSelection(ServerPlayer player, BlockPos first, BlockPos second) {
SELECTIONS.put(player.getUUID(), new Selection(player.level().dimension(), first.immutable(), second.immutable()));
}
public static void clearAll() {
SELECTIONS.clear();
}
public static void serverTick(MinecraftServer server) {
tickCounter++;
if (tickCounter % DRAW_INTERVAL_TICKS != 0) {
return;
}
try {
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
if (!isHoldingWand(player)) {
continue;
}
Selection selection = selection(player);
if (selection == null) {
continue;
}
draw(player.level(), player, selection);
}
} catch (Throwable e) {
LOGGER.error("Iris wand selection draw failed", e);
}
}
private static void draw(ServerLevel level, ServerPlayer player, Selection selection) {
BlockPos min = selection.min();
BlockPos max = selection.max();
double lowX = min.getX();
double lowY = min.getY();
double lowZ = min.getZ();
double highX = max.getX() + 1;
double highY = max.getY() + 1;
double highZ = max.getZ() + 1;
double[][] edges = {
{lowX, lowY, lowZ, highX, lowY, lowZ},
{lowX, lowY, lowZ, lowX, highY, lowZ},
{lowX, lowY, lowZ, lowX, lowY, highZ},
{highX, lowY, lowZ, highX, highY, lowZ},
{highX, lowY, lowZ, highX, lowY, highZ},
{lowX, highY, lowZ, highX, highY, lowZ},
{lowX, highY, lowZ, lowX, highY, highZ},
{lowX, lowY, highZ, highX, lowY, highZ},
{lowX, lowY, highZ, lowX, highY, highZ},
{highX, highY, lowZ, highX, highY, highZ},
{lowX, highY, highZ, highX, highY, highZ},
{highX, lowY, highZ, highX, highY, highZ}
};
ThreadLocalRandom random = ThreadLocalRandom.current();
double px = player.getX();
double py = player.getY();
double pz = player.getZ();
int sent = 0;
for (double[] edge : edges) {
double dx = edge[3] - edge[0];
double dy = edge[4] - edge[1];
double dz = edge[5] - edge[2];
double length = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (length <= 0) {
continue;
}
double ux = dx / length;
double uy = dy / length;
double uz = dz / length;
for (double d = 0; d <= length; d += DRAW_STEP) {
if (random.nextDouble() > DRAW_KEEP_CHANCE) {
continue;
}
double x = edge[0] + ux * d;
double y = edge[1] + uy * d;
double z = edge[2] + uz * d;
double distX = x - px;
double distY = y - py;
double distZ = z - pz;
if (distX * distX + distY * distY + distZ * distZ > DRAW_DISTANCE_SQUARED) {
continue;
}
float hue = (float) (0.5F + (Math.sin((x + y + z + (player.tickCount / 2.0F)) / 20.0F) / 2.0D));
Color color = Color.getHSBColor(hue, 1.0F, 1.0F);
DustParticleOptions options = new DustParticleOptions(color.getRGB() & 0xFFFFFF, 0.9F);
level.sendParticles(player, options, true, true, x, y, z, 1, 0.0D, 0.0D, 0.0D, 0.0D);
sent++;
if (sent >= DRAW_POINT_CAP) {
return;
}
}
}
}
}
+2
View File
@@ -177,6 +177,8 @@ tasks.named('shadowJar', ShadowJar).configure {
exclude('META-INF/*.DSA')
exclude('META-INF/*.RSA')
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
relocate('org.objectweb.asm', 'art.arcane.iris.shadow.asm')
exclude('oshi.properties')
}
tasks.named('assemble').configure {
@@ -23,8 +23,12 @@ import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedParityProbe;
import art.arcane.iris.modded.ModdedWorldCheck;
import art.arcane.iris.modded.ModdedWorldEngines;
import art.arcane.iris.modded.command.IrisModdedCommands;
import art.arcane.iris.modded.command.ModdedObjectUndo;
import art.arcane.iris.modded.command.ModdedWandService;
import com.mojang.serialization.MapCodec;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.ModContainer;
@@ -33,7 +37,10 @@ import net.neoforged.fml.common.Mod;
import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.fml.loading.VersionInfo;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import net.neoforged.neoforge.event.tick.ServerTickEvent;
import net.neoforged.neoforge.registries.DeferredRegister;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -44,6 +51,7 @@ 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");
@@ -58,7 +66,24 @@ public final class IrisNeoForgeBootstrap {
chunkGenerators.register(modBus);
LOGGER.info("Iris chunk generator registered as irisworldgen:iris");
NeoForge.EVENT_BUS.addListener((ServerStoppingEvent event) -> ModdedWorldEngines.shutdown());
NeoForge.EVENT_BUS.addListener((ServerStoppingEvent event) -> {
ModdedObjectUndo.clearAll();
ModdedWandService.clearAll();
ModdedWorldEngines.shutdown();
});
NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> IrisModdedCommands.register(event.getDispatcher()));
NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.LeftClickBlock event) -> {
if (ModdedWandService.attackBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos())) {
event.setCanceled(true);
}
});
NeoForge.EVENT_BUS.addListener((PlayerInteractEvent.RightClickBlock event) -> {
if (ModdedWandService.useBlock(event.getEntity(), event.getLevel(), event.getHand(), event.getPos())) {
event.setCanceled(true);
event.setCancellationResult(InteractionResult.SUCCESS);
}
});
NeoForge.EVENT_BUS.addListener((ServerTickEvent.Post event) -> ModdedWandService.serverTick(event.getServer()));
String parity = System.getProperty("iris.parity");
if (parity != null) {
@@ -40,6 +40,13 @@ public final class NeoForgeModdedLoader implements ModdedLoader {
return FMLLoader.getCurrent().getVersionInfo().mcVersion();
}
@Override
public String modVersion() {
return ModList.get().getModContainerById("irisworldgen")
.map((net.neoforged.fml.ModContainer container) -> container.getModInfo().getVersion().toString())
.orElse("unknown");
}
@Override
public MinecraftServer currentServer() {
return ServerLifecycleHooks.getCurrentServer();
@@ -19,10 +19,9 @@
package art.arcane.iris.core.structure;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.util.project.matter.IrisMatterSupport;
import art.arcane.volmlib.util.collection.KList;
import com.google.gson.GsonBuilder;
import java.io.File;
@@ -64,8 +63,8 @@ public final class StructureIndexService {
}
public static File write(IrisData data) {
KList<String> structures = INMS.get().getStructureKeys();
KList<String> sets = INMS.get().getStructureSetKeys();
List<String> structures = IrisPlatforms.get().structureHooks().structureKeys();
List<String> sets = IrisPlatforms.get().structureHooks().structureSetKeys();
List<String> vanilla = new ArrayList<>();
List<String> datapack = new ArrayList<>();
@@ -27,14 +27,13 @@ import art.arcane.iris.engine.object.IrisStructurePlacement;
import art.arcane.iris.engine.object.StructureDistribution;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
/**
* Finds where IRIS_PLACED structures generate. A structure key matches either the iris
@@ -51,7 +50,7 @@ import java.util.WeakHashMap;
* exhaustive per-chunk ring scan.
*/
public final class IrisStructureLocator {
private static final Map<IrisData, PlacementIndex> INDEX_CACHE = Collections.synchronizedMap(new WeakHashMap<>());
private static final com.github.benmanes.caffeine.cache.Cache<IrisData, PlacementIndex> INDEX_CACHE = Caffeine.newBuilder().weakKeys().build();
private IrisStructureLocator() {
}
@@ -280,13 +279,7 @@ public final class IrisStructureLocator {
private static PlacementIndex index(Engine engine) {
IrisData data = engine.getData();
PlacementIndex cached = INDEX_CACHE.get(data);
if (cached != null) {
return cached;
}
PlacementIndex built = build(engine, data);
INDEX_CACHE.put(data, built);
return built;
return INDEX_CACHE.get(data, (IrisData keyData) -> build(engine, keyData));
}
private static PlacementIndex build(Engine engine, IrisData data) {