mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 13:30:08 +00:00
d
This commit is contained in:
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);
|
||||
|
||||
+97
@@ -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()) {
|
||||
|
||||
Reference in new issue
Block a user