This commit is contained in:
Brian Neumann-Fopiano committed 2026-07-08 17:55:14 -04:00
1 parent 848e056bb8
commit d1fec88769
23 files changed
+1189 -149

No files matched your search

@@ -0,0 +1,199 @@
/*
* 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 java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
public final class MainWorldService {
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
private static final String PRESET_NAMESPACE = "irisworldgen";
private static final String MARKER_NAME = "mainworld.pending";
private static final String[] VANILLA_DIMENSION_FOLDERS = {
"region",
"entities",
"poi",
"mantle",
"dimensions/minecraft/overworld",
"dimensions/minecraft/the_nether",
"dimensions/minecraft/the_end",
"DIM-1",
"DIM1"
};
private MainWorldService() {
}
public static String presetIdFor(String packRef) {
String value = packRef.trim();
int colon = value.indexOf(':');
String pack = colon >= 0 ? value.substring(0, colon) : value;
String dimension = colon >= 0 ? value.substring(colon + 1) : value;
String presetKey = dimension.equals(pack) ? pack : pack + "_" + dimension;
return PRESET_NAMESPACE + ":" + presetKey;
}
public static void reconcileEarly() {
try {
ModdedModConfig config = ModdedModConfig.get();
String pack = config.mainWorldPack();
if (pack == null || pack.isBlank()) {
return;
}
Path properties = instanceRoot().resolve("server.properties");
String target = presetIdFor(pack);
String currentType = readProperty(properties, "level-type");
if (!target.equals(currentType)) {
writeLevelProperties(properties, target, config.mainWorldSeed());
markPending();
LOGGER.warn("Iris main world '{}' staged: server.properties level-type set to {}. Restart again to generate it (this boot still uses the previous overworld; player data is kept).", pack, target);
return;
}
if (!isPending()) {
return;
}
String levelName = firstNonBlank(readProperty(properties, "level-name"), "world");
wipeVanillaDimensions(instanceRoot().resolve(levelName));
clearPending();
LOGGER.warn("Iris main world '{}' generated fresh: cleared the previous overworld/nether/end so this boot regenerates them as {} (player data kept).", pack, target);
} catch (Throwable e) {
LOGGER.error("Iris main world reconciliation failed", e);
}
}
public static boolean stage(String packRef, long seed) {
try {
Path properties = instanceRoot().resolve("server.properties");
writeLevelProperties(properties, presetIdFor(packRef), seed);
markPending();
return true;
} catch (IOException e) {
LOGGER.error("Iris failed to stage the main world in server.properties", e);
return false;
}
}
public static void clearOverride() {
try {
clearPending();
} catch (IOException e) {
LOGGER.error("Iris failed to clear the pending main world marker", e);
}
}
private static Path instanceRoot() {
return ModdedEngineBootstrap.loader().configDir().getParent();
}
private static Path markerFile() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve(MARKER_NAME);
}
private static boolean isPending() {
return Files.isRegularFile(markerFile());
}
private static void markPending() throws IOException {
Path marker = markerFile();
Files.createDirectories(marker.getParent());
Files.writeString(marker, "pending", StandardCharsets.UTF_8);
}
private static void clearPending() throws IOException {
Files.deleteIfExists(markerFile());
}
private static String readProperty(Path properties, String key) throws IOException {
if (!Files.isRegularFile(properties)) {
return null;
}
List<String> lines = Files.readAllLines(properties, StandardCharsets.UTF_8);
String prefix = key + "=";
for (String line : lines) {
if (line.startsWith(prefix)) {
return unescape(line.substring(prefix.length()).trim());
}
}
return null;
}
private static void writeLevelProperties(Path properties, String target, long seed) throws IOException {
List<String> lines = Files.isRegularFile(properties)
? new ArrayList<>(Files.readAllLines(properties, StandardCharsets.UTF_8))
: new ArrayList<>();
setProperty(lines, "level-type", escape(target));
if (seed != 0L) {
setProperty(lines, "level-seed", Long.toString(seed));
}
Files.write(properties, lines, StandardCharsets.UTF_8);
}
private static void setProperty(List<String> lines, String key, String value) {
String prefix = key + "=";
for (int i = 0; i < lines.size(); i++) {
if (lines.get(i).startsWith(prefix)) {
lines.set(i, prefix + value);
return;
}
}
lines.add(prefix + value);
}
private static void wipeVanillaDimensions(Path worldRoot) throws IOException {
Files.deleteIfExists(worldRoot.resolve("level.dat"));
Files.deleteIfExists(worldRoot.resolve("level.dat_old"));
for (String folder : VANILLA_DIMENSION_FOLDERS) {
deleteRecursively(worldRoot.resolve(folder));
}
}
private static void deleteRecursively(Path path) throws IOException {
if (!Files.exists(path)) {
return;
}
List<Path> entries = new ArrayList<>();
try (Stream<Path> walk = Files.walk(path)) {
walk.sorted(Comparator.comparingInt(Path::getNameCount).reversed()).forEach(entries::add);
}
for (Path entry : entries) {
Files.deleteIfExists(entry);
}
}
private static String escape(String value) {
return value.replace(":", "\\:");
}
private static String unescape(String value) {
return value.replace("\\:", ":").replace("\\=", "=");
}
private static String firstNonBlank(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value;
}
}
@@ -116,6 +116,7 @@ public final class ModdedEngineBootstrap {
ModdedIrisLog.info("Iris " + moddedLoader.modVersion() + " bootstrapping on Minecraft " + moddedLoader.minecraftVersion() + " (" + loaderDescription + ")");
selfTest(moddedLoader.getClass().getClassLoader());
bind();
MainWorldService.reconcileEarly();
chunkGeneratorRegistration.run();
ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
armParityProbe();
@@ -36,12 +36,19 @@ public final class ModdedModConfig {
private final boolean autoDownloadDefaultPack;
private final String primaryWorld;
private final boolean routePlayersToPrimaryWorld;
private final String mainWorldPack;
private final long mainWorldSeed;
private final boolean mainWorldAutoRestart;
private ModdedModConfig(String defaultPack, boolean autoDownloadDefaultPack, String primaryWorld, boolean routePlayersToPrimaryWorld) {
private ModdedModConfig(String defaultPack, boolean autoDownloadDefaultPack, String primaryWorld, boolean routePlayersToPrimaryWorld,
String mainWorldPack, long mainWorldSeed, boolean mainWorldAutoRestart) {
this.defaultPack = defaultPack;
this.autoDownloadDefaultPack = autoDownloadDefaultPack;
this.primaryWorld = primaryWorld == null ? "" : primaryWorld.trim();
this.routePlayersToPrimaryWorld = routePlayersToPrimaryWorld;
this.mainWorldPack = mainWorldPack == null ? "" : mainWorldPack.trim();
this.mainWorldSeed = mainWorldSeed;
this.mainWorldAutoRestart = mainWorldAutoRestart;
}
public static ModdedModConfig get() {
@@ -61,7 +68,18 @@ public final class ModdedModConfig {
public static void setPrimaryWorld(String dimensionId) {
synchronized (LOCK) {
ModdedModConfig current = get();
ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.autoDownloadDefaultPack, dimensionId, current.routePlayersToPrimaryWorld);
ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.autoDownloadDefaultPack, dimensionId, current.routePlayersToPrimaryWorld,
current.mainWorldPack, current.mainWorldSeed, current.mainWorldAutoRestart);
instance = updated;
write(configFile(), updated);
}
}
public static void setMainWorld(String packRef, long seed) {
synchronized (LOCK) {
ModdedModConfig current = get();
ModdedModConfig updated = new ModdedModConfig(current.defaultPack, current.autoDownloadDefaultPack, current.primaryWorld, current.routePlayersToPrimaryWorld,
packRef == null ? "" : packRef.trim(), seed, current.mainWorldAutoRestart);
instance = updated;
write(configFile(), updated);
}
@@ -83,13 +101,25 @@ public final class ModdedModConfig {
return routePlayersToPrimaryWorld;
}
public String mainWorldPack() {
return mainWorldPack;
}
public long mainWorldSeed() {
return mainWorldSeed;
}
public boolean mainWorldAutoRestart() {
return mainWorldAutoRestart;
}
private static Path configFile() {
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("modded.json");
}
private static ModdedModConfig load() {
Path file = configFile();
ModdedModConfig defaults = new ModdedModConfig("overworld", true, "", true);
ModdedModConfig defaults = new ModdedModConfig("overworld", true, "", true, "", 0L, false);
if (!Files.isRegularFile(file)) {
write(file, defaults);
return defaults;
@@ -100,7 +130,10 @@ public final class ModdedModConfig {
json.optString("defaultPack", defaults.defaultPack),
json.optBoolean("autoDownloadDefaultPack", defaults.autoDownloadDefaultPack),
json.optString("primaryWorld", defaults.primaryWorld),
json.optBoolean("routePlayersToPrimaryWorld", defaults.routePlayersToPrimaryWorld));
json.optBoolean("routePlayersToPrimaryWorld", defaults.routePlayersToPrimaryWorld),
json.optString("mainWorldPack", defaults.mainWorldPack),
json.optLong("mainWorldSeed", defaults.mainWorldSeed),
json.optBoolean("mainWorldAutoRestart", defaults.mainWorldAutoRestart));
} catch (RuntimeException | IOException e) {
LOGGER.error("Iris modded config at {} is invalid; using defaults", file, e);
return defaults;
@@ -113,6 +146,9 @@ public final class ModdedModConfig {
json.put("autoDownloadDefaultPack", config.autoDownloadDefaultPack);
json.put("primaryWorld", config.primaryWorld);
json.put("routePlayersToPrimaryWorld", config.routePlayersToPrimaryWorld);
json.put("mainWorldPack", config.mainWorldPack);
json.put("mainWorldSeed", config.mainWorldSeed);
json.put("mainWorldAutoRestart", config.mainWorldAutoRestart);
try {
Files.createDirectories(file.getParent());
Files.writeString(file, json.toString(4), StandardCharsets.UTF_8);
@@ -23,6 +23,7 @@ import art.arcane.iris.core.pack.PackValidationRegistry;
import art.arcane.iris.core.pack.PackValidationResult;
import art.arcane.iris.engine.object.IrisDimension;
import art.arcane.iris.modded.IrisModdedChunkGenerator;
import art.arcane.iris.modded.MainWorldService;
import art.arcane.iris.modded.ModdedDimensionManager;
import art.arcane.iris.modded.ModdedEngineBootstrap;
import art.arcane.iris.modded.ModdedModConfig;
@@ -75,6 +76,7 @@ public final class ModdedWorldCommands {
root.then(enableTree("enable"));
root.then(enableTree("create"));
root.then(replaceOverworldTree());
root.then(mainWorldTree());
root.then(disableTree());
root.then(deleteTree("delete"));
@@ -123,6 +125,20 @@ public final class ModdedWorldCommands {
StringArgumentType.getString(context, "seed")))));
}
private static LiteralArgumentBuilder<CommandSourceStack> mainWorldTree() {
return Commands.literal("mainworld")
.then(Commands.literal("off")
.executes((CommandContext<CommandSourceStack> context) -> clearMainWorld(context.getSource())))
.then(Commands.argument("pack", StringArgumentType.string()).suggests(IrisModdedCommands.PACK_NAMES)
.executes((CommandContext<CommandSourceStack> context) -> mainWorld(context.getSource(),
StringArgumentType.getString(context, "pack"),
null))
.then(Commands.argument("seed", StringArgumentType.word())
.executes((CommandContext<CommandSourceStack> context) -> mainWorld(context.getSource(),
StringArgumentType.getString(context, "pack"),
StringArgumentType.getString(context, "seed")))));
}
public static int createWorld(CommandSourceStack source, String name, String pack, long seed) {
String[] packRef = parsePackRef(pack);
return enable(source, name, packRef[0], packRef[1], seed);
@@ -223,6 +239,87 @@ public final class ModdedWorldCommands {
return 1;
}
private static int clearMainWorld(CommandSourceStack source) {
ModdedModConfig.setMainWorld("", 0L);
MainWorldService.clearOverride();
IrisModdedCommands.ok(source, "Iris main world override cleared. The overworld keeps its current generator; edit server.properties level-type and restart to change it back.");
return 1;
}
private static int mainWorld(CommandSourceStack source, String packRaw, String seedRaw) {
MinecraftServer server = source.getServer();
long seed;
if (seedRaw == null || seedRaw.isBlank()) {
seed = 0L;
} else if (seedRaw.equalsIgnoreCase("random")) {
long rolled = ThreadLocalRandom.current().nextLong();
seed = rolled == 0L ? 1L : rolled;
} else {
try {
seed = Long.parseLong(seedRaw.trim());
} catch (NumberFormatException e) {
IrisModdedCommands.fail(source, "Invalid seed '" + seedRaw + "'. Use a number or 'random'.");
return 0;
}
}
String[] packRef = parsePackRef(packRaw);
String pack = packRef[0];
String packDimension = packRef[1];
if (!validPackRef(source, pack, packDimension)) {
return 0;
}
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
if (packFolder.isDirectory()) {
return applyMainWorld(source, pack, packDimension, packRaw, seed);
}
IrisModdedCommands.ok(source, "Pack '" + pack + "' is not installed; downloading IrisDimensions/" + pack + "...");
Thread thread = new Thread(() -> {
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, "master",
(String line) -> server.execute(() -> IrisModdedCommands.ok(source, line)));
server.execute(() -> {
if (!installed || !packFolder.isDirectory()) {
IrisModdedCommands.fail(source, "Pack '" + pack + "' could not be downloaded; check the name or install it with /iris download " + pack + ".");
return;
}
applyMainWorld(source, pack, packDimension, packRaw, seed);
});
}, "Iris Main World Pack Download");
thread.setDaemon(true);
thread.start();
return 1;
}
private static int applyMainWorld(CommandSourceStack source, String pack, String packDimension, String packRef, long seed) {
try {
if (!loadPackDimension(source, pack, packDimension)) {
return 0;
}
} catch (Throwable e) {
LOGGER.error("Iris main world pack load failed for {} (dim={})", pack, packDimension, e);
IrisModdedCommands.fail(source, "Pack '" + pack + "' is not ready yet (still loading or validating). Try the command again in a moment.");
return 0;
}
if (blockIfPackBroken(source, "the main world", pack)) {
return 0;
}
ModdedModConfig.setMainWorld(packRef, seed);
if (!MainWorldService.stage(packRef, seed)) {
IrisModdedCommands.fail(source, "Failed to write server.properties; check file permissions and set level-type manually.");
return 0;
}
String preset = MainWorldService.presetIdFor(packRef);
IrisModdedCommands.ok(source, "Iris main world set to '" + pack + "' (preset " + preset + ", seed " + (seed == 0L ? "random" : Long.toString(seed)) + ").");
IrisModdedCommands.ok(source, "server.properties level-type is now " + preset + ". On the next restart the overworld, nether, and end regenerate as this Iris world.");
IrisModdedCommands.ok(source, "Player data (inventories, advancements, stats) is kept; existing terrain in those dimensions is replaced. This applies once.");
if (ModdedModConfig.get().mainWorldAutoRestart()) {
IrisModdedCommands.ok(source, "mainWorldAutoRestart is enabled - stopping the server now so your restart wrapper brings it back on the new main world.");
source.getServer().halt(false);
} else {
IrisModdedCommands.ok(source, "Restart the server now to generate it. (Set mainWorldAutoRestart=true in modded.json to have Iris stop the server for you.)");
}
return 1;
}
private static boolean blockIfPackBroken(CommandSourceStack source, String dimensionId, String pack) {
PackValidationResult validation = PackValidationRegistry.get(pack);
if (validation == null || validation.isLoadable()) {