mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-23 23:31:00 +00:00
iggy
This commit is contained in:
+49
-9
@@ -19,6 +19,7 @@
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
@@ -64,6 +65,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -76,28 +78,39 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
).apply(instance, IrisModdedChunkGenerator::new));
|
||||
|
||||
private final String dimensionKey;
|
||||
private final String defaultPack;
|
||||
private final String defaultDimensionKey;
|
||||
private final ConcurrentHashMap<String, Holder<Biome>> biomeHolders = new ConcurrentHashMap<>();
|
||||
private final Set<String> missingBiomeWarnings = ConcurrentHashMap.newKeySet();
|
||||
private final AtomicBoolean announced = new AtomicBoolean(false);
|
||||
private volatile Engine engine;
|
||||
private volatile String activePackKey;
|
||||
private volatile String activePack;
|
||||
private volatile String activeDimensionKey;
|
||||
private volatile long seedOverride = Long.MIN_VALUE;
|
||||
private volatile long lastChunkGenAt = 0L;
|
||||
|
||||
public IrisModdedChunkGenerator(BiomeSource biomeSource, String dimensionKey) {
|
||||
super(biomeSource);
|
||||
this.dimensionKey = dimensionKey;
|
||||
this.activePackKey = dimensionKey;
|
||||
int colon = dimensionKey.indexOf(':');
|
||||
this.defaultPack = colon >= 0 ? dimensionKey.substring(0, colon) : dimensionKey;
|
||||
this.defaultDimensionKey = colon >= 0 ? dimensionKey.substring(colon + 1) : dimensionKey;
|
||||
this.activePack = defaultPack;
|
||||
this.activeDimensionKey = defaultDimensionKey;
|
||||
}
|
||||
|
||||
public synchronized void repoint(String packKey, long seed) {
|
||||
public synchronized void repoint(String pack, String packDimensionKey, long seed) {
|
||||
ServerLevel level = boundLevel();
|
||||
if (level != null) {
|
||||
ModdedWorldEngines.evict(level);
|
||||
}
|
||||
this.activePackKey = packKey;
|
||||
this.activePack = pack;
|
||||
this.activeDimensionKey = packDimensionKey;
|
||||
this.seedOverride = seed;
|
||||
this.engine = null;
|
||||
this.announced.set(false);
|
||||
this.biomeHolders.clear();
|
||||
this.missingBiomeWarnings.clear();
|
||||
}
|
||||
|
||||
public synchronized void unbindEngine() {
|
||||
@@ -108,14 +121,19 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
this.engine = null;
|
||||
this.announced.set(false);
|
||||
this.biomeHolders.clear();
|
||||
this.missingBiomeWarnings.clear();
|
||||
}
|
||||
|
||||
public synchronized void resetToDefault() {
|
||||
repoint(dimensionKey, Long.MIN_VALUE);
|
||||
repoint(defaultPack, defaultDimensionKey, Long.MIN_VALUE);
|
||||
}
|
||||
|
||||
public String activePackKey() {
|
||||
return activePackKey;
|
||||
public String activePack() {
|
||||
return activePack;
|
||||
}
|
||||
|
||||
public String activeDimensionKey() {
|
||||
return activeDimensionKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -149,7 +167,7 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
if (level == null) {
|
||||
throw new IllegalStateException("Iris generator '" + dimensionKey + "' has no bound ServerLevel yet");
|
||||
}
|
||||
Engine created = ModdedWorldEngines.get(level, activePackKey, seedOverride);
|
||||
Engine created = ModdedWorldEngines.get(level, activePack, activeDimensionKey, seedOverride);
|
||||
engine = created;
|
||||
return created;
|
||||
}
|
||||
@@ -179,10 +197,20 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
return engine();
|
||||
}
|
||||
|
||||
public long lastChunkGenAt() {
|
||||
return lastChunkGenAt;
|
||||
}
|
||||
|
||||
public void onHotload() {
|
||||
biomeHolders.clear();
|
||||
missingBiomeWarnings.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) {
|
||||
Engine generationEngine = engine();
|
||||
ChunkPos pos = chunk.getPos();
|
||||
lastChunkGenAt = System.currentTimeMillis();
|
||||
if (announced.compareAndSet(false, true)) {
|
||||
LOGGER.info("Iris generating {} through IrisModdedChunkGenerator (dim={} first chunk {},{})",
|
||||
dimensionKey, generationEngine.getDimension().getLoadKey(), pos.x(), pos.z());
|
||||
@@ -197,6 +225,13 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
|
||||
try {
|
||||
generationEngine.generate(pos.getMinBlockX(), pos.getMinBlockZ(), blocks, biomes, false);
|
||||
} catch (GenerationSessionException e) {
|
||||
if (e.isExpectedTeardown()) {
|
||||
LOGGER.debug("Iris chunk {},{} skipped: engine sealed for hotload/teardown", pos.x(), pos.z());
|
||||
return CompletableFuture.completedFuture(chunk);
|
||||
}
|
||||
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
|
||||
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to generate chunk {},{}", pos.x(), pos.z(), e);
|
||||
throw new IllegalStateException("Iris generation failed for chunk " + pos.x() + "," + pos.z(), e);
|
||||
@@ -232,7 +267,12 @@ public final class IrisModdedChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
Identifier identifier = Identifier.tryParse(key);
|
||||
Optional<Holder.Reference<Biome>> reference = identifier == null ? Optional.empty() : registry.get(identifier);
|
||||
Holder<Biome> resolved = reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElseGet(() -> fallbackBiome(registry));
|
||||
Holder<Biome> resolved = reference.<Holder<Biome>>map((Holder.Reference<Biome> value) -> value).orElseGet(() -> {
|
||||
if (missingBiomeWarnings.size() < 256 && missingBiomeWarnings.add(key)) {
|
||||
LOGGER.warn("Iris biome '{}' (pack {}) is not in the biome registry; writing minecraft:plains. Restart so the forced datapack registers the pack biomes.", key, activePack);
|
||||
}
|
||||
return fallbackBiome(registry);
|
||||
});
|
||||
Holder<Biome> raced = biomeHolders.putIfAbsent(key, resolved);
|
||||
return raced != null ? raced : resolved;
|
||||
}
|
||||
|
||||
@@ -98,11 +98,11 @@ public final class ModdedBiomeWriter implements PlatformBiomeWriter {
|
||||
if (owner == null) {
|
||||
return -1;
|
||||
}
|
||||
org.bukkit.block.Biome derivative = owner.getVanillaDerivative();
|
||||
if (derivative == null || derivative.getKey() == null) {
|
||||
String derivativeKey = owner.getVanillaDerivativeKey();
|
||||
if (derivativeKey == null) {
|
||||
return -1;
|
||||
}
|
||||
return idForKey(registry, derivative.getKey().toString());
|
||||
return idForKey(registry, derivativeKey);
|
||||
}
|
||||
|
||||
private IrisBiome findCustomBiomeOwner(String dimensionLoadKey, String customBiomeId) {
|
||||
|
||||
+98
-32
@@ -18,7 +18,10 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
@@ -28,7 +31,9 @@ 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.server.level.TicketType;
|
||||
import net.minecraft.world.entity.Relative;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
@@ -45,9 +50,13 @@ import net.minecraft.world.level.storage.WorldData;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@@ -55,6 +64,8 @@ public final class ModdedDimensionManager {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final Object LOCK = new Object();
|
||||
private static final ConcurrentHashMap<String, Handle> HANDLES = new ConcurrentHashMap<>();
|
||||
private static final Set<String> TYPE_FALLBACK_WARNINGS = ConcurrentHashMap.newKeySet();
|
||||
private static final TicketType TELEPORT_WARM_TICKET = new TicketType(TicketType.NO_TIMEOUT, TicketType.FLAG_LOADING);
|
||||
private static volatile ModdedServerAccess access;
|
||||
|
||||
private ModdedDimensionManager() {
|
||||
@@ -64,6 +75,10 @@ public final class ModdedDimensionManager {
|
||||
access = serverAccess;
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
HANDLES.clear();
|
||||
}
|
||||
|
||||
public static Handle handle(String dimensionId) {
|
||||
return HANDLES.get(dimensionId);
|
||||
}
|
||||
@@ -97,14 +112,16 @@ public final class ModdedDimensionManager {
|
||||
return generator.commandEngine();
|
||||
}
|
||||
|
||||
public static Handle create(MinecraftServer server, String dimensionId, String packKey, long seed) {
|
||||
public static Handle create(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
||||
ModdedServerAccess serverAccess = requireAccess();
|
||||
synchronized (LOCK) {
|
||||
ResourceKey<Level> key = levelKey(dimensionId);
|
||||
Handle existing = HANDLES.get(dimensionId);
|
||||
if (existing != null && serverAccess.hasLevel(server, key)) {
|
||||
existing.generator().repoint(packKey, seed);
|
||||
return existing;
|
||||
existing.generator().repoint(pack, packDimensionKey, seed);
|
||||
Handle refreshed = new Handle(dimensionId, pack, packDimensionKey, seed, existing.level(), existing.generator());
|
||||
HANDLES.put(dimensionId, refreshed);
|
||||
return refreshed;
|
||||
}
|
||||
if (serverAccess.hasLevel(server, key)) {
|
||||
ServerLevel present = level(server, dimensionId);
|
||||
@@ -112,40 +129,36 @@ public final class ModdedDimensionManager {
|
||||
throw new IllegalStateException("Iris cannot inject dimension '" + dimensionId + "': a non-Iris level with that id is already loaded");
|
||||
}
|
||||
LOGGER.warn("Iris dimension '{}' is already present in the running server; reusing it", dimensionId);
|
||||
generator.repoint(packKey, seed);
|
||||
Handle handle = new Handle(dimensionId, packKey, seed, present, generator);
|
||||
generator.repoint(pack, packDimensionKey, seed);
|
||||
Handle handle = new Handle(dimensionId, pack, packDimensionKey, seed, present, generator);
|
||||
HANDLES.put(dimensionId, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
try {
|
||||
Handle handle = inject(server, serverAccess, dimensionId, key, packKey, seed);
|
||||
Handle handle = inject(server, serverAccess, dimensionId, key, pack, packDimensionKey, seed);
|
||||
HANDLES.put(dimensionId, handle);
|
||||
LOGGER.info("Iris injected runtime dimension '{}' (pack={} seed={})", dimensionId, packKey, seed);
|
||||
LOGGER.info("Iris injected runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed);
|
||||
return handle;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to inject runtime dimension '{}' (pack={} seed={})", dimensionId, packKey, seed, e);
|
||||
LOGGER.error("Iris failed to inject runtime dimension '{}' (pack={} dim={} seed={})", dimensionId, pack, packDimensionKey, seed, e);
|
||||
throw new IllegalStateException("Iris runtime dimension injection failed for " + dimensionId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Handle createPersistent(MinecraftServer server, String dimensionId, String packKey, long seed) {
|
||||
Handle handle = create(server, dimensionId, packKey, seed);
|
||||
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, packKey, seed));
|
||||
public static Handle createPersistent(MinecraftServer server, String dimensionId, String pack, String packDimensionKey, long seed) {
|
||||
Handle handle = create(server, dimensionId, pack, packDimensionKey, seed);
|
||||
ModdedDimensionRegistryStore.put(server, new ModdedDimensionRegistryStore.PersistentDimension(dimensionId, pack, packDimensionKey, seed));
|
||||
return handle;
|
||||
}
|
||||
|
||||
public static boolean removePersistent(MinecraftServer server, String dimensionId) {
|
||||
boolean removed = remove(server, dimensionId);
|
||||
public static boolean removePersistent(MinecraftServer server, String dimensionId, boolean wipeStorage) {
|
||||
boolean removed = remove(server, dimensionId, wipeStorage);
|
||||
ModdedDimensionRegistryStore.remove(server, dimensionId);
|
||||
return removed;
|
||||
}
|
||||
|
||||
public static boolean remove(MinecraftServer server, String dimensionId) {
|
||||
return remove(server, dimensionId, false);
|
||||
}
|
||||
|
||||
public static boolean remove(MinecraftServer server, String dimensionId, boolean wipeStorage) {
|
||||
ModdedServerAccess serverAccess = requireAccess();
|
||||
synchronized (LOCK) {
|
||||
@@ -153,6 +166,9 @@ public final class ModdedDimensionManager {
|
||||
ServerLevel level = level(server, dimensionId);
|
||||
if (level == null) {
|
||||
HANDLES.remove(dimensionId);
|
||||
if (wipeStorage) {
|
||||
ModdedDimensionStorage.wipe(server, key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
@@ -184,29 +200,78 @@ public final class ModdedDimensionManager {
|
||||
}
|
||||
int blockX = (int) Math.floor(x);
|
||||
int blockZ = (int) Math.floor(z);
|
||||
ChunkPos chunkPos = new ChunkPos(blockX >> 4, blockZ >> 4);
|
||||
if (level.getChunkSource().hasChunk(chunkPos.x(), chunkPos.z())) {
|
||||
completeTeleport(player, level, x, y, z, blockX, blockZ);
|
||||
return true;
|
||||
}
|
||||
UUID playerId = player.getUUID();
|
||||
CompletableFuture
|
||||
.supplyAsync(() -> level.getChunkSource().addTicketAndLoadWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1), server)
|
||||
.thenCompose((CompletableFuture<?> inner) -> inner)
|
||||
.whenComplete((Object result, Throwable error) -> server.execute(() -> {
|
||||
level.getChunkSource().removeTicketWithRadius(TELEPORT_WARM_TICKET, chunkPos, 1);
|
||||
if (error != null) {
|
||||
LOGGER.warn("Iris chunk warm for teleport into '{}' at {},{} failed: {}", dimensionId, chunkPos.x(), chunkPos.z(), error.toString());
|
||||
}
|
||||
ServerPlayer target = server.getPlayerList().getPlayer(playerId);
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
completeTeleport(target, level, x, y, z, blockX, blockZ);
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void completeTeleport(ServerPlayer player, ServerLevel level, double x, double y, double z, int blockX, int blockZ) {
|
||||
double targetY = y;
|
||||
if (y == Double.MIN_VALUE) {
|
||||
targetY = level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ);
|
||||
}
|
||||
player.teleportTo(level, x, targetY, z, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess) {
|
||||
private static Holder<DimensionType> resolveDimensionType(RegistryAccess registryAccess, String pack, String packDimensionKey) {
|
||||
Registry<DimensionType> registry = registryAccess.lookupOrThrow(Registries.DIMENSION_TYPE);
|
||||
IrisDimension dimension = loadPackDimension(pack, packDimensionKey);
|
||||
if (dimension != null) {
|
||||
String typeRef = ModdedForcedDatapack.dimensionTypeRef(dimension);
|
||||
ResourceKey<DimensionType> typeKey = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse(typeRef));
|
||||
Optional<Holder.Reference<DimensionType>> packType = registry.get(typeKey);
|
||||
if (packType.isPresent()) {
|
||||
return packType.get();
|
||||
}
|
||||
if (TYPE_FALLBACK_WARNINGS.add(typeRef)) {
|
||||
LOGGER.warn("Iris dimension type {} (pack {} dim {}) is not registered yet; injecting with fallback heights. Restart the server so the forced datapack installs it.", typeRef, pack, packDimensionKey);
|
||||
}
|
||||
}
|
||||
ResourceKey<DimensionType> studioPool = ResourceKey.create(Registries.DIMENSION_TYPE, Identifier.parse("irisworldgen:studio_pool"));
|
||||
return registry.get(studioPool)
|
||||
.map(reference -> (Holder<DimensionType>) reference)
|
||||
.orElseGet(() -> registry.getOrThrow(BuiltinDimensionTypes.OVERWORLD));
|
||||
}
|
||||
|
||||
private static Handle inject(MinecraftServer server, ModdedServerAccess serverAccess, String dimensionId, ResourceKey<Level> key, String packKey, long seed) {
|
||||
private static IrisDimension loadPackDimension(String pack, String packDimensionKey) {
|
||||
try {
|
||||
File packFolder = ModdedWorldEngines.packFolder(pack);
|
||||
if (!packFolder.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
return IrisData.get(packFolder).getDimensionLoader().load(packDimensionKey);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.warn("Iris could not load pack '{}' dimension '{}' for dimension type resolution: {}", pack, packDimensionKey, e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Handle inject(MinecraftServer server, ModdedServerAccess serverAccess, String dimensionId, ResourceKey<Level> key, String pack, String packDimensionKey, long seed) {
|
||||
RegistryAccess registryAccess = server.registryAccess();
|
||||
Holder<DimensionType> dimensionType = resolveDimensionType(registryAccess);
|
||||
Holder<DimensionType> dimensionType = resolveDimensionType(registryAccess, pack, packDimensionKey);
|
||||
Holder<Biome> plains = registryAccess.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.PLAINS);
|
||||
FixedBiomeSource biomeSource = new FixedBiomeSource(plains);
|
||||
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, packKey);
|
||||
generator.repoint(packKey, seed);
|
||||
String generatorRef = pack.equals(packDimensionKey) ? pack : pack + ":" + packDimensionKey;
|
||||
IrisModdedChunkGenerator generator = new IrisModdedChunkGenerator(biomeSource, generatorRef);
|
||||
generator.repoint(pack, packDimensionKey, seed);
|
||||
LevelStem stem = new LevelStem(dimensionType, generator);
|
||||
|
||||
WorldData worldData = server.getWorldData();
|
||||
@@ -231,20 +296,21 @@ public final class ModdedDimensionManager {
|
||||
|
||||
serverAccess.putLevel(server, key, level);
|
||||
server.getPlayerList().addWorldborderListener(level);
|
||||
return new Handle(dimensionId, packKey, seed, level, generator);
|
||||
return new Handle(dimensionId, pack, packDimensionKey, seed, level, generator);
|
||||
}
|
||||
|
||||
private static void evacuate(MinecraftServer server, ServerLevel from) {
|
||||
public static int evacuate(MinecraftServer server, ServerLevel from) {
|
||||
ServerLevel fallback = server.overworld();
|
||||
if (fallback == from) {
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
int spawnX = 0;
|
||||
int spawnZ = 0;
|
||||
int spawnY = fallback.getHeight(Heightmap.Types.MOTION_BLOCKING, spawnX, spawnZ);
|
||||
for (ServerPlayer player : new ArrayList<>(from.players())) {
|
||||
player.teleportTo(fallback, spawnX + 0.5D, spawnY, spawnZ + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
BlockPos spawn = fallback.getRespawnData().pos();
|
||||
int spawnY = fallback.getHeight(Heightmap.Types.MOTION_BLOCKING, spawn.getX(), spawn.getZ());
|
||||
List<ServerPlayer> players = new ArrayList<>(from.players());
|
||||
for (ServerPlayer player : players) {
|
||||
player.teleportTo(fallback, spawn.getX() + 0.5D, spawnY, spawn.getZ() + 0.5D, Set.<Relative>of(), player.getYRot(), player.getXRot(), false);
|
||||
}
|
||||
return players.size();
|
||||
}
|
||||
|
||||
private static ResourceKey<Level> levelKey(String dimensionId) {
|
||||
@@ -260,6 +326,6 @@ public final class ModdedDimensionManager {
|
||||
return bound;
|
||||
}
|
||||
|
||||
public record Handle(String dimensionId, String packKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
|
||||
public record Handle(String dimensionId, String pack, String packDimensionKey, long seed, ServerLevel level, IrisModdedChunkGenerator generator) {
|
||||
}
|
||||
}
|
||||
|
||||
+15
-6
@@ -57,12 +57,20 @@ public final class ModdedDimensionRegistryStore {
|
||||
for (int index = 0; index < entries.length(); index++) {
|
||||
JSONObject entry = entries.getJSONObject(index);
|
||||
String id = entry.optString("id", null);
|
||||
String packKey = entry.optString("packKey", null);
|
||||
if (id == null || packKey == null) {
|
||||
if (id == null) {
|
||||
continue;
|
||||
}
|
||||
long seed = entry.optLong("seed", 0L);
|
||||
deduplicated.put(id, new PersistentDimension(id, packKey, seed));
|
||||
String pack = entry.optString("pack", null);
|
||||
String dimension = entry.optString("dimension", null);
|
||||
if (pack == null || dimension == null) {
|
||||
LOGGER.error("Iris registry entry '{}' in {} has no pack/dimension fields; skipping it. Re-create the world with /iris world enable", id, file);
|
||||
continue;
|
||||
}
|
||||
if (!entry.has("seed")) {
|
||||
LOGGER.warn("Iris registry entry '{}' in {} has no seed; skipping it. Re-create the world with /iris world enable", id, file);
|
||||
continue;
|
||||
}
|
||||
deduplicated.put(id, new PersistentDimension(id, pack, dimension, entry.getLong("seed")));
|
||||
}
|
||||
return new ArrayList<>(deduplicated.values());
|
||||
} catch (RuntimeException | IOException e) {
|
||||
@@ -98,7 +106,8 @@ public final class ModdedDimensionRegistryStore {
|
||||
for (PersistentDimension dimension : dimensions) {
|
||||
JSONObject entry = new JSONObject();
|
||||
entry.put("id", dimension.id());
|
||||
entry.put("packKey", dimension.packKey());
|
||||
entry.put("pack", dimension.pack());
|
||||
entry.put("dimension", dimension.dimension());
|
||||
entry.put("seed", dimension.seed());
|
||||
entries.put(entry);
|
||||
}
|
||||
@@ -118,6 +127,6 @@ public final class ModdedDimensionRegistryStore {
|
||||
return server.getWorldPath(LevelResource.ROOT).resolve("iris").resolve(FILE_NAME);
|
||||
}
|
||||
|
||||
public record PersistentDimension(String id, String packKey, long seed) {
|
||||
public record PersistentDimension(String id, String pack, String dimension, long seed) {
|
||||
}
|
||||
}
|
||||
|
||||
+70
-8
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.gui.GuiHost;
|
||||
import art.arcane.iris.engine.decorator.DecoratorPlatformHooks;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineWorldManager;
|
||||
@@ -28,8 +29,16 @@ import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.modded.api.ModdedCustomContentRegistry;
|
||||
import art.arcane.iris.modded.command.ModdedGuiHost;
|
||||
import art.arcane.iris.modded.command.ModdedObjectUndo;
|
||||
import art.arcane.iris.modded.command.ModdedPregenJob;
|
||||
import art.arcane.iris.modded.command.ModdedStudioCommands;
|
||||
import art.arcane.iris.modded.command.ModdedWandService;
|
||||
import art.arcane.iris.modded.service.ModdedChunkUpdateService;
|
||||
import art.arcane.iris.modded.service.ModdedEngineMaintenanceService;
|
||||
import art.arcane.iris.modded.service.ModdedLogFilterService;
|
||||
import art.arcane.iris.modded.service.ModdedPreservationService;
|
||||
import art.arcane.iris.modded.service.ModdedSettingsHotloadService;
|
||||
import art.arcane.iris.modded.service.ModdedStudioHotloadService;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
@@ -51,6 +60,7 @@ public final class ModdedEngineBootstrap {
|
||||
private static final ModdedServiceManager SERVICE_MANAGER = new ModdedServiceManager();
|
||||
private static volatile ModdedLoader loader;
|
||||
private static volatile ModdedPlatform platform;
|
||||
private static volatile MinecraftServer currentServer;
|
||||
|
||||
private ModdedEngineBootstrap() {
|
||||
}
|
||||
@@ -71,32 +81,76 @@ public final class ModdedEngineBootstrap {
|
||||
SERVICE_MANAGER.tick(server);
|
||||
}
|
||||
|
||||
public static void stop() {
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
SERVICE_MANAGER.disableAll();
|
||||
public static void start(MinecraftServer server) {
|
||||
currentServer = server;
|
||||
bind();
|
||||
ModdedStartup.reset();
|
||||
ModdedScheduler scheduler = schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdown();
|
||||
scheduler.reset();
|
||||
}
|
||||
SERVICE_MANAGER.enableAll();
|
||||
}
|
||||
|
||||
public static void initialize(ModdedLoader moddedLoader) {
|
||||
public static void stop() {
|
||||
ModdedPregenJob.shutdown();
|
||||
ModdedObjectUndo.clearAll();
|
||||
ModdedWandService.clearAll();
|
||||
ModdedStudioCommands.clear();
|
||||
ModdedWorldEngines.shutdown();
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
SERVICE_MANAGER.disableAll();
|
||||
ModdedDimensionManager.clear();
|
||||
ModdedScheduler scheduler = schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
scheduler.reset();
|
||||
}
|
||||
ModdedStartup.reset();
|
||||
currentServer = null;
|
||||
}
|
||||
|
||||
public static void bootCommon(ModdedLoader moddedLoader, String loaderDescription, Runnable chunkGeneratorRegistration) {
|
||||
loader = moddedLoader;
|
||||
ModdedIrisLog.info("Iris " + moddedLoader.modVersion() + " bootstrapping on Minecraft " + moddedLoader.minecraftVersion() + " (" + loaderDescription + ")");
|
||||
selfTest(moddedLoader.getClass().getClassLoader());
|
||||
bind();
|
||||
chunkGeneratorRegistration.run();
|
||||
ModdedIrisLog.info("Iris chunk generator registered as irisworldgen:iris");
|
||||
armParityProbe();
|
||||
armWorldCheck();
|
||||
}
|
||||
|
||||
private static void armParityProbe() {
|
||||
String parity = System.getProperty("iris.parity");
|
||||
if (parity == null) {
|
||||
return;
|
||||
}
|
||||
ModdedIrisLog.info("Iris parity probe armed: " + parity);
|
||||
ModdedParityProbe.schedule(parity);
|
||||
}
|
||||
|
||||
private static void armWorldCheck() {
|
||||
if (System.getProperty("iris.worldcheck") == null) {
|
||||
return;
|
||||
}
|
||||
ModdedIrisLog.info("Iris world check armed");
|
||||
ModdedWorldCheck.schedule();
|
||||
}
|
||||
|
||||
public static ModdedLoader loader() {
|
||||
ModdedLoader bound = loader;
|
||||
if (bound == null) {
|
||||
throw new IllegalStateException("Iris modded loader is not initialized; the loader bootstrap must call ModdedEngineBootstrap.initialize first");
|
||||
throw new IllegalStateException("Iris modded loader is not initialized; the loader bootstrap must call ModdedEngineBootstrap.bootCommon first");
|
||||
}
|
||||
return bound;
|
||||
}
|
||||
|
||||
public static MinecraftServer currentServer() {
|
||||
return loader().currentServer();
|
||||
MinecraftServer tracked = currentServer;
|
||||
return tracked != null ? tracked : loader().currentServer();
|
||||
}
|
||||
|
||||
public static void selfTest(ClassLoader classLoader) {
|
||||
private static void selfTest(ClassLoader classLoader) {
|
||||
int loadedClasses = 0;
|
||||
for (String className : CORE_SELF_TEST_CLASSES) {
|
||||
try {
|
||||
@@ -124,6 +178,7 @@ public final class ModdedEngineBootstrap {
|
||||
return platform;
|
||||
}
|
||||
ModdedLoader boundLoader = loader();
|
||||
GuiHost.suppressDesktop(boundLoader.clientEnvironment());
|
||||
ModdedPlatform created = new ModdedPlatform(boundLoader);
|
||||
IrisPlatforms.bind(created);
|
||||
ModdedDimensionManager.bindAccess(new ModdedServerLevels());
|
||||
@@ -135,11 +190,18 @@ public final class ModdedEngineBootstrap {
|
||||
DecoratorPlatformHooks.bind(decoratorHooks, decoratorHooks);
|
||||
ModdedPreservationService preservation = SERVICE_MANAGER.register(ModdedPreservationService.class, new ModdedPreservationService());
|
||||
SERVICE_MANAGER.register(ModdedLogFilterService.class, new ModdedLogFilterService());
|
||||
SERVICE_MANAGER.register(ModdedEngineMaintenanceService.class, new ModdedEngineMaintenanceService());
|
||||
SERVICE_MANAGER.register(ModdedSettingsHotloadService.class, new ModdedSettingsHotloadService());
|
||||
SERVICE_MANAGER.register(ModdedStudioHotloadService.class, new ModdedStudioHotloadService());
|
||||
SERVICE_MANAGER.register(ModdedChunkUpdateService.class, new ModdedChunkUpdateService());
|
||||
IrisServices.register(PreservationRegistry.class, preservation);
|
||||
IrisServices.register(EngineWorldManagerProvider.class, (EngineWorldManagerProvider) (Engine engine) -> new InertWorldManager());
|
||||
ModdedCustomContentRegistry.discover();
|
||||
platform = created;
|
||||
SERVICE_MANAGER.enableAll();
|
||||
if (boundLoader.clientEnvironment()) {
|
||||
ModdedStartup.prefetchDefaultPack();
|
||||
}
|
||||
ModdedIrisSplash.print(boundLoader);
|
||||
return created;
|
||||
}
|
||||
|
||||
+102
-16
@@ -121,10 +121,11 @@ public final class ModdedForcedDatapack {
|
||||
IDataFixer fixer = DataVersion.getLatest().get();
|
||||
|
||||
int packCount = 0;
|
||||
KList<String> presetIds = new KList<>();
|
||||
File[] packs = packsRoot().toFile().listFiles(File::isDirectory);
|
||||
if (packs != null) {
|
||||
for (File pack : packs) {
|
||||
if (installPack(pack, fixer, folders, seenBiomes)) {
|
||||
if (installPack(pack, fixer, folders, seenBiomes, presetIds)) {
|
||||
packCount++;
|
||||
}
|
||||
}
|
||||
@@ -132,28 +133,113 @@ public final class ModdedForcedDatapack {
|
||||
|
||||
writePackMeta(packDirectory);
|
||||
writeStudioPoolType(packDirectory);
|
||||
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} custom biome(s) at {}", packCount, seenBiomes.size(), packDirectory);
|
||||
if (!presetIds.isEmpty()) {
|
||||
writeWorldPresetTag(packDirectory, presetIds);
|
||||
}
|
||||
LOGGER.info("Iris forced startup datapack regenerated: {} pack(s), {} world preset(s), {} custom biome(s) at {}", packCount, presetIds.size(), seenBiomes.size(), packDirectory);
|
||||
return packDirectory;
|
||||
}
|
||||
|
||||
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, KSet<String> seenBiomes) {
|
||||
String packKey = packFolder.getName();
|
||||
if (!new File(packFolder, "dimensions/" + packKey + ".json").isFile()) {
|
||||
private static boolean installPack(File packFolder, IDataFixer fixer, KList<File> folders, KSet<String> seenBiomes, KList<String> presetIds) {
|
||||
String packName = packFolder.getName();
|
||||
File[] dimensionFiles = new File(packFolder, "dimensions").listFiles((File file) -> file.isFile() && file.getName().endsWith(".json"));
|
||||
if (dimensionFiles == null || dimensionFiles.length == 0) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(packKey);
|
||||
if (dimension == null) {
|
||||
return false;
|
||||
boolean installed = false;
|
||||
for (File dimensionFile : dimensionFiles) {
|
||||
String dimensionKey = dimensionFile.getName().substring(0, dimensionFile.getName().length() - ".json".length());
|
||||
try {
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
continue;
|
||||
}
|
||||
dimension.installBiomes(fixer, () -> data, folders, seenBiomes);
|
||||
writeDimensionType(folders, fixer, dimension);
|
||||
String presetKey = dimensionKey.equals(packName) ? packName : packName + "_" + dimensionKey;
|
||||
writeWorldPreset(folders, dimension, packName, dimensionKey, presetKey);
|
||||
presetIds.add("irisworldgen:" + presetKey);
|
||||
installed = true;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to install forced datapack content for pack '{}' dimension '{}'", packName, dimensionKey, e);
|
||||
}
|
||||
dimension.installBiomes(fixer, () -> data, folders, seenBiomes);
|
||||
writeDimensionType(folders, fixer, dimension);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to install forced datapack content for pack '{}'", packKey, e);
|
||||
return false;
|
||||
}
|
||||
return installed;
|
||||
}
|
||||
|
||||
public static String dimensionTypeRef(IrisDimension dimension) {
|
||||
return fitsStudioPool(dimension)
|
||||
? "irisworldgen:" + STUDIO_POOL_TYPE_KEY
|
||||
: "irisworldgen:" + dimension.getDimensionTypeKey();
|
||||
}
|
||||
|
||||
private static void writeWorldPreset(KList<File> folders, IrisDimension dimension, String packName, String dimensionKey, String presetKey) throws IOException {
|
||||
String dimensionRef = dimensionKey.equals(packName) ? packName : packName + ":" + dimensionKey;
|
||||
String json = worldPresetJson(dimensionRef, dimensionTypeRef(dimension));
|
||||
for (File parent : folders) {
|
||||
Path output = parent.toPath().resolve(PACK_FOLDER).resolve("data").resolve("irisworldgen").resolve("worldgen").resolve("world_preset").resolve(presetKey + ".json");
|
||||
Files.createDirectories(output.getParent());
|
||||
Files.writeString(output, json, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private static String worldPresetJson(String dimensionRef, String dimensionTypeRef) {
|
||||
return "{\n"
|
||||
+ " \"dimensions\": {\n"
|
||||
+ " \"minecraft:overworld\": {\n"
|
||||
+ " \"type\": \"" + dimensionTypeRef + "\",\n"
|
||||
+ " \"generator\": {\n"
|
||||
+ " \"type\": \"irisworldgen:iris\",\n"
|
||||
+ " \"biome_source\": {\n"
|
||||
+ " \"type\": \"minecraft:fixed\",\n"
|
||||
+ " \"biome\": \"minecraft:plains\"\n"
|
||||
+ " },\n"
|
||||
+ " \"dimension\": \"" + dimensionRef + "\"\n"
|
||||
+ " }\n"
|
||||
+ " },\n"
|
||||
+ " \"minecraft:the_nether\": {\n"
|
||||
+ " \"type\": \"minecraft:the_nether\",\n"
|
||||
+ " \"generator\": {\n"
|
||||
+ " \"type\": \"minecraft:noise\",\n"
|
||||
+ " \"settings\": \"minecraft:nether\",\n"
|
||||
+ " \"biome_source\": {\n"
|
||||
+ " \"type\": \"minecraft:multi_noise\",\n"
|
||||
+ " \"preset\": \"minecraft:nether\"\n"
|
||||
+ " }\n"
|
||||
+ " }\n"
|
||||
+ " },\n"
|
||||
+ " \"minecraft:the_end\": {\n"
|
||||
+ " \"type\": \"minecraft:the_end\",\n"
|
||||
+ " \"generator\": {\n"
|
||||
+ " \"type\": \"minecraft:noise\",\n"
|
||||
+ " \"settings\": \"minecraft:end\",\n"
|
||||
+ " \"biome_source\": {\n"
|
||||
+ " \"type\": \"minecraft:the_end\"\n"
|
||||
+ " }\n"
|
||||
+ " }\n"
|
||||
+ " }\n"
|
||||
+ " }\n"
|
||||
+ "}\n";
|
||||
}
|
||||
|
||||
private static void writeWorldPresetTag(Path packDirectory, KList<String> presetIds) throws IOException {
|
||||
StringBuilder values = new StringBuilder();
|
||||
for (int i = 0; i < presetIds.size(); i++) {
|
||||
if (i > 0) {
|
||||
values.append(",\n");
|
||||
}
|
||||
values.append(" \"").append(presetIds.get(i)).append("\"");
|
||||
}
|
||||
String json = "{\n"
|
||||
+ " \"replace\": false,\n"
|
||||
+ " \"values\": [\n"
|
||||
+ values
|
||||
+ "\n ]\n"
|
||||
+ "}\n";
|
||||
Path output = packDirectory.resolve("data").resolve("minecraft").resolve("tags").resolve("worldgen").resolve("world_preset").resolve("normal.json");
|
||||
Files.createDirectories(output.getParent());
|
||||
Files.writeString(output, json, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static void writeDimensionType(KList<File> folders, IDataFixer fixer, IrisDimension dimension) throws IOException {
|
||||
|
||||
@@ -18,17 +18,12 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.BuildConstants;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.splash.IrisSplashPackScanner;
|
||||
import art.arcane.iris.core.splash.IrisSplashPackScanner.SplashPackMetadata;
|
||||
import art.arcane.iris.core.splash.IrisSplashComposer;
|
||||
import art.arcane.iris.core.splash.IrisSplashRenderer;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
public final class ModdedIrisSplash {
|
||||
|
||||
@@ -44,44 +39,16 @@ public final class ModdedIrisSplash {
|
||||
|
||||
private static void printPacks(ModdedLoader loader) {
|
||||
File packFolder = loader.configDir().resolve("irisworldgen").resolve("packs").toFile();
|
||||
List<SplashPackMetadata> packs = IrisSplashPackScanner.collect(packFolder, IrisLogging::reportError);
|
||||
if (packs.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
IrisLogging.info("Custom Dimensions: " + packs.size());
|
||||
for (SplashPackMetadata pack : packs) {
|
||||
IrisLogging.info(" " + pack.name() + " v" + pack.version());
|
||||
for (String line : IrisSplashComposer.composePackLines(packFolder, IrisLogging::reportError)) {
|
||||
IrisLogging.info(line);
|
||||
}
|
||||
}
|
||||
|
||||
private static void printLogo(ModdedLoader loader) {
|
||||
String padding = " ".repeat(4);
|
||||
String version = loader.modVersion();
|
||||
String releaseTrain = getReleaseTrain(version);
|
||||
String startupDate = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
|
||||
int javaVersion = getJavaVersion();
|
||||
String serverLine = loader.platformName() + " / Minecraft " + loader.minecraftVersion();
|
||||
String[] splash = IrisSplashRenderer.renderPlain();
|
||||
String[] info = new String[]{
|
||||
"",
|
||||
" Iris, Dimension Engine [" + releaseTrain + " RC.1.1.6]",
|
||||
" Version: " + version,
|
||||
" By: Volmit Software (Arcane Arts)",
|
||||
" Server: " + loader.platformName() + " / Minecraft " + loader.minecraftVersion(),
|
||||
" Java: " + javaVersion + " | Date: " + startupDate,
|
||||
" Commit: " + BuildConstants.COMMIT + "/" + BuildConstants.ENVIRONMENT,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
};
|
||||
|
||||
StringBuilder builder = new StringBuilder("\n\n");
|
||||
for (int i = 0; i < splash.length; i++) {
|
||||
builder.append(padding).append(splash[i]).append(info[i]).append('\n');
|
||||
}
|
||||
|
||||
IrisLogging.info(builder.toString());
|
||||
String[] info = IrisSplashComposer.composeInfo(loader.modVersion(), serverLine, IrisSplashComposer.InfoStyle.PLAIN);
|
||||
IrisLogging.info(IrisSplashComposer.compose(splash, info));
|
||||
}
|
||||
|
||||
private static boolean isLogoEnabled() {
|
||||
@@ -92,30 +59,4 @@ public final class ModdedIrisSplash {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getJavaVersion() {
|
||||
String version = System.getProperty("java.version");
|
||||
if (version.startsWith("1.")) {
|
||||
version = version.substring(2, 3);
|
||||
} else {
|
||||
int dot = version.indexOf('.');
|
||||
if (dot != -1) {
|
||||
version = version.substring(0, dot);
|
||||
}
|
||||
}
|
||||
return Integer.parseInt(version);
|
||||
}
|
||||
|
||||
private static String getReleaseTrain(String version) {
|
||||
String value = version == null ? "unknown" : version;
|
||||
int suffixIndex = value.indexOf('-');
|
||||
if (suffixIndex >= 0) {
|
||||
value = value.substring(0, suffixIndex);
|
||||
}
|
||||
String[] split = value.split("\\.");
|
||||
if (split.length >= 2) {
|
||||
return split[0] + "." + split[1];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
/*
|
||||
* 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 art.arcane.iris.engine.object.InventorySlotType;
|
||||
import art.arcane.iris.engine.object.IrisAttributeModifier;
|
||||
import art.arcane.iris.engine.object.IrisEnchantment;
|
||||
import art.arcane.iris.engine.object.IrisLoot;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.engine.object.NoiseStyle;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.TagParser;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.Unit;
|
||||
import net.minecraft.world.item.Item;
|
||||
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.CustomModelData;
|
||||
import net.minecraft.world.item.component.DyedItemColor;
|
||||
import net.minecraft.world.item.component.ItemLore;
|
||||
import net.minecraft.world.item.enchantment.Enchantment;
|
||||
import net.minecraft.world.item.enchantment.ItemEnchantments;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedItemTranslator {
|
||||
private static final Set<String> WARNED = ConcurrentHashMap.newKeySet();
|
||||
private static final Field TYPE_FIELD = lootField("type");
|
||||
private static final Field DYE_COLOR_FIELD = lootField("dyeColor");
|
||||
private static final String COLOR_CODES = "0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx";
|
||||
|
||||
private ModdedItemTranslator() {
|
||||
}
|
||||
|
||||
public static KList<ItemStack> loot(IrisLootTable table, RNG rng, InventorySlotType slot, ServerLevel level, int x, int y, int z) {
|
||||
KList<ItemStack> out = new KList<>();
|
||||
KList<IrisLoot> entries = table.getLoot();
|
||||
if (entries.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
|
||||
int m = 0;
|
||||
int c = 0;
|
||||
int mx = rng.i(table.getMinPicked(), table.getMaxPicked());
|
||||
|
||||
while (m < mx && c++ < table.getMaxTries()) {
|
||||
IrisLoot entry = entries.get(rng.i(entries.size()));
|
||||
if (entry.getSlotTypes() != slot) {
|
||||
continue;
|
||||
}
|
||||
ItemStack item = item(entry, table, rng, level, x, y, z);
|
||||
if (item != null && !item.isEmpty()) {
|
||||
out.add(item);
|
||||
m++;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public static ItemStack item(IrisLoot loot, IrisLootTable table, RNG rng, ServerLevel level, int x, int y, int z) {
|
||||
if (loot.getChance().aquire(() -> NoiseStyle.STATIC.create(rng)).fit(1, loot.getRarity() * table.getRarity(), x, y, z) != 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ItemStack stack = baseStack(loot, rng);
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
applyComponents(loot, stack, rng, level);
|
||||
applyCustomNbt(stack, loot.getCustomNbt());
|
||||
return stack;
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static ItemStack baseStack(IrisLoot loot, RNG rng) {
|
||||
String raw = readString(TYPE_FIELD, loot);
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = raw.toLowerCase(Locale.ROOT);
|
||||
Identifier id = Identifier.tryParse(key.contains(":") ? key : "minecraft:" + key);
|
||||
if (id == null || !BuiltInRegistries.ITEM.containsKey(id)) {
|
||||
warnOnce("item:" + key, "Iris loot: unknown item '" + raw + "'");
|
||||
return null;
|
||||
}
|
||||
|
||||
Item item = BuiltInRegistries.ITEM.getValue(id);
|
||||
return new ItemStack(item, Math.max(1, rng.i(loot.getMinAmount(), loot.getMaxAmount())));
|
||||
}
|
||||
|
||||
private static void applyComponents(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) {
|
||||
applyEnchantments(loot, stack, rng, level);
|
||||
consumeAttributeDraws(loot, rng);
|
||||
|
||||
if (loot.isUnbreakable()) {
|
||||
stack.set(DataComponents.UNBREAKABLE, Unit.INSTANCE);
|
||||
}
|
||||
|
||||
if (loot.getItemFlags().isNotEmpty()) {
|
||||
warnOnce("itemFlags", "Iris loot: itemFlags are not supported on modded; skipping flags");
|
||||
}
|
||||
|
||||
if (loot.getCustomModel() != null) {
|
||||
stack.set(DataComponents.CUSTOM_MODEL_DATA, new CustomModelData(List.of(loot.getCustomModel().floatValue()), List.of(), List.of(), List.of()));
|
||||
}
|
||||
|
||||
if (stack.getMaxDamage() > 0) {
|
||||
int max = stack.getMaxDamage();
|
||||
int damage = (int) Math.round(Math.max(0, Math.min(max, (1D - rng.d(loot.getMinDurability(), loot.getMaxDurability())) * max)));
|
||||
stack.set(DataComponents.DAMAGE, damage);
|
||||
}
|
||||
|
||||
if (loot.getLeatherColor() != null) {
|
||||
try {
|
||||
int rgb = Integer.decode(loot.getLeatherColor()) & 0xFFFFFF;
|
||||
stack.set(DataComponents.DYED_COLOR, new DyedItemColor(rgb));
|
||||
} catch (NumberFormatException e) {
|
||||
warnOnce("leatherColor:" + loot.getLeatherColor(), "Iris loot: invalid leatherColor '" + loot.getLeatherColor() + "'");
|
||||
}
|
||||
}
|
||||
|
||||
String dye = readString(DYE_COLOR_FIELD, loot);
|
||||
if (dye != null) {
|
||||
warnOnce("dyeColor", "Iris loot: dyeColor is not supported on modded; skipping");
|
||||
}
|
||||
|
||||
if (loot.getDisplayName() != null) {
|
||||
stack.set(DataComponents.CUSTOM_NAME, Component.literal(colorize(loot.getDisplayName())));
|
||||
}
|
||||
|
||||
applyLore(loot, stack);
|
||||
}
|
||||
|
||||
private static void applyEnchantments(IrisLoot loot, ItemStack stack, RNG rng, ServerLevel level) {
|
||||
if (loot.getEnchantments().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Registry<Enchantment> registry = level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT);
|
||||
DataComponentType<ItemEnchantments> component = stack.is(Items.ENCHANTED_BOOK) ? DataComponents.STORED_ENCHANTMENTS : DataComponents.ENCHANTMENTS;
|
||||
ItemEnchantments.Mutable mutable = new ItemEnchantments.Mutable(stack.getOrDefault(component, ItemEnchantments.EMPTY));
|
||||
boolean changed = false;
|
||||
|
||||
for (IrisEnchantment enchantment : loot.getEnchantments()) {
|
||||
String name = enchantment.getEnchantment();
|
||||
if (name == null || name.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String key = name.toLowerCase(Locale.ROOT);
|
||||
Identifier id = Identifier.tryParse(key.contains(":") ? key : "minecraft:" + key);
|
||||
Optional<Holder.Reference<Enchantment>> holder = id == null ? Optional.empty() : registry.get(id);
|
||||
if (holder.isEmpty()) {
|
||||
warnOnce("enchantment:" + key, "Iris loot: unknown enchantment '" + name + "'");
|
||||
continue;
|
||||
}
|
||||
if (rng.nextDouble() < enchantment.getChance()) {
|
||||
mutable.set(holder.get(), enchantment.getLevel(rng));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
stack.set(component, mutable.toImmutable());
|
||||
}
|
||||
}
|
||||
|
||||
private static void consumeAttributeDraws(IrisLoot loot, RNG rng) {
|
||||
if (loot.getAttributes().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
warnOnce("attributes", "Iris loot: attribute modifiers are not supported on modded; skipping");
|
||||
for (IrisAttributeModifier attribute : loot.getAttributes()) {
|
||||
if (rng.nextDouble() < attribute.getChance()) {
|
||||
attribute.getAmount(rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyLore(IrisLoot loot, ItemStack stack) {
|
||||
if (loot.getLore().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Component> lines = new ArrayList<>();
|
||||
for (String line : loot.getLore()) {
|
||||
String colored = colorize(line);
|
||||
if (colored.length() > 24) {
|
||||
for (String wrapped : Form.wrapWords(colored, 24).split("\\Q\n\\E")) {
|
||||
lines.add(Component.literal(wrapped.trim()));
|
||||
}
|
||||
} else {
|
||||
lines.add(Component.literal(colored));
|
||||
}
|
||||
}
|
||||
stack.set(DataComponents.LORE, new ItemLore(lines));
|
||||
}
|
||||
|
||||
private static void applyCustomNbt(ItemStack stack, KMap<String, Object> customNbt) {
|
||||
if (customNbt == null || customNbt.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
CompoundTag tag = TagParser.parseCompoundFully(new JSONObject(customNbt).toString());
|
||||
tag.merge(stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag());
|
||||
stack.set(DataComponents.CUSTOM_DATA, CustomData.of(tag));
|
||||
} catch (CommandSyntaxException e) {
|
||||
warnOnce("customNbt:" + e.getMessage(), "Iris loot: invalid customNbt: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static String colorize(String text) {
|
||||
char[] chars = text.toCharArray();
|
||||
for (int i = 0; i < chars.length - 1; i++) {
|
||||
if (chars[i] == '&' && COLOR_CODES.indexOf(chars[i + 1]) > -1) {
|
||||
chars[i] = '§';
|
||||
chars[i + 1] = Character.toLowerCase(chars[i + 1]);
|
||||
}
|
||||
}
|
||||
return new String(chars);
|
||||
}
|
||||
|
||||
private static void warnOnce(String key, String message) {
|
||||
if (WARNED.add(key)) {
|
||||
IrisLogging.warn(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static Field lootField(String name) {
|
||||
try {
|
||||
Field field = IrisLoot.class.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
} catch (NoSuchFieldException e) {
|
||||
throw new IllegalStateException("IrisLoot field missing: " + name, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String readString(Field field, IrisLoot loot) {
|
||||
try {
|
||||
Object value = field.get(loot);
|
||||
return value == null ? null : value.toString();
|
||||
} catch (IllegalAccessException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ public interface ModdedLoader {
|
||||
|
||||
MinecraftServer currentServer();
|
||||
|
||||
boolean clientEnvironment();
|
||||
|
||||
Path configDir();
|
||||
|
||||
File modJar();
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
* 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 art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.PlacedObject;
|
||||
import art.arcane.iris.engine.object.IObjectLoot;
|
||||
import art.arcane.iris.engine.object.InventorySlotType;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.engine.object.IrisObjectLoot;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisObjectVanillaLoot;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.block.ChestBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class ModdedLootApplier {
|
||||
private ModdedLootApplier() {
|
||||
}
|
||||
|
||||
private record Candidate(IrisLootTable table, Identifier vanillaId, int weight) {
|
||||
}
|
||||
|
||||
private record Resolution(KList<IrisLootTable> tables, Identifier vanillaTable) {
|
||||
boolean isEmpty() {
|
||||
return tables.isEmpty() && vanillaTable == null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void apply(Engine engine, ServerLevel level, BlockPos pos, BlockState state, MantleChunk<Matter> mc, RNG rng, int localX, int localZ) {
|
||||
Resolution resolution = resolveTables(engine, rng, pos, state, mc);
|
||||
if (resolution.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resolution.vanillaTable() != null) {
|
||||
BlockEntity blockEntity = level.getBlockEntity(pos);
|
||||
if (blockEntity instanceof RandomizableContainerBlockEntity randomizable) {
|
||||
randomizable.setLootTable(ResourceKey.create(Registries.LOOT_TABLE, resolution.vanillaTable()), rng.nextLong());
|
||||
} else {
|
||||
IrisLogging.debug("Iris loot: no randomizable container at " + pos + " for vanilla table " + resolution.vanillaTable());
|
||||
}
|
||||
}
|
||||
|
||||
if (resolution.tables().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Container container = resolveContainer(level, pos, state);
|
||||
if (container == null) {
|
||||
IrisLogging.debug("Iris loot: no container at " + pos);
|
||||
return;
|
||||
}
|
||||
|
||||
KList<ItemStack> items = new KList<>();
|
||||
for (IrisLootTable table : resolution.tables()) {
|
||||
if (table == null) {
|
||||
continue;
|
||||
}
|
||||
items.addAll(ModdedItemTranslator.loot(table, rng, InventorySlotType.STORAGE, level, localX, pos.getY(), localZ));
|
||||
}
|
||||
|
||||
for (ItemStack item : items) {
|
||||
addItem(container, item);
|
||||
}
|
||||
|
||||
scramble(container, rng);
|
||||
container.setChanged();
|
||||
}
|
||||
|
||||
private static Resolution resolveTables(Engine engine, RNG rng, BlockPos pos, BlockState state, MantleChunk<Matter> mc) {
|
||||
int rx = pos.getX();
|
||||
int rz = pos.getZ();
|
||||
int ry = pos.getY() - engine.getWorld().minHeight();
|
||||
double he = engine.getComplex().getHeightStream().get(rx, rz);
|
||||
KList<IrisLootTable> tables = new KList<>();
|
||||
Identifier vanillaTable = null;
|
||||
|
||||
PlacedObject po = engine.getObjectPlacement(rx, ry, rz, mc);
|
||||
if (po != null && po.getPlacement() != null && ModdedBlockResolution.isStorageChest(state)) {
|
||||
Candidate picked = pickPlacementTable(engine, po.getPlacement(), state, rng);
|
||||
if (picked != null) {
|
||||
if (picked.table() != null) {
|
||||
tables.add(picked.table());
|
||||
} else {
|
||||
vanillaTable = picked.vanillaId();
|
||||
}
|
||||
if (po.getPlacement().isOverrideGlobalLoot()) {
|
||||
return new Resolution(tables, vanillaTable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IrisRegion region = engine.getComplex().getRegionStream().get(rx, rz);
|
||||
IrisBiome biomeSurface = engine.getComplex().getTrueBiomeStream().get(rx, rz);
|
||||
IrisBiome biomeUnder = ry < he ? engine.getCaveBiome(rx, ry, rz) : biomeSurface;
|
||||
|
||||
double multiplier = 1D * engine.getDimension().getLoot().getMultiplier() * region.getLoot().getMultiplier() * biomeSurface.getLoot().getMultiplier() * biomeUnder.getLoot().getMultiplier();
|
||||
boolean fallback = tables.isEmpty() && vanillaTable == null;
|
||||
engine.injectTables(tables, engine.getDimension().getLoot(), fallback);
|
||||
engine.injectTables(tables, region.getLoot(), fallback);
|
||||
engine.injectTables(tables, biomeSurface.getLoot(), fallback);
|
||||
engine.injectTables(tables, biomeUnder.getLoot(), fallback);
|
||||
|
||||
if (tables.isNotEmpty()) {
|
||||
int target = (int) Math.round(tables.size() * multiplier);
|
||||
|
||||
while (tables.size() < target && tables.isNotEmpty()) {
|
||||
tables.add(tables.get(rng.i(tables.size() - 1)));
|
||||
}
|
||||
|
||||
while (tables.size() > target && tables.isNotEmpty()) {
|
||||
tables.remove(rng.i(tables.size() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
return new Resolution(tables, vanillaTable);
|
||||
}
|
||||
|
||||
private static Candidate pickPlacementTable(Engine engine, IrisObjectPlacement placement, BlockState state, RNG rng) {
|
||||
List<Candidate> exact = new ArrayList<>();
|
||||
List<Candidate> basic = new ArrayList<>();
|
||||
List<Candidate> global = new ArrayList<>();
|
||||
|
||||
for (IrisObjectLoot loot : placement.getLoot()) {
|
||||
if (loot == null) {
|
||||
continue;
|
||||
}
|
||||
IrisLootTable table = engine.getData().getLootLoader().load(loot.getName());
|
||||
if (table == null) {
|
||||
IrisLogging.warn("Couldn't find loot table " + loot.getName());
|
||||
continue;
|
||||
}
|
||||
bucket(engine, loot, new Candidate(table, null, loot.getWeight()), state, exact, basic, global);
|
||||
}
|
||||
|
||||
for (IrisObjectVanillaLoot loot : placement.getVanillaLoot()) {
|
||||
if (loot == null) {
|
||||
continue;
|
||||
}
|
||||
Identifier id = loot.getName() == null ? null : Identifier.tryParse(loot.getName());
|
||||
if (id == null) {
|
||||
IrisLogging.warn("Couldn't parse vanilla loot table " + loot.getName());
|
||||
continue;
|
||||
}
|
||||
bucket(engine, loot, new Candidate(null, id, loot.getWeight()), state, exact, basic, global);
|
||||
}
|
||||
|
||||
List<Candidate> pool = !exact.isEmpty() ? exact : !basic.isEmpty() ? basic : global;
|
||||
return pickWeighted(pool, rng);
|
||||
}
|
||||
|
||||
private static void bucket(Engine engine, IObjectLoot loot, Candidate candidate, BlockState state, List<Candidate> exact, List<Candidate> basic, List<Candidate> global) {
|
||||
if (loot.getFilter().isEmpty()) {
|
||||
global.add(candidate);
|
||||
return;
|
||||
}
|
||||
|
||||
for (PlatformBlockState filterState : loot.getFilter(engine.getData())) {
|
||||
BlockState filter = (BlockState) filterState.nativeHandle();
|
||||
if (loot.isExact() ? filter == state : filter.getBlock() == state.getBlock()) {
|
||||
(loot.isExact() ? exact : basic).add(candidate);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Candidate pickWeighted(List<Candidate> pool, RNG rng) {
|
||||
if (pool.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int total = 0;
|
||||
for (Candidate candidate : pool) {
|
||||
total += Math.max(0, candidate.weight());
|
||||
}
|
||||
if (total <= 0) {
|
||||
return pool.get(rng.nextInt(pool.size()));
|
||||
}
|
||||
int pull = rng.nextInt(total);
|
||||
for (Candidate candidate : pool) {
|
||||
pull -= Math.max(0, candidate.weight());
|
||||
if (pull < 0) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return pool.get(pool.size() - 1);
|
||||
}
|
||||
|
||||
private static Container resolveContainer(ServerLevel level, BlockPos pos, BlockState state) {
|
||||
if (state.getBlock() instanceof ChestBlock chestBlock) {
|
||||
Container combined = ChestBlock.getContainer(chestBlock, state, level, pos, true);
|
||||
if (combined != null) {
|
||||
return combined;
|
||||
}
|
||||
}
|
||||
BlockEntity blockEntity = level.getBlockEntity(pos);
|
||||
return blockEntity instanceof Container container ? container : null;
|
||||
}
|
||||
|
||||
private static void addItem(Container container, ItemStack stack) {
|
||||
if (stack == null || stack.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < container.getContainerSize() && !stack.isEmpty(); i++) {
|
||||
ItemStack existing = container.getItem(i);
|
||||
if (existing.isEmpty()) {
|
||||
container.setItem(i, stack);
|
||||
return;
|
||||
}
|
||||
if (ItemStack.isSameItemSameComponents(existing, stack) && existing.getCount() < existing.getMaxStackSize()) {
|
||||
int move = Math.min(stack.getCount(), existing.getMaxStackSize() - existing.getCount());
|
||||
existing.grow(move);
|
||||
stack.shrink(move);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void scramble(Container container, RNG rng) {
|
||||
int size = container.getContainerSize();
|
||||
ItemStack[] items = new ItemStack[size];
|
||||
for (int i = 0; i < size; i++) {
|
||||
items[i] = container.getItem(i);
|
||||
}
|
||||
boolean packedFull = false;
|
||||
|
||||
splitting:
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
ItemStack is = items[i];
|
||||
|
||||
if (!is.isEmpty() && is.getCount() > 1 && !packedFull) {
|
||||
for (int j = 0; j < items.length; j++) {
|
||||
if (items[j].isEmpty()) {
|
||||
int take = rng.nextInt(is.getCount());
|
||||
take = take == 0 ? 1 : take;
|
||||
is.setCount(is.getCount() - take);
|
||||
items[j] = is.copyWithCount(take);
|
||||
continue splitting;
|
||||
}
|
||||
}
|
||||
|
||||
packedFull = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = items.length; i > 1; i--) {
|
||||
int j = rng.nextInt(i);
|
||||
ItemStack tmp = items[i - 1];
|
||||
items[i - 1] = items[j];
|
||||
items[j] = tmp;
|
||||
}
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
container.setItem(i, items[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-89
@@ -18,25 +18,15 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
import art.arcane.iris.core.pack.PackDownloader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
|
||||
import java.io.File;
|
||||
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");
|
||||
@@ -56,87 +46,13 @@ public final class ModdedPackInstaller {
|
||||
return false;
|
||||
}
|
||||
|
||||
Path target = configDir.resolve("irisworldgen").resolve("packs").resolve(pack);
|
||||
String url = "https://codeload.github.com/IrisDimensions/" + pack + "/zip/refs/heads/" + branch;
|
||||
|
||||
File packs = configDir.resolve("irisworldgen").resolve("packs").toFile();
|
||||
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) {
|
||||
return PackDownloader.download(packs, "IrisDimensions/" + pack, branch, true, false, feedback) != null;
|
||||
} catch (IOException 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,21 +21,16 @@ package art.arcane.iris.modded;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.LogLevel;
|
||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||
import art.arcane.iris.spi.PlatformCapabilities;
|
||||
import art.arcane.iris.spi.PlatformEntityType;
|
||||
import art.arcane.iris.spi.PlatformItem;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.spi.PlatformScheduler;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.EntitySpawnReason;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.function.Consumer;
|
||||
@@ -46,7 +41,6 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
private final ModdedLoader loader;
|
||||
private final ModdedRegistries registries;
|
||||
private final ModdedScheduler scheduler;
|
||||
private final ModdedCapabilities capabilities;
|
||||
private final ModdedStructureHooks structureHooks;
|
||||
private final ModdedBiomeWriter biomeWriter;
|
||||
|
||||
@@ -54,7 +48,6 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
this.loader = loader;
|
||||
this.registries = new ModdedRegistries(loader::currentServer);
|
||||
this.scheduler = new ModdedScheduler();
|
||||
this.capabilities = new ModdedCapabilities();
|
||||
this.structureHooks = new ModdedStructureHooks(loader::currentServer);
|
||||
this.biomeWriter = new ModdedBiomeWriter(loader::currentServer);
|
||||
}
|
||||
@@ -91,11 +84,6 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformCapabilities capabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformStructureHooks structureHooks() {
|
||||
return structureHooks;
|
||||
@@ -159,20 +147,6 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
return entity != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean giveItem(Object player, String itemKey, int amount) {
|
||||
if (!(player instanceof ServerPlayer serverPlayer) || itemKey == null || amount <= 0) {
|
||||
return false;
|
||||
}
|
||||
PlatformItem resolved = registries.item(itemKey);
|
||||
if (resolved == null) {
|
||||
return false;
|
||||
}
|
||||
Item item = (Item) resolved.nativeHandle();
|
||||
ItemStack stack = new ItemStack(item, amount);
|
||||
return serverPlayer.getInventory().add(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(LogLevel level, String message) {
|
||||
ModdedIrisLog.log(level, message);
|
||||
@@ -194,26 +168,4 @@ public final class ModdedPlatform implements IrisPlatform {
|
||||
ModdedIrisLog.error("Iris reported error", error);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ModdedCapabilities implements PlatformCapabilities {
|
||||
@Override
|
||||
public boolean customBiomes() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dataPacks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean structurePlacement() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean regionizedThreading() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,10 +114,11 @@ public final class ModdedScheduler implements PlatformScheduler {
|
||||
laterGlobal(task, ticks);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
asyncExecutor.shutdownNow();
|
||||
public void reset() {
|
||||
asyncExecutor.getQueue().clear();
|
||||
mainQueue.clear();
|
||||
delayedQueue.clear();
|
||||
mainThread = null;
|
||||
}
|
||||
|
||||
private void drain() {
|
||||
|
||||
@@ -77,7 +77,6 @@ public final class ModdedServiceManager {
|
||||
}
|
||||
enabled = false;
|
||||
forEachReversed(this::disableService);
|
||||
services.clear();
|
||||
}
|
||||
|
||||
private void enableService(ModdedService service) {
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
|
||||
package art.arcane.iris.modded;
|
||||
|
||||
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.command.ModdedPackCommands;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -30,10 +34,26 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
public final class ModdedStartup {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
||||
private static final Object PACK_LOCK = new Object();
|
||||
|
||||
private ModdedStartup() {
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
STARTED.set(false);
|
||||
}
|
||||
|
||||
public static void prefetchDefaultPack() {
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler != null) {
|
||||
scheduler.async(ModdedStartup::ensureDefaultPack);
|
||||
return;
|
||||
}
|
||||
Thread thread = new Thread(ModdedStartup::ensureDefaultPack, "iris-modded-pack-prefetch");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public static void runOnce(MinecraftServer server) {
|
||||
if (server == null || !STARTED.compareAndSet(false, true)) {
|
||||
return;
|
||||
@@ -42,10 +62,44 @@ public final class ModdedStartup {
|
||||
|
||||
ModdedScheduler scheduler = ModdedEngineBootstrap.schedulerOrNull();
|
||||
if (scheduler == null) {
|
||||
validateAllPacks();
|
||||
ensureDefaultPack();
|
||||
return;
|
||||
}
|
||||
scheduler.async(ModdedStartup::ensureDefaultPack);
|
||||
scheduler.async(() -> {
|
||||
validateAllPacks();
|
||||
ensureDefaultPack();
|
||||
});
|
||||
}
|
||||
|
||||
public static void validateAllPacks() {
|
||||
File packsRoot = ModdedPackCommands.packsRoot();
|
||||
File[] packDirs = packsRoot.listFiles(File::isDirectory);
|
||||
if (packDirs == null || packDirs.length == 0) {
|
||||
return;
|
||||
}
|
||||
PackValidationRegistry.clear();
|
||||
for (File packDir : packDirs) {
|
||||
try {
|
||||
PackValidationResult result = PackValidator.validate(packDir);
|
||||
PackValidationRegistry.publish(result);
|
||||
if (!result.isLoadable()) {
|
||||
LOGGER.error("Iris pack '{}' FAILED validation - world/studio creation will be refused. Reasons:", result.getPackName());
|
||||
for (String reason : result.getBlockingErrors()) {
|
||||
LOGGER.error(" - {}", reason);
|
||||
}
|
||||
} else if (!result.getWarnings().isEmpty() || !result.getRemovedUnusedFiles().isEmpty()) {
|
||||
LOGGER.info("Iris pack '{}' validated ({} unused file(s) quarantined to .iris-trash/, {} warning(s)).", result.getPackName(), result.getRemovedUnusedFiles().size(), result.getWarnings().size());
|
||||
for (String warning : result.getWarnings()) {
|
||||
LOGGER.warn(" [{}] {}", result.getPackName(), warning);
|
||||
}
|
||||
} else {
|
||||
LOGGER.info("Iris pack '{}' validated.", result.getPackName());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris pack validation failed for '{}'", packDir.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void reinjectPersistentDimensions(MinecraftServer server) {
|
||||
@@ -56,30 +110,32 @@ public final class ModdedStartup {
|
||||
int injected = 0;
|
||||
for (ModdedDimensionRegistryStore.PersistentDimension dimension : dimensions) {
|
||||
try {
|
||||
ModdedDimensionManager.create(server, dimension.id(), dimension.packKey(), dimension.seed());
|
||||
ModdedDimensionManager.create(server, dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed());
|
||||
injected++;
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris failed to re-inject persistent dimension '{}' (pack={} seed={})", dimension.id(), dimension.packKey(), dimension.seed(), e);
|
||||
LOGGER.error("Iris failed to re-inject persistent dimension '{}' (pack={} dim={} seed={})", dimension.id(), dimension.pack(), dimension.dimension(), dimension.seed(), e);
|
||||
}
|
||||
}
|
||||
LOGGER.info("Iris re-injected {} persistent dimension(s) at startup", injected);
|
||||
}
|
||||
|
||||
private static void ensureDefaultPack() {
|
||||
ModdedModConfig config = ModdedModConfig.get();
|
||||
if (!config.autoDownloadDefaultPack()) {
|
||||
return;
|
||||
}
|
||||
String pack = config.defaultPack();
|
||||
Path configDir = ModdedEngineBootstrap.loader().configDir();
|
||||
File packFolder = configDir.resolve("irisworldgen").resolve("packs").resolve(pack).toFile();
|
||||
if (new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
|
||||
return;
|
||||
}
|
||||
LOGGER.info("Iris default pack '{}' missing; downloading IrisDimensions/{} (master)", pack, pack);
|
||||
boolean installed = ModdedPackInstaller.install(configDir, pack, "master", (String line) -> LOGGER.info("Iris: {}", line));
|
||||
if (!installed) {
|
||||
LOGGER.warn("Iris default pack '{}' could not be downloaded; install it with /iris download {}", pack, pack);
|
||||
public static void ensureDefaultPack() {
|
||||
synchronized (PACK_LOCK) {
|
||||
ModdedModConfig config = ModdedModConfig.get();
|
||||
if (!config.autoDownloadDefaultPack()) {
|
||||
return;
|
||||
}
|
||||
String pack = config.defaultPack();
|
||||
Path configDir = ModdedEngineBootstrap.loader().configDir();
|
||||
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
|
||||
if (new File(packFolder, "dimensions/" + pack + ".json").isFile()) {
|
||||
return;
|
||||
}
|
||||
LOGGER.info("Iris default pack '{}' missing; downloading IrisDimensions/{} (master)", pack, pack);
|
||||
boolean installed = ModdedPackInstaller.install(configDir, pack, "master", (String line) -> LOGGER.info("Iris: {}", line));
|
||||
if (!installed) {
|
||||
LOGGER.warn("Iris default pack '{}' could not be downloaded; install it with /iris download {}", pack, pack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-20
@@ -43,12 +43,12 @@ public final class ModdedWorldEngines {
|
||||
private ModdedWorldEngines() {
|
||||
}
|
||||
|
||||
public static Engine get(ServerLevel level, String dimensionKey, long seedOverride) {
|
||||
public static Engine get(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
||||
Engine existing = ENGINES.get(level);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
return ENGINES.computeIfAbsent(level, (ServerLevel l) -> create(l, dimensionKey, seedOverride));
|
||||
return ENGINES.computeIfAbsent(level, (ServerLevel l) -> create(l, pack, dimensionKey, seedOverride));
|
||||
}
|
||||
|
||||
public static Collection<Engine> activeEngines() {
|
||||
@@ -70,15 +70,15 @@ public final class ModdedWorldEngines {
|
||||
}
|
||||
}
|
||||
|
||||
private static Engine create(ServerLevel level, String dimensionKey, long seedOverride) {
|
||||
private static Engine create(ServerLevel level, String pack, String dimensionKey, long seedOverride) {
|
||||
ModdedEngineBootstrap.bind();
|
||||
File pack = resolvePack(dimensionKey);
|
||||
IrisData data = IrisData.get(pack);
|
||||
File packDir = resolvePack(pack, dimensionKey);
|
||||
IrisData data = IrisData.get(packDir);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(dimensionKey);
|
||||
if (dimension == null) {
|
||||
LOGGER.error("Iris pack at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.",
|
||||
pack.getAbsolutePath(), dimensionKey, dimensionKey);
|
||||
throw new IllegalStateException("Iris dimension '" + dimensionKey + "' missing from pack " + pack.getAbsolutePath());
|
||||
LOGGER.error("Iris pack '{}' at {} does not contain dimension '{}' (expected dimensions/{}.json). Install a matching Iris pack and restart.",
|
||||
pack, packDir.getAbsolutePath(), dimensionKey, dimensionKey);
|
||||
throw new IllegalStateException("Iris dimension '" + dimensionKey + "' missing from pack " + packDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
long seed = seedOverride == Long.MIN_VALUE ? level.getSeed() : seedOverride;
|
||||
@@ -94,24 +94,28 @@ public final class ModdedWorldEngines {
|
||||
|
||||
int levelMinY = level.getMinY();
|
||||
int levelMaxY = level.getMinY() + level.getHeight();
|
||||
if (dimension.getMinHeight() != levelMinY || dimension.getMaxHeight() != levelMaxY) {
|
||||
LOGGER.error("Iris pack height mismatch for {}: pack generates {}..{} but the level is {}..{}. Terrain outside the level range will be clipped; ship a matching dimension_type.",
|
||||
if (dimension.getMinHeight() < levelMinY || dimension.getMaxHeight() > levelMaxY) {
|
||||
LOGGER.warn("Iris pack height range for {} exceeds the level: pack generates {}..{} but the level is {}..{}. Terrain outside the level range will be clipped; restart so the forced datapack registers the pack dimension type.",
|
||||
level.dimension().identifier(), dimension.getMinHeight(), dimension.getMaxHeight(), levelMinY, levelMaxY);
|
||||
}
|
||||
|
||||
LOGGER.info("Iris engine up for {}: pack={} dim={} seed={} height={}..{}",
|
||||
level.dimension().identifier(), pack.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
|
||||
level.dimension().identifier(), packDir.getAbsolutePath(), dimension.getLoadKey(), seed, dimension.getMinHeight(), dimension.getMaxHeight());
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static File resolvePack(String dimensionKey) {
|
||||
File pack = ModdedEngineBootstrap.loader().configDir()
|
||||
public static File packFolder(String pack) {
|
||||
return ModdedEngineBootstrap.loader().configDir()
|
||||
.resolve("irisworldgen")
|
||||
.resolve("packs")
|
||||
.resolve(dimensionKey)
|
||||
.resolve(pack)
|
||||
.toFile();
|
||||
if (pack.isDirectory()) {
|
||||
return pack;
|
||||
}
|
||||
|
||||
private static File resolvePack(String pack, String dimensionKey) {
|
||||
File packDir = packFolder(pack);
|
||||
if (packDir.isDirectory()) {
|
||||
return packDir;
|
||||
}
|
||||
|
||||
String parity = System.getProperty("iris.parity");
|
||||
@@ -127,17 +131,17 @@ public final class ModdedWorldEngines {
|
||||
}
|
||||
File parityPack = new File(parityPath);
|
||||
if (parityPack.isDirectory()) {
|
||||
LOGGER.warn("Iris pack missing at {}; falling back to parity pack {}", pack.getAbsolutePath(), parityPack.getAbsolutePath());
|
||||
LOGGER.warn("Iris pack missing at {}; falling back to parity pack {}", packDir.getAbsolutePath(), parityPack.getAbsolutePath());
|
||||
return parityPack;
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.error("===============================================================");
|
||||
LOGGER.error("Iris pack for dimension '{}' is not installed.", dimensionKey);
|
||||
LOGGER.error("Expected a pack folder at: {}", pack.getAbsolutePath());
|
||||
LOGGER.error("Iris pack '{}' is not installed.", pack);
|
||||
LOGGER.error("Expected a pack folder at: {}", packDir.getAbsolutePath());
|
||||
LOGGER.error("Install an Iris pack there (the folder must contain dimensions/{}.json) and restart the server.", dimensionKey);
|
||||
LOGGER.error("===============================================================");
|
||||
throw new IllegalStateException("Iris pack not installed: " + pack.getAbsolutePath());
|
||||
throw new IllegalStateException("Iris pack not installed: " + packDir.getAbsolutePath());
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
|
||||
@@ -22,17 +22,10 @@ import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.command.ModdedPregenJob;
|
||||
import art.arcane.iris.modded.command.ModdedPregenMode;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class IrisModdedAPI {
|
||||
private static final Map<ServerLevel, AtomicInteger> WORLD_MAINTENANCE_DEPTH = new ConcurrentHashMap<>();
|
||||
|
||||
private IrisModdedAPI() {
|
||||
}
|
||||
|
||||
@@ -64,15 +57,15 @@ public final class IrisModdedAPI {
|
||||
}
|
||||
|
||||
public static boolean pregenerate(ServerLevel level, int radiusBlocks) {
|
||||
return pregenerate(level, radiusBlocks, 0, 0, ModdedPregenMode.ASYNC, false);
|
||||
return pregenerate(level, radiusBlocks, 0, 0, false, true);
|
||||
}
|
||||
|
||||
public static boolean pregenerate(ServerLevel level, int radiusBlocks, int centerBlockX, int centerBlockZ, ModdedPregenMode mode, boolean cached) {
|
||||
public static boolean pregenerate(ServerLevel level, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean sync, boolean cached) {
|
||||
Engine engine = getEngine(level);
|
||||
if (engine == null) {
|
||||
return false;
|
||||
}
|
||||
return ModdedPregenJob.start(level.getServer(), level, engine, radiusBlocks, centerBlockX, centerBlockZ, false, mode, cached);
|
||||
return ModdedPregenJob.start(level.getServer(), level, engine, radiusBlocks, centerBlockX, centerBlockZ, false, sync, cached);
|
||||
}
|
||||
|
||||
public static <T> T getMantleData(ServerLevel level, int x, int y, int z, Class<T> type) {
|
||||
@@ -113,32 +106,4 @@ public final class IrisModdedAPI {
|
||||
public static void registerCustomBlockData(String namespace, String key, String state) {
|
||||
ModdedCustomContentRegistry.registerCustomBlockData(namespace, key, state);
|
||||
}
|
||||
|
||||
public static void beginWorldMaintenance(ServerLevel level) {
|
||||
if (level == null) {
|
||||
return;
|
||||
}
|
||||
WORLD_MAINTENANCE_DEPTH.computeIfAbsent(level, (ServerLevel l) -> new AtomicInteger()).incrementAndGet();
|
||||
}
|
||||
|
||||
public static void endWorldMaintenance(ServerLevel level) {
|
||||
if (level == null) {
|
||||
return;
|
||||
}
|
||||
AtomicInteger counter = WORLD_MAINTENANCE_DEPTH.get(level);
|
||||
if (counter == null) {
|
||||
return;
|
||||
}
|
||||
if (counter.decrementAndGet() <= 0) {
|
||||
WORLD_MAINTENANCE_DEPTH.remove(level);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isWorldMaintenanceActive(ServerLevel level) {
|
||||
if (level == null) {
|
||||
return false;
|
||||
}
|
||||
AtomicInteger counter = WORLD_MAINTENANCE_DEPTH.get(level);
|
||||
return counter != null && counter.get() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
+256
-85
@@ -18,6 +18,9 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.GuiHost;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.Locator;
|
||||
@@ -27,6 +30,7 @@ import art.arcane.iris.engine.object.IrisPosition;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedBlockState;
|
||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedLoader;
|
||||
import art.arcane.iris.modded.ModdedPackInstaller;
|
||||
@@ -39,8 +43,11 @@ import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
@@ -48,14 +55,18 @@ import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.SharedSuggestionProvider;
|
||||
import net.minecraft.commands.arguments.DimensionArgument;
|
||||
import net.minecraft.commands.arguments.EntityArgument;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.particles.DustParticleOptions;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
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.item.ItemStack;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
@@ -64,10 +75,10 @@ import net.minecraft.world.phys.HitResult;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
@@ -118,10 +129,29 @@ public final class IrisModdedCommands {
|
||||
.executes((CommandContext<CommandSourceStack> context) -> what(context.getSource()))
|
||||
.then(Commands.literal("block")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> whatBlock(context.getSource())))
|
||||
.then(Commands.literal("hand")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> whatHand(context.getSource())))
|
||||
.then(Commands.literal("markers")
|
||||
.then(Commands.argument("marker", StringArgumentType.greedyString()).suggests(MARKER_TYPES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> whatMarkers(context.getSource(), StringArgumentType.getString(context, "marker"))))));
|
||||
|
||||
root.then(Commands.literal("tp").requires(GATE)
|
||||
.then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(DIMENSION_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), null))
|
||||
.then(Commands.argument("player", EntityArgument.player())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> tp(context.getSource(), DimensionArgument.getDimension(context, "dimension"), EntityArgument.getPlayer(context, "player"))))));
|
||||
|
||||
root.then(Commands.literal("evacuate").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> evacuate(context.getSource(), null))
|
||||
.then(Commands.argument("dimension", DimensionArgument.dimension()).suggests(DIMENSION_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> evacuate(context.getSource(), DimensionArgument.getDimension(context, "dimension")))));
|
||||
|
||||
root.then(Commands.literal("debug").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> debug(context.getSource())));
|
||||
|
||||
root.then(Commands.literal("reload").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> reload(context.getSource())));
|
||||
|
||||
root.then(gotoTree("goto"));
|
||||
root.then(gotoTree("find"));
|
||||
|
||||
@@ -171,7 +201,7 @@ public final class IrisModdedCommands {
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> createTree() {
|
||||
return Commands.literal("create").requires(GATE)
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(PACK_NAMES)
|
||||
.then(Commands.argument("pack", StringArgumentType.string()).suggests(PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedWorldCommands.createWorld(context.getSource(),
|
||||
StringArgumentType.getString(context, "name"),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
@@ -231,27 +261,20 @@ public final class IrisModdedCommands {
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> pregenTree(String name) {
|
||||
RequiredArgumentBuilder<CommandSourceStack, Integer> radius = Commands.argument("radius", IntegerArgumentType.integer(1, 100000))
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context, false, false, false, false, false));
|
||||
attachPregenCenter(radius, false);
|
||||
attachPregenFlags(radius, false, false, false, false, false);
|
||||
RequiredArgumentBuilder<CommandSourceStack, Identifier> dimension = Commands.argument("dimension", DimensionArgument.dimension()).suggests(DIMENSION_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context, true, false, false, false, false));
|
||||
attachPregenCenter(dimension, true);
|
||||
attachPregenFlags(dimension, true, false, false, false, false);
|
||||
radius.then(dimension);
|
||||
|
||||
return Commands.literal(name).requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), name))
|
||||
.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, false, ModdedPregenMode.ASYNC, false))
|
||||
.then(pregenStartFlag("gui", false, true, ModdedPregenMode.ASYNC, false))
|
||||
.then(pregenStartFlag("sync", false, false, ModdedPregenMode.SYNC, false))
|
||||
.then(pregenStartFlag("cached", false, false, ModdedPregenMode.SYNC, true))
|
||||
.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"), false, ModdedPregenMode.ASYNC, false))
|
||||
.then(pregenStartFlag("gui", true, true, ModdedPregenMode.ASYNC, false))
|
||||
.then(pregenStartFlag("sync", true, false, ModdedPregenMode.SYNC, false))
|
||||
.then(pregenStartFlag("cached", true, false, ModdedPregenMode.SYNC, true))))
|
||||
.then(Commands.argument("dimension", StringArgumentType.greedyString()).suggests(DIMENSION_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(),
|
||||
IntegerArgumentType.getInteger(context, "radius"), 0, 0, false, ModdedPregenMode.ASYNC, false,
|
||||
StringArgumentType.getString(context, "dimension"))))))
|
||||
.then(radius))
|
||||
.then(Commands.literal("stop")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStop(context.getSource())))
|
||||
.then(Commands.literal("x")
|
||||
@@ -264,12 +287,32 @@ public final class IrisModdedCommands {
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStatus(context.getSource())));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> pregenStartFlag(String name, boolean withCenter, boolean gui, ModdedPregenMode mode, boolean cached) {
|
||||
return Commands.literal(name).executes((CommandContext<CommandSourceStack> context) -> pregenStart(context.getSource(),
|
||||
IntegerArgumentType.getInteger(context, "radius"),
|
||||
withCenter ? IntegerArgumentType.getInteger(context, "x") : 0,
|
||||
withCenter ? IntegerArgumentType.getInteger(context, "z") : 0,
|
||||
gui, mode, cached));
|
||||
private static void attachPregenCenter(ArgumentBuilder<CommandSourceStack, ?> node, boolean withDimension) {
|
||||
RequiredArgumentBuilder<CommandSourceStack, Integer> z = Commands.argument("z", IntegerArgumentType.integer())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context, withDimension, true, false, false, false));
|
||||
attachPregenFlags(z, withDimension, true, false, false, false);
|
||||
node.then(Commands.literal("at")
|
||||
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||
.then(z)));
|
||||
}
|
||||
|
||||
private static void attachPregenFlags(ArgumentBuilder<CommandSourceStack, ?> node, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) {
|
||||
if (!gui) {
|
||||
node.then(pregenFlagNode("gui", withDimension, withCenter, true, sync, nocache));
|
||||
}
|
||||
if (!sync) {
|
||||
node.then(pregenFlagNode("sync", withDimension, withCenter, gui, true, nocache));
|
||||
}
|
||||
if (!nocache) {
|
||||
node.then(pregenFlagNode("nocache", withDimension, withCenter, gui, sync, true));
|
||||
}
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> pregenFlagNode(String name, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) {
|
||||
LiteralArgumentBuilder<CommandSourceStack> flag = Commands.literal(name)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> pregenStart(context, withDimension, withCenter, gui, sync, nocache));
|
||||
attachPregenFlags(flag, withDimension, withCenter, gui, sync, nocache);
|
||||
return flag;
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> goldenhashTree(String name) {
|
||||
@@ -302,21 +345,176 @@ public final class IrisModdedCommands {
|
||||
}
|
||||
|
||||
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 Commands.literal("edit").requires(GATE)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> ModdedCommandHelp.send(context.getSource(), "edit"))
|
||||
.then(Commands.literal("biome")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> editBiome(context.getSource(), null))
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(BIOME_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> editBiome(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("region")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> editRegion(context.getSource(), null))
|
||||
.then(Commands.argument("key", StringArgumentType.greedyString()).suggests(REGION_KEYS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> editRegion(context.getSource(), StringArgumentType.getString(context, "key")))))
|
||||
.then(Commands.literal("dimension")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> editDimension(context.getSource())));
|
||||
}
|
||||
|
||||
private static int editBiome(CommandSourceStack source, String key) {
|
||||
Engine engine = engineFor(source.getLevel());
|
||||
if (engine == null) {
|
||||
fail(source, "This dimension is not generated by Iris.");
|
||||
return 0;
|
||||
}
|
||||
return node;
|
||||
IrisBiome biome;
|
||||
if (key == null || key.isBlank()) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
fail(source, "Console must name a biome: /iris edit biome <key>");
|
||||
return 0;
|
||||
}
|
||||
BlockPos pos = player.blockPosition();
|
||||
try {
|
||||
biome = engine.getBiome(pos.getX(), pos.getY() - engine.getMinHeight(), pos.getZ());
|
||||
} catch (Throwable e) {
|
||||
fail(source, "Biome lookup failed: " + e.getClass().getSimpleName());
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
biome = engine.getData().getBiomeLoader().load(key.trim());
|
||||
if (biome == null) {
|
||||
fail(source, "Unknown biome: " + key);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return openJson(source, biome);
|
||||
}
|
||||
|
||||
private static int editRegion(CommandSourceStack source, String key) {
|
||||
Engine engine = engineFor(source.getLevel());
|
||||
if (engine == null) {
|
||||
fail(source, "This dimension is not generated by Iris.");
|
||||
return 0;
|
||||
}
|
||||
IrisRegion region;
|
||||
if (key == null || key.isBlank()) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
fail(source, "Console must name a region: /iris edit region <key>");
|
||||
return 0;
|
||||
}
|
||||
BlockPos pos = player.blockPosition();
|
||||
try {
|
||||
region = engine.getRegion(pos.getX(), pos.getZ());
|
||||
} catch (Throwable e) {
|
||||
fail(source, "Region lookup failed: " + e.getClass().getSimpleName());
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
region = engine.getData().getRegionLoader().load(key.trim());
|
||||
if (region == null) {
|
||||
fail(source, "Unknown region: " + key);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return openJson(source, region);
|
||||
}
|
||||
|
||||
private static int editDimension(CommandSourceStack source) {
|
||||
Engine engine = engineFor(source.getLevel());
|
||||
if (engine == null) {
|
||||
fail(source, "This dimension is not generated by Iris.");
|
||||
return 0;
|
||||
}
|
||||
return openJson(source, engine.getDimension());
|
||||
}
|
||||
|
||||
private static int openJson(CommandSourceStack source, IrisRegistrant registrant) {
|
||||
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
|
||||
fail(source, "Cannot open files here: " + ModdedGuiHost.guiUnavailableReason());
|
||||
return 0;
|
||||
}
|
||||
if (registrant == null || registrant.getLoadFile() == null || !registrant.getLoadFile().isFile()) {
|
||||
fail(source, "Cannot find the file; perhaps it was not loaded directly from a file?");
|
||||
return 0;
|
||||
}
|
||||
File file = registrant.getLoadFile();
|
||||
try {
|
||||
Desktop.getDesktop().open(file);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris edit failed to open {}", file, e);
|
||||
fail(source, "Could not open " + file.getName() + ": " + e.getClass().getSimpleName());
|
||||
return 0;
|
||||
}
|
||||
ok(source, "Opening " + registrant.getTypeName() + " " + file.getName() + " in your editor.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int tp(CommandSourceStack source, ServerLevel level, ServerPlayer target) {
|
||||
ServerPlayer player = target != null ? target : source.getPlayer();
|
||||
if (player == null) {
|
||||
fail(source, "Console must name a player: /iris tp <dimension> <player>");
|
||||
return 0;
|
||||
}
|
||||
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) {
|
||||
fail(source, level.dimension().identifier() + " is not generated by Iris.");
|
||||
return 0;
|
||||
}
|
||||
String dimensionId = level.dimension().identifier().toString();
|
||||
if (!ModdedDimensionManager.teleport(player, source.getServer(), dimensionId, 8.5D, Double.MIN_VALUE, 8.5D)) {
|
||||
fail(source, "Teleport failed: dimension " + dimensionId + " is not loaded.");
|
||||
return 0;
|
||||
}
|
||||
ok(source, "Teleporting " + player.getScoreboardName() + " to " + dimensionId + "...");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int evacuate(CommandSourceStack source, ServerLevel target) {
|
||||
MinecraftServer server = source.getServer();
|
||||
ServerLevel level = target != null ? target : source.getLevel();
|
||||
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) {
|
||||
fail(source, level.dimension().identifier() + " is not generated by Iris.");
|
||||
return 0;
|
||||
}
|
||||
ServerLevel fallback = server.overworld();
|
||||
if (fallback == level) {
|
||||
fail(source, "Cannot evacuate the primary world; there is nowhere to send players.");
|
||||
return 0;
|
||||
}
|
||||
int count = ModdedDimensionManager.evacuate(server, level);
|
||||
ok(source, "Evacuated " + count + " player(s) from " + level.dimension().identifier() + " to " + fallback.dimension().identifier() + ".");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int debug(CommandSourceStack source) {
|
||||
boolean to = !IrisSettings.get().getGeneral().isDebug();
|
||||
IrisSettings.get().getGeneral().setDebug(to);
|
||||
IrisSettings.get().forceSave();
|
||||
ok(source, "Set debug to: " + to);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int reload(CommandSourceStack source) {
|
||||
if (IrisSettings.settings != null) {
|
||||
IrisSettings.invalidate();
|
||||
}
|
||||
IrisSettings.get();
|
||||
ok(source, "Hotloaded settings");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int whatHand(CommandSourceStack source) {
|
||||
ServerPlayer player = source.getPlayer();
|
||||
if (player == null) {
|
||||
fail(source, "This command can only be used by players (it inspects your held item).");
|
||||
return 0;
|
||||
}
|
||||
ItemStack stack = player.getMainHandItem();
|
||||
if (stack.isEmpty()) {
|
||||
fail(source, "Your main hand is empty.");
|
||||
return 0;
|
||||
}
|
||||
ok(source, "Hand: " + BuiltInRegistries.ITEM.getKey(stack.getItem()) + " x" + stack.getCount());
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int regen(CommandSourceStack source, int radius) {
|
||||
@@ -339,28 +537,23 @@ public final class IrisModdedCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int pregenStart(CommandSourceStack source, int radius, int centerX, int centerZ, boolean gui, ModdedPregenMode mode, boolean cached) {
|
||||
return pregenStart(source, radius, centerX, centerZ, gui, mode, cached, null);
|
||||
}
|
||||
|
||||
private static int pregenStart(CommandSourceStack source, int radius, int centerX, int centerZ, boolean gui, ModdedPregenMode mode, boolean cached, String dimension) {
|
||||
ServerLevel level;
|
||||
if (dimension == null || dimension.isBlank()) {
|
||||
level = source.getLevel();
|
||||
} else {
|
||||
level = resolveIrisLevel(source.getServer(), dimension);
|
||||
if (level == null) {
|
||||
fail(source, "No loaded Iris dimension matches '" + dimension + "'. Use a bare name (irisworld) or full id (irisworldgen:irisworld); see /iris info for loaded dimensions.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
private static int pregenStart(CommandContext<CommandSourceStack> context, boolean withDimension, boolean withCenter, boolean gui, boolean sync, boolean nocache) throws CommandSyntaxException {
|
||||
CommandSourceStack source = context.getSource();
|
||||
int radius = IntegerArgumentType.getInteger(context, "radius");
|
||||
int centerX = withCenter ? IntegerArgumentType.getInteger(context, "x") : 0;
|
||||
int centerZ = withCenter ? IntegerArgumentType.getInteger(context, "z") : 0;
|
||||
ServerLevel level = withDimension ? DimensionArgument.getDimension(context, "dimension") : source.getLevel();
|
||||
Engine engine = engineFor(level);
|
||||
if (engine == null) {
|
||||
fail(source, "This dimension is not generated by Iris.");
|
||||
if (withDimension) {
|
||||
fail(source, level.dimension().identifier() + " is not generated by Iris; see /iris info for loaded Iris dimensions.");
|
||||
} else {
|
||||
fail(source, "The current dimension (" + level.dimension().identifier() + ") is not generated by Iris. Name one explicitly: /iris pregen start " + radius + " <dimension>; see /iris info for loaded Iris dimensions.");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
boolean showGui = gui && ModdedGuiHost.isGuiLaunchable();
|
||||
if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, mode, cached)) {
|
||||
if (!ModdedPregenJob.start(source.getServer(), level, engine, radius, centerX, centerZ, showGui, sync, !nocache)) {
|
||||
fail(source, "A pregeneration task is already running. Stop it first with /iris pregen stop.");
|
||||
return 0;
|
||||
}
|
||||
@@ -372,7 +565,7 @@ public final class IrisModdedCommands {
|
||||
} else {
|
||||
guiNote = " (GUI requested but unavailable: " + ModdedGuiHost.guiUnavailableReason() + ")";
|
||||
}
|
||||
String modeNote = mode == ModdedPregenMode.SYNC ? " Mode: sync" + (cached ? " (checkpoint cache resumable)." : ".") : "";
|
||||
String modeNote = " Mode: " + (sync ? "sync" : "async") + (nocache ? ", cache disabled." : ", resumable (checkpoint cache).");
|
||||
ok(source, "Pregen started in " + level.dimension().identifier() + " of " + (radius * 2) + " by " + (radius * 2)
|
||||
+ " blocks from " + centerX + "," + centerZ + "." + modeNote + " Progress logs to console; see /iris pregen status." + guiNote);
|
||||
return 1;
|
||||
@@ -786,14 +979,15 @@ public final class IrisModdedCommands {
|
||||
|
||||
private static int download(CommandSourceStack source, String pack, String branch) {
|
||||
MinecraftServer server = source.getServer();
|
||||
ok(source, "Downloading IrisDimensions/" + pack + " (branch " + branch + ")...");
|
||||
String effectiveBranch = pack.equals("overworld") ? "master" : branch;
|
||||
ok(source, "Downloading IrisDimensions/" + pack + " (branch " + effectiveBranch + ")...");
|
||||
Thread thread = new Thread(() -> {
|
||||
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, branch,
|
||||
boolean installed = ModdedPackInstaller.install(ModdedEngineBootstrap.loader().configDir(), pack, effectiveBranch,
|
||||
(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."));
|
||||
server.execute(() -> ok(source, "Pack '" + pack + "' installed. Its dimension types and custom biomes join the forced Iris datapack on the next server restart; worlds created before restarting run with fallback heights until then."));
|
||||
} else {
|
||||
server.execute(() -> fail(source, "Pack download failed for " + pack + "/" + branch + " (see console)."));
|
||||
server.execute(() -> fail(source, "Pack download failed for " + pack + "/" + effectiveBranch + " (see console)."));
|
||||
}
|
||||
}, "Iris Pack Download");
|
||||
thread.setDaemon(true);
|
||||
@@ -843,29 +1037,6 @@ public final class IrisModdedCommands {
|
||||
return count;
|
||||
}
|
||||
|
||||
private static ServerLevel resolveIrisLevel(MinecraftServer server, String raw) {
|
||||
String target = raw.trim().toLowerCase(Locale.ROOT);
|
||||
boolean qualified = target.indexOf(':') >= 0;
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator)) {
|
||||
continue;
|
||||
}
|
||||
String fullId = level.dimension().identifier().toString();
|
||||
if (qualified) {
|
||||
if (fullId.equals(target)) {
|
||||
return level;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int separator = fullId.indexOf(':');
|
||||
String path = separator >= 0 ? fullId.substring(separator + 1) : fullId;
|
||||
if (path.equals(target)) {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static CompletableFuture<Suggestions> suggestBiomeKeys(CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) {
|
||||
ModdedCommandFeedback.tab(context.getSource());
|
||||
try {
|
||||
|
||||
+32
-16
@@ -52,18 +52,24 @@ final class ModdedCommandHelp {
|
||||
SECTIONS.put("", List.of(
|
||||
Entry.command("version", "", "Print version information"),
|
||||
Entry.command("info", "[dimension]", "List loaded Iris dimensions and pack details"),
|
||||
Entry.command("what", "", "Inspect the Iris biome, region, cave biome, surface and chunk at your position"),
|
||||
Entry.command("what", "[block|hand|markers]", "Inspect the Iris biome, region, cave biome, surface and chunk at your position, the block you look at, your held item, or nearby markers"),
|
||||
Entry.group("find", "Find and teleport to Iris biomes, regions, objects, structures and points of interest", "goto"),
|
||||
Entry.command("tp", "<dimension> [player]", "Teleport yourself or a named player into a loaded Iris dimension"),
|
||||
Entry.command("evacuate", "[dimension]", "Teleport every player out of an Iris dimension to the primary world spawn"),
|
||||
Entry.command("seed", "", "Print world and engine seed information"),
|
||||
Entry.command("debug", "", "Toggle Iris debug logging and save settings.json"),
|
||||
Entry.command("reload", "", "Reload settings.json (also hotloaded automatically every 3s)"),
|
||||
Entry.command("download", "<pack> [branch]", "Download a pack project", "dl"),
|
||||
Entry.command("metrics", "", "Print generation metrics for your current Iris dimension", "measure"),
|
||||
Entry.command("regen", "[radius]", "Delete and regenerate nearby chunks in place", "rg"),
|
||||
Entry.group("pregen", "Pregenerate an Iris dimension", "pregenerate"),
|
||||
Entry.command("wand", "", "Get an Iris object wand"),
|
||||
Entry.group("object", "Object wand, save, paste, analyze and undo tools", "o"),
|
||||
Entry.group("edit", "Open pack biome, region and dimension json files in your desktop editor"),
|
||||
Entry.command("create", "<name> <pack|pack:dimensionKey> [seed]", "Create and inject a persistent Iris dimension; quote pack:dimensionKey to pick a specific pack dimension"),
|
||||
Entry.group("studio", "Pack project creation, packaging and reports", "std", "s"),
|
||||
Entry.group("pack", "Pack validation and maintenance", "pk"),
|
||||
Entry.group("world", "Explicit Iris dimension enablement and removal", "w"),
|
||||
Entry.group("world", "Runtime Iris dimension creation, removal and status", "w"),
|
||||
Entry.group("datapack", "World datapack install and status helpers", "datapacks", "dp"),
|
||||
Entry.group("structure", "Iris structure index, info and placement tools", "struct", "str"),
|
||||
Entry.command("goldenhash", "[radius] [threads] [capture|verify]", "Generate deterministic block hashes for parity testing", "gold")
|
||||
@@ -76,8 +82,13 @@ final class ModdedCommandHelp {
|
||||
Entry.command("poi", "<type>", "Find a supported point of interest")
|
||||
));
|
||||
SECTIONS.put("goto", SECTIONS.get("find"));
|
||||
SECTIONS.put("edit", List.of(
|
||||
Entry.command("biome", "[key]", "Open a biome json in your desktop editor; no key opens the biome at your position"),
|
||||
Entry.command("region", "[key]", "Open a region json in your desktop editor; no key opens the region at your position"),
|
||||
Entry.command("dimension", "", "Open the current pack's dimension json in your desktop editor")
|
||||
));
|
||||
SECTIONS.put("pregen", List.of(
|
||||
Entry.command("start", "<radius> [x] [z]", "Start pregeneration"),
|
||||
Entry.command("start", "<radius> [dimension] [at] [x] [z] [gui] [sync] [nocache]", "Start pregeneration; radius in blocks, resumable checkpoint cache on by default, center via 'at <x> <z>', flags compose in any order"),
|
||||
Entry.command("stop", "", "Stop the active pregeneration task", "x"),
|
||||
Entry.command("pause", "", "Pause or resume pregeneration", "resume"),
|
||||
Entry.command("status", "", "Show pregeneration status")
|
||||
@@ -86,15 +97,15 @@ final class ModdedCommandHelp {
|
||||
SECTIONS.put("object", List.of(
|
||||
Entry.command("wand", "", "Get an Iris object wand"),
|
||||
Entry.command("dust", "", "Get dust that reveals object placements", "d"),
|
||||
Entry.command("save", "<name>", "Save the selected wand volume as an object"),
|
||||
Entry.command("paste", "<key>", "Paste an object at your position"),
|
||||
Entry.command("save", "[overwrite] <name>", "Save the selected wand volume as an object"),
|
||||
Entry.command("paste", "[at] [x] [y] [z] [rotate] [degrees] <key>", "Paste an object at your position or a given position, optionally rotated"),
|
||||
Entry.command("expand", "[amount]", "Expand the wand selection in your looking direction"),
|
||||
Entry.command("contract", "[amount]", "Contract the wand selection in your looking direction", "-"),
|
||||
Entry.command("shift", "[amount]", "Shift the wand selection in your looking direction"),
|
||||
Entry.command("position1", "[look]", "Set selection point 1", "p1"),
|
||||
Entry.command("position2", "[look]", "Set selection point 2", "p2"),
|
||||
Entry.command("x+y", "", "Autoselect up and out"),
|
||||
Entry.command("x&y", "", "Autoselect up, down and out"),
|
||||
Entry.command("x+y", "", "Autoselect up and out", "xpy"),
|
||||
Entry.command("x&y", "", "Autoselect up, down and out", "xay"),
|
||||
Entry.command("analyze", "<key>", "Show object composition"),
|
||||
Entry.command("shrink", "<key>", "Shrink an object to its minimum size"),
|
||||
Entry.command("undo", "[amount]", "Undo pasted objects", "u")
|
||||
@@ -105,10 +116,14 @@ final class ModdedCommandHelp {
|
||||
Entry.command("package", "[pack]", "Package a dimension into a compressed format"),
|
||||
Entry.command("version", "[pack]", "Print a pack version"),
|
||||
Entry.command("regions", "[radius]", "Calculate nearby region distribution"),
|
||||
Entry.command("open", "<pack> [seed]", "Open or prepare a dimension pack studio workflow"),
|
||||
Entry.command("close", "", "Explain modded studio workflow"),
|
||||
Entry.command("vscode", "", "Explain editor workflow"),
|
||||
Entry.command("update", "", "Explain workspace regeneration workflow"),
|
||||
Entry.command("open", "<pack> [seed]", "Open a temporary studio dimension for a pack", "o"),
|
||||
Entry.command("close", "", "Close the open studio dimension and discard its world", "x"),
|
||||
Entry.command("tpstudio", "", "Teleport into the open studio dimension", "stp"),
|
||||
Entry.command("status", "", "Show the open studio dimension and its pack"),
|
||||
Entry.command("noise", "[generator] [seed]", "Open the Noise Explorer GUI on the server display", "nmap"),
|
||||
Entry.command("map", "", "Open the Vision map GUI on the server display", "render"),
|
||||
Entry.command("vscode", "[pack]", "Regenerate the .code-workspace for a pack and open it in your desktop editor", "vsc"),
|
||||
Entry.command("update", "[pack]", "Regenerate the .code-workspace for a pack"),
|
||||
Entry.command("importvanilla", "", "Explain vanilla import workflow", "importv", "iv")
|
||||
));
|
||||
SECTIONS.put("std", SECTIONS.get("studio"));
|
||||
@@ -120,11 +135,12 @@ final class ModdedCommandHelp {
|
||||
));
|
||||
SECTIONS.put("pk", SECTIONS.get("pack"));
|
||||
SECTIONS.put("world", List.of(
|
||||
Entry.command("enable", "<dimension> <pack> [packDimension]", "Create an Iris dimension in world/datapacks/iris", "create"),
|
||||
Entry.command("replace-overworld", "<pack> [packDimension]", "Explicitly make minecraft:overworld use an Iris pack"),
|
||||
Entry.command("disable", "<dimension>", "Remove an Iris dimension definition from the world datapack", "remove", "rm"),
|
||||
Entry.command("list", "", "List Iris dimensions staged in the world datapack", "ls"),
|
||||
Entry.command("status", "", "Show staged and currently loaded Iris dimensions")
|
||||
Entry.command("enable", "<dimension> <pack|pack:dimensionKey> [seed|random]", "Create and inject a persistent Iris dimension at runtime; downloads the pack if missing, quote pack:dimensionKey to pick a specific pack dimension", "create"),
|
||||
Entry.command("replace-overworld", "<pack|pack:dimensionKey> [seed|random]", "Inject an Iris primary world and route players there instead of the vanilla overworld"),
|
||||
Entry.command("disable", "<dimension>", "Evacuate and unload an Iris dimension; world data on disk is kept for re-enabling"),
|
||||
Entry.command("delete", "<dimension>", "Disable an Iris dimension and wipe its chunk and mantle data from disk", "remove", "rm"),
|
||||
Entry.command("list", "", "List loaded Iris dimensions", "ls"),
|
||||
Entry.command("status", "", "Show loaded Iris dimensions and the configured primary world")
|
||||
));
|
||||
SECTIONS.put("w", SECTIONS.get("world"));
|
||||
SECTIONS.put("datapack", List.of(
|
||||
|
||||
+88
-304
@@ -18,13 +18,13 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.runtime.GoldenHashEngine;
|
||||
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.server.MinecraftServer;
|
||||
@@ -33,21 +33,6 @@ 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;
|
||||
|
||||
@@ -59,33 +44,38 @@ public final class ModdedGoldenHash {
|
||||
}
|
||||
|
||||
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 final GoldenHashEngine hashEngine;
|
||||
|
||||
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;
|
||||
GoldenHashEngine.Mode engineMode = switch (mode) {
|
||||
case AUTO -> GoldenHashEngine.Mode.AUTO;
|
||||
case CAPTURE -> GoldenHashEngine.Mode.CAPTURE;
|
||||
case VERIFY -> GoldenHashEngine.Mode.VERIFY;
|
||||
};
|
||||
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");
|
||||
GoldenHashEngine.Request request = new GoldenHashEngine.Request(
|
||||
engine.getWorld().name(),
|
||||
level.getSeed(),
|
||||
ModdedEngineBootstrap.loader().minecraftVersion(),
|
||||
engine.getMinHeight(),
|
||||
engine.getMaxHeight(),
|
||||
0,
|
||||
0,
|
||||
radius,
|
||||
threads,
|
||||
engineMode,
|
||||
true,
|
||||
false,
|
||||
"minecraft:plains");
|
||||
this.hashEngine = new GoldenHashEngine(engine, request, goldenDir, this::snapshot, feedback(), progress());
|
||||
}
|
||||
|
||||
public static void start(CommandSourceStack source, ServerLevel level, Engine engine, int radius, int threads, Mode mode) {
|
||||
@@ -94,16 +84,14 @@ public final class ModdedGoldenHash {
|
||||
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);
|
||||
int boundedRadius = Math.max(0, radius);
|
||||
int chunks = (boundedRadius * 2 + 1) * (boundedRadius * 2 + 1);
|
||||
scan.ok("GoldenHash started: " + chunks + " chunk(s) around 0,0 in buffers (world untouched), threads=" + Math.max(1, 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());
|
||||
engine.getDimension().getLoadKey(), level.getSeed(), boundedRadius, Math.max(1, threads), mode, scan.hashEngine.getGoldenFile().getName());
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
scan.run();
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("goldenhash failed", e);
|
||||
scan.fail("GoldenHash failed: " + e);
|
||||
scan.hashEngine.run();
|
||||
} finally {
|
||||
ACTIVE.set(false);
|
||||
}
|
||||
@@ -112,265 +100,77 @@ public final class ModdedGoldenHash {
|
||||
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();
|
||||
private GoldenHashEngine.ChunkSnapshot snapshot(int chunkX, int chunkZ) throws Exception {
|
||||
int minY = engine.getMinHeight();
|
||||
int height = engine.getMaxHeight() - minY;
|
||||
PlatformBlockState air = IrisPlatforms.get().registries().air();
|
||||
ModdedBlockBuffer blocks = new ModdedBlockBuffer(height, air);
|
||||
Hunk<PlatformBiome> biomes = Hunk.newArrayHunk(16, height, 16);
|
||||
engine.generate(chunkX << 4, chunkZ << 4, blocks, biomes, false);
|
||||
return new GoldenHashEngine.ChunkSnapshot() {
|
||||
@Override
|
||||
public int minY() {
|
||||
return minY;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@Override
|
||||
public int maxY() {
|
||||
return minY + height;
|
||||
}
|
||||
|
||||
done.await();
|
||||
return lines;
|
||||
@Override
|
||||
public PlatformBlockState block(int x, int y, int z) {
|
||||
return blocks.get(x, y - minY, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBiome biome(int x, int y, int z) {
|
||||
return biomes.get(x, y - minY, z);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
private GoldenHashEngine.Feedback feedback() {
|
||||
return new GoldenHashEngine.Feedback() {
|
||||
@Override
|
||||
public void ok(String message) {
|
||||
ModdedGoldenHash.this.ok(message);
|
||||
}
|
||||
|
||||
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);
|
||||
@Override
|
||||
public void warn(String message) {
|
||||
ModdedGoldenHash.this.ok(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fail(String message) {
|
||||
ModdedGoldenHash.this.fail(message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private GoldenHashEngine.Progress progress() {
|
||||
return new GoldenHashEngine.Progress() {
|
||||
private final AtomicInteger hashed = new AtomicInteger();
|
||||
|
||||
@Override
|
||||
public void chunkDone(int chunkX, int chunkZ, boolean ok, int done, int total) {
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
int doneCount = hashed.incrementAndGet();
|
||||
int stride = total <= 64 ? 1 : 32;
|
||||
if (doneCount % stride == 0 || doneCount == total) {
|
||||
ModdedGoldenHash.this.ok("[" + doneCount + "/" + total + "] chunk " + chunkX + "," + chunkZ + " hashed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@Override
|
||||
public void chunkFailed(int chunkX, int chunkZ, Throwable error) {
|
||||
LOGGER.error("goldenhash chunk {},{} failed", chunkX, chunkZ, error);
|
||||
ModdedGoldenHash.this.fail("Chunk " + chunkX + "," + chunkZ + " FAILED: " + error.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -380,20 +180,4 @@ public final class ModdedGoldenHash {
|
||||
private void fail(String message) {
|
||||
server.execute(() -> IrisModdedCommands.fail(source, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-2
@@ -26,12 +26,14 @@ import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class ModdedGuiHost implements GuiHost.Provider {
|
||||
private static final ModdedGuiHost INSTANCE = new ModdedGuiHost();
|
||||
|
||||
private final Map<Engine, ServerLevel> levels = new ConcurrentHashMap<>();
|
||||
private final Map<Engine, UUID> openers = new ConcurrentHashMap<>();
|
||||
private volatile Engine active;
|
||||
private volatile MinecraftServer server;
|
||||
|
||||
@@ -42,10 +44,15 @@ public final class ModdedGuiHost implements GuiHost.Provider {
|
||||
GuiHost.set(INSTANCE);
|
||||
}
|
||||
|
||||
public static void bindContext(MinecraftServer server, ServerLevel level, Engine engine) {
|
||||
public static void bindContext(MinecraftServer server, ServerLevel level, Engine engine, UUID opener) {
|
||||
INSTANCE.server = server;
|
||||
INSTANCE.active = engine;
|
||||
INSTANCE.levels.put(engine, level);
|
||||
if (opener == null) {
|
||||
INSTANCE.openers.remove(engine);
|
||||
} else {
|
||||
INSTANCE.openers.put(engine, opener);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isGuiLaunchable() {
|
||||
@@ -53,6 +60,9 @@ public final class ModdedGuiHost implements GuiHost.Provider {
|
||||
}
|
||||
|
||||
public static String guiUnavailableReason() {
|
||||
if (GuiHost.isDesktopSuppressed()) {
|
||||
return "running inside a Minecraft client (desktop GUIs disabled to avoid a client crash)";
|
||||
}
|
||||
if (!GuiHost.isAvailable()) {
|
||||
return "headless JVM (no display)";
|
||||
}
|
||||
@@ -82,6 +92,6 @@ public final class ModdedGuiHost implements GuiHost.Provider {
|
||||
if (level == null || server == null) {
|
||||
return null;
|
||||
}
|
||||
return new ModdedVisionOverlay(server, level, engine);
|
||||
return new ModdedVisionOverlay(server, level, engine, openers.get(engine));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ public final class ModdedPackCommands {
|
||||
return root;
|
||||
}
|
||||
|
||||
static File packsRoot() {
|
||||
public static File packsRoot() {
|
||||
return ModdedEngineBootstrap.loader().configDir().resolve("irisworldgen").resolve("packs").toFile();
|
||||
}
|
||||
|
||||
|
||||
+46
-254
@@ -18,10 +18,8 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.gui.PregenRenderSource;
|
||||
import art.arcane.iris.core.gui.PregenRenderer;
|
||||
import art.arcane.iris.core.pregenerator.IrisPregenerator;
|
||||
import art.arcane.iris.core.pregenerator.PregenListener;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.pregenerator.PregenPerformanceProfile;
|
||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||
import art.arcane.iris.core.pregenerator.cache.PregenCache;
|
||||
@@ -35,60 +33,34 @@ 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.awt.Color;
|
||||
import java.io.File;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class ModdedPregenJob implements PregenListener, PregenRenderSource {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final AtomicReference<ModdedPregenJob> ACTIVE = new AtomicReference<>();
|
||||
private static final Color COLOR_GENERATING = new Color(0x66967f);
|
||||
private static final Color COLOR_GENERATED = new Color(0x65c295);
|
||||
public final class ModdedPregenJob {
|
||||
private static volatile String dimension = "?";
|
||||
|
||||
private final String dimension;
|
||||
private final IrisPregenerator pregenerator;
|
||||
private final Engine engine;
|
||||
private final Position2 min;
|
||||
private final Position2 max;
|
||||
private final PregenRenderer renderer;
|
||||
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() {
|
||||
}
|
||||
|
||||
private ModdedPregenJob(MinecraftServer server, ServerLevel level, Engine engine, PregenTask task, boolean gui, ModdedPregenMode mode, boolean cached) {
|
||||
this.dimension = level.dimension().identifier().toString();
|
||||
this.engine = engine;
|
||||
this.min = new Position2(Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||
this.max = new Position2(Integer.MIN_VALUE, Integer.MIN_VALUE);
|
||||
task.iterateAllChunks((int chunkX, int chunkZ) -> {
|
||||
min.setX(Math.min(chunkX, min.getX()));
|
||||
min.setZ(Math.min(chunkZ, min.getZ()));
|
||||
max.setX(Math.max(chunkX, max.getX()));
|
||||
max.setZ(Math.max(chunkZ, max.getZ()));
|
||||
});
|
||||
PregeneratorMethod baseMethod = new ModdedPregenMethod(level, engine, mode);
|
||||
PregeneratorMethod resolvedMethod = baseMethod;
|
||||
public static boolean start(MinecraftServer server, ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean gui, boolean sync, boolean cached) {
|
||||
if (PregeneratorJob.getInstance() != null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PregenPerformanceProfile.apply(engine);
|
||||
PregenTask task = PregenTask.builder()
|
||||
.gui(gui)
|
||||
.center(new Position2(centerBlockX, centerBlockZ))
|
||||
.radiusX(radiusBlocks)
|
||||
.radiusZ(radiusBlocks)
|
||||
.build();
|
||||
PregeneratorMethod method = new ModdedPregenMethod(level, engine, sync);
|
||||
if (cached) {
|
||||
PregenCache cache = PregenCache.create(cacheDirectory(level)).sync();
|
||||
resolvedMethod = new CachedPregenMethod(baseMethod, cache);
|
||||
method = new CachedPregenMethod(method, PregenCache.create(cacheDirectory(level)).sync(), task);
|
||||
}
|
||||
this.method = mode == ModdedPregenMode.SYNC ? "Modded Sync" : "Modded";
|
||||
this.pregenerator = new IrisPregenerator(task, resolvedMethod, this);
|
||||
PregenRenderer openedRenderer = null;
|
||||
if (gui) {
|
||||
try {
|
||||
openedRenderer = PregenRenderer.open("Iris Pregen: " + dimension, this, ModdedPregenJob::pauseResume);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris pregen GUI failed to open for {}", dimension, e);
|
||||
}
|
||||
}
|
||||
this.renderer = openedRenderer;
|
||||
dimension = level.dimension().identifier().toString();
|
||||
new PregeneratorJob(task, method, engine);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static File cacheDirectory(ServerLevel level) {
|
||||
@@ -96,119 +68,53 @@ public final class ModdedPregenJob implements PregenListener, PregenRenderSource
|
||||
return new File(worldFolder, "iris" + File.separator + "pregen");
|
||||
}
|
||||
|
||||
public static boolean start(MinecraftServer server, ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean gui) {
|
||||
return start(server, level, engine, radiusBlocks, centerBlockX, centerBlockZ, gui, ModdedPregenMode.ASYNC, false);
|
||||
}
|
||||
|
||||
public static boolean start(MinecraftServer server, ServerLevel level, Engine engine, int radiusBlocks, int centerBlockX, int centerBlockZ, boolean gui, ModdedPregenMode mode, boolean cached) {
|
||||
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(server, level, engine, task, gui, mode, cached);
|
||||
if (!ACTIVE.compareAndSet(null, job)) {
|
||||
job.closeRenderer();
|
||||
return false;
|
||||
}
|
||||
Thread thread = new Thread(() -> {
|
||||
try {
|
||||
job.pregenerator.start();
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris pregen failed for {}", job.dimension, e);
|
||||
} finally {
|
||||
job.closeRenderer();
|
||||
ACTIVE.compareAndSet(job, null);
|
||||
}
|
||||
}, "Iris Pregen");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void closeRenderer() {
|
||||
if (renderer != null) {
|
||||
renderer.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void draw(int chunkX, int chunkZ, Color color) {
|
||||
if (renderer == null || !renderer.isVisibleFrame()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Color resolved = color;
|
||||
if (engine != null) {
|
||||
resolved = engine.draw((chunkX << 4) + 8, (chunkZ << 4) + 8);
|
||||
}
|
||||
renderer.submit(chunkX, chunkZ, resolved);
|
||||
} catch (Throwable e) {
|
||||
renderer.submit(chunkX, chunkZ, color);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean stop() {
|
||||
ModdedPregenJob job = ACTIVE.get();
|
||||
if (job == null) {
|
||||
return false;
|
||||
}
|
||||
job.pregenerator.close();
|
||||
return true;
|
||||
return PregeneratorJob.shutdownInstance();
|
||||
}
|
||||
|
||||
public static void shutdown() {
|
||||
PregeneratorJob.shutdownAndWait(10_000L);
|
||||
}
|
||||
|
||||
public static Boolean pauseResume() {
|
||||
ModdedPregenJob job = ACTIVE.get();
|
||||
if (job == null) {
|
||||
if (PregeneratorJob.getInstance() == 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;
|
||||
}
|
||||
return job.statusText();
|
||||
PregeneratorJob.pauseResume();
|
||||
return PregeneratorJob.isPaused();
|
||||
}
|
||||
|
||||
public static Component statusComponent() {
|
||||
ModdedPregenJob job = ACTIVE.get();
|
||||
if (job == null) {
|
||||
PregeneratorJob.PregenProgress progress = PregeneratorJob.progressSnapshot();
|
||||
if (progress == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
double percent = job.percent();
|
||||
MutableComponent status = Component.empty();
|
||||
status.append(ModdedCommandFeedback.header("Iris Pregen"));
|
||||
status.append(Component.literal("\n"));
|
||||
status.append(ModdedCommandFeedback.text("Dimension ", ModdedCommandFeedback.DARK_GREEN));
|
||||
status.append(ModdedCommandFeedback.text(job.dimension, ModdedCommandFeedback.PARAMETER_ALT));
|
||||
status.append(ModdedCommandFeedback.text(dimension, ModdedCommandFeedback.PARAMETER_ALT));
|
||||
status.append(ModdedCommandFeedback.text(" · Method ", ModdedCommandFeedback.DARK_GREEN));
|
||||
status.append(ModdedCommandFeedback.text(job.method, ModdedCommandFeedback.PARAMETER));
|
||||
status.append(ModdedCommandFeedback.text(progress.method(), ModdedCommandFeedback.PARAMETER));
|
||||
status.append(Component.literal("\n"));
|
||||
status.append(ModdedCommandFeedback.progressBar(percent, 32));
|
||||
status.append(ModdedCommandFeedback.text(" " + String.format("%.1f", percent) + "%", ModdedCommandFeedback.USAGE));
|
||||
status.append(ModdedCommandFeedback.progressBar(progress.percent(), 32));
|
||||
status.append(ModdedCommandFeedback.text(" " + String.format("%.1f", progress.percent()) + "%", ModdedCommandFeedback.USAGE));
|
||||
status.append(Component.literal("\n"));
|
||||
status.append(ModdedCommandFeedback.text("Chunks ", ModdedCommandFeedback.DARK_GREEN));
|
||||
status.append(ModdedCommandFeedback.text(Form.f(job.generated) + "/" + Form.f(job.totalChunks), ModdedCommandFeedback.VALUE));
|
||||
status.append(ModdedCommandFeedback.text(Form.f(progress.generated()) + "/" + Form.f(progress.totalChunks()), ModdedCommandFeedback.VALUE));
|
||||
status.append(ModdedCommandFeedback.text(" · Speed ", ModdedCommandFeedback.DARK_GREEN));
|
||||
status.append(ModdedCommandFeedback.text(Form.f((int) job.chunksPerSecond) + "/s", ModdedCommandFeedback.VALUE));
|
||||
status.append(ModdedCommandFeedback.text(Form.f((int) progress.chunksPerSecond()) + "/s", ModdedCommandFeedback.VALUE));
|
||||
if (progress.failed() > 0) {
|
||||
status.append(ModdedCommandFeedback.text(" · Failed ", ModdedCommandFeedback.DARK_GREEN));
|
||||
status.append(ModdedCommandFeedback.text(Form.f(progress.failed()), ModdedCommandFeedback.REQUIRED));
|
||||
}
|
||||
status.append(Component.literal("\n"));
|
||||
status.append(ModdedCommandFeedback.text("ETA ", ModdedCommandFeedback.DARK_GREEN));
|
||||
status.append(ModdedCommandFeedback.text(Form.duration(job.eta, 2), ModdedCommandFeedback.VALUE));
|
||||
status.append(ModdedCommandFeedback.text(Form.duration(progress.eta(), 2), ModdedCommandFeedback.VALUE));
|
||||
status.append(ModdedCommandFeedback.text(" · Elapsed ", ModdedCommandFeedback.DARK_GREEN));
|
||||
status.append(ModdedCommandFeedback.text(Form.duration(job.elapsed, 2), ModdedCommandFeedback.VALUE));
|
||||
if (job.pregenerator.paused()) {
|
||||
status.append(ModdedCommandFeedback.text(Form.duration(progress.elapsed(), 2), ModdedCommandFeedback.VALUE));
|
||||
if (progress.paused()) {
|
||||
status.append(ModdedCommandFeedback.text(" · PAUSED", ModdedCommandFeedback.REQUIRED, true, false));
|
||||
}
|
||||
status.append(Component.literal("\n"));
|
||||
@@ -219,118 +125,4 @@ public final class ModdedPregenJob implements PregenListener, PregenRenderSource
|
||||
status.append(ModdedCommandFeedback.footer());
|
||||
return status;
|
||||
}
|
||||
|
||||
private String statusText() {
|
||||
double percent = percent();
|
||||
return "Pregen " + dimension + ": "
|
||||
+ Form.f(generated) + "/" + Form.f(totalChunks)
|
||||
+ " (" + String.format("%.1f", percent) + "%), "
|
||||
+ Form.f((int) chunksPerSecond) + "/s"
|
||||
+ ", ETA " + Form.duration(eta, 2)
|
||||
+ ", elapsed " + Form.duration(elapsed, 2)
|
||||
+ ", method " + method
|
||||
+ (pregenerator.paused() ? ", PAUSED" : "");
|
||||
}
|
||||
|
||||
private double percent() {
|
||||
long total = Math.max(1L, totalChunks);
|
||||
return ((double) generated / (double) total) * 100D;
|
||||
}
|
||||
|
||||
@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) {
|
||||
draw(x, z, COLOR_GENERATING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChunkGenerated(int x, int z, boolean cached) {
|
||||
draw(x, z, COLOR_GENERATED);
|
||||
}
|
||||
|
||||
@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) {
|
||||
draw(x, z, COLOR_GENERATED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position2 min() {
|
||||
return min;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position2 max() {
|
||||
return max;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] progress() {
|
||||
double percent = percent();
|
||||
return new String[]{
|
||||
(pregenerator.paused() ? "PAUSED " : "Generating ") + Form.f(generated) + " of " + Form.f(totalChunks)
|
||||
+ " (" + String.format("%.1f", percent) + "%)",
|
||||
"Speed: " + Form.f((int) chunksPerSecond) + " Chunks/s",
|
||||
Form.duration(eta, 2) + " Remaining (" + Form.duration(elapsed, 2) + " Elapsed)",
|
||||
"Dimension: " + dimension,
|
||||
"Method: " + method
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean paused() {
|
||||
return pregenerator.paused();
|
||||
}
|
||||
}
|
||||
|
||||
+51
-14
@@ -20,6 +20,7 @@ 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.PregenMantleBackpressure;
|
||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
@@ -42,45 +43,76 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
|
||||
private final ServerLevel level;
|
||||
private final Engine engine;
|
||||
private final ModdedPregenMode mode;
|
||||
private final boolean sync;
|
||||
private final Semaphore semaphore;
|
||||
private final int permits;
|
||||
private final int timeoutSeconds;
|
||||
private final PregenMantleBackpressure backpressure;
|
||||
|
||||
public ModdedPregenMethod(ServerLevel level, Engine engine) {
|
||||
this(level, engine, ModdedPregenMode.ASYNC);
|
||||
this(level, engine, false);
|
||||
}
|
||||
|
||||
public ModdedPregenMethod(ServerLevel level, Engine engine, ModdedPregenMode mode) {
|
||||
public ModdedPregenMethod(ServerLevel level, Engine engine, boolean sync) {
|
||||
this.level = level;
|
||||
this.engine = engine;
|
||||
this.mode = mode;
|
||||
this.sync = sync;
|
||||
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());
|
||||
IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen();
|
||||
this.timeoutSeconds = Math.max(120, pregen.getChunkLoadTimeoutSeconds());
|
||||
this.backpressure = new PregenMantleBackpressure(
|
||||
this::getMantle,
|
||||
pregen.getEffectiveResidentTectonicPlates(engine.getHeight()),
|
||||
pregen.getMantleBackpressureWaitMs(),
|
||||
pregen.getMantleBackpressureTimeoutMs(),
|
||||
() -> {
|
||||
},
|
||||
() -> "dim=" + level.dimension().identifier());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
LOGGER.info("Iris modded pregen init: dim={} mode={} inFlightCap={} timeout={}s",
|
||||
level.dimension().identifier(), mode, mode == ModdedPregenMode.ASYNC ? permits : 1, timeoutSeconds);
|
||||
level.dimension().identifier(), sync ? "sync" : "async", sync ? 1 : permits, timeoutSeconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (mode == ModdedPregenMode.ASYNC) {
|
||||
semaphore.acquireUninterruptibly(permits);
|
||||
if (!sync) {
|
||||
try {
|
||||
semaphore.tryAcquire(permits, 5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
saveLevel();
|
||||
saveLevel(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
saveLevel();
|
||||
saveLevel(false);
|
||||
}
|
||||
|
||||
private void saveLevel() {
|
||||
level.getServer().execute(() -> level.save(null, false, false));
|
||||
private void saveLevel(boolean wait) {
|
||||
CompletableFuture<Void> saved = new CompletableFuture<>();
|
||||
level.getServer().execute(() -> {
|
||||
try {
|
||||
level.save(null, false, false);
|
||||
} finally {
|
||||
saved.complete(null);
|
||||
}
|
||||
});
|
||||
if (!wait) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
saved.get(10, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
LOGGER.warn("Iris pregen level save did not complete in time for {}", level.dimension().identifier());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -95,7 +127,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
|
||||
@Override
|
||||
public boolean isAsyncChunkMode() {
|
||||
return mode == ModdedPregenMode.ASYNC;
|
||||
return !sync;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,7 +137,8 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
|
||||
@Override
|
||||
public void generateChunk(int x, int z, PregenListener listener) {
|
||||
if (mode == ModdedPregenMode.SYNC) {
|
||||
backpressure.apply();
|
||||
if (sync) {
|
||||
generateChunkSync(x, z, listener);
|
||||
return;
|
||||
}
|
||||
@@ -122,6 +155,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
Object result = loadFuture.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||
if (result instanceof ChunkResult<?> chunkResult && !chunkResult.isSuccess()) {
|
||||
LOGGER.warn("Iris pregen chunk {},{} returned no chunk: {}", x, z, chunkResult.getError());
|
||||
listener.onChunkFailed(x, z);
|
||||
return;
|
||||
}
|
||||
listener.onChunkGenerated(x, z);
|
||||
@@ -131,6 +165,7 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, e.toString());
|
||||
listener.onChunkFailed(x, z);
|
||||
} finally {
|
||||
level.getServer().execute(() -> level.getChunkSource().removeTicketWithRadius(PREGEN_TICKET, pos, 0));
|
||||
}
|
||||
@@ -155,10 +190,12 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
|
||||
try {
|
||||
if (error != null) {
|
||||
LOGGER.warn("Iris pregen chunk {},{} failed: {}", x, z, error.toString());
|
||||
listener.onChunkFailed(x, z);
|
||||
return;
|
||||
}
|
||||
if (result instanceof ChunkResult<?> chunkResult && !chunkResult.isSuccess()) {
|
||||
LOGGER.warn("Iris pregen chunk {},{} returned no chunk: {}", x, z, chunkResult.getError());
|
||||
listener.onChunkFailed(x, z);
|
||||
return;
|
||||
}
|
||||
listener.onChunkGenerated(x, z);
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
public enum ModdedPregenMode {
|
||||
ASYNC,
|
||||
SYNC
|
||||
}
|
||||
+6
-17
@@ -18,6 +18,7 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
@@ -25,6 +26,7 @@ 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.math.ChunkSpiral;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
@@ -54,7 +56,6 @@ 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;
|
||||
@@ -105,9 +106,11 @@ public final class ModdedRegen {
|
||||
|
||||
private void run() {
|
||||
long startedAt = M.ms();
|
||||
String worldName = engine.getWorld() == null ? null : engine.getWorld().name();
|
||||
IrisToolbelt.beginWorldMaintenance(worldName, "regen");
|
||||
try {
|
||||
resetMantleMargin();
|
||||
List<int[]> targets = orderedTargets(centerX, centerZ, radius);
|
||||
List<int[]> targets = ChunkSpiral.centerOut(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);
|
||||
@@ -117,6 +120,7 @@ public final class ModdedRegen {
|
||||
LOGGER.error("Iris regen failed", e);
|
||||
fail("Regen failed: " + e);
|
||||
} finally {
|
||||
IrisToolbelt.endWorldMaintenance(worldName, "regen");
|
||||
ACTIVE.set(false);
|
||||
}
|
||||
}
|
||||
@@ -259,21 +263,6 @@ public final class ModdedRegen {
|
||||
}
|
||||
}
|
||||
|
||||
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(() -> IrisModdedCommands.ok(source, message));
|
||||
}
|
||||
|
||||
+96
-48
@@ -23,6 +23,7 @@ import art.arcane.iris.core.gui.GuiHost;
|
||||
import art.arcane.iris.core.gui.NoiseExplorerGUI;
|
||||
import art.arcane.iris.core.gui.VisionGUI;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.project.IrisProjectCopier;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeGeneratorLink;
|
||||
@@ -41,6 +42,7 @@ import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.format.Form;
|
||||
import art.arcane.volmlib.util.function.Function2;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
@@ -63,13 +65,10 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
|
||||
import java.awt.Desktop;
|
||||
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.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -80,7 +79,6 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class ModdedStudioCommands {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
@@ -150,9 +148,9 @@ public final class ModdedStudioCommands {
|
||||
.executes((CommandContext<CommandSourceStack> context) -> tpStudio(context.getSource())));
|
||||
root.then(Commands.literal("status")
|
||||
.executes((CommandContext<CommandSourceStack> context) -> status(context.getSource())));
|
||||
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("vsc", "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(workspaceTree("vscode", true));
|
||||
root.then(workspaceTree("vsc", true));
|
||||
root.then(workspaceTree("update", false));
|
||||
root.then(message("importvanilla", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over."));
|
||||
root.then(message("importv", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over."));
|
||||
root.then(message("iv", "Vanilla tree/object/structure capture generates features in throwaway Bukkit worlds via NMS; run /iris studio importvanilla on a Bukkit server against this pack, then copy the pack folder over."));
|
||||
@@ -170,6 +168,10 @@ public final class ModdedStudioCommands {
|
||||
return root;
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
STUDIOS.clear();
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> openTree(String name) {
|
||||
return Commands.literal(name)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> openHelp(context.getSource()))
|
||||
@@ -206,7 +208,8 @@ public final class ModdedStudioCommands {
|
||||
return 0;
|
||||
}
|
||||
if (engine != null) {
|
||||
ModdedGuiHost.bindContext(source.getServer(), level, engine);
|
||||
ServerPlayer player = source.getPlayer();
|
||||
ModdedGuiHost.bindContext(source.getServer(), level, engine, player == null ? null : player.getUUID());
|
||||
}
|
||||
if (generatorKey == null || generatorKey.isBlank()) {
|
||||
NoiseExplorerGUI.launch();
|
||||
@@ -240,13 +243,91 @@ public final class ModdedStudioCommands {
|
||||
IrisModdedCommands.fail(source, guiUnavailableMessage());
|
||||
return 0;
|
||||
}
|
||||
ModdedGuiHost.bindContext(source.getServer(), level, engine);
|
||||
ServerPlayer player = source.getPlayer();
|
||||
ModdedGuiHost.bindContext(source.getServer(), level, engine, player == null ? null : player.getUUID());
|
||||
VisionGUI.launch(engine);
|
||||
IrisModdedCommands.ok(source, "Opening the Vision map for " + level.dimension().identifier() + " on the server display.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> workspaceTree(String name, boolean open) {
|
||||
return Commands.literal(name)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> workspace(context.getSource(), null, open))
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> workspace(context.getSource(), StringArgumentType.getString(context, "pack"), open)));
|
||||
}
|
||||
|
||||
private static int workspace(CommandSourceStack source, String pack, boolean open) {
|
||||
File folder = resolvePack(source, pack);
|
||||
if (folder == null) {
|
||||
return 0;
|
||||
}
|
||||
File workspace = new File(folder, folder.getName() + ".code-workspace");
|
||||
try {
|
||||
IO.writeAll(workspace, workspaceConfig().toString(4));
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Iris workspace write failed for {}", workspace, e);
|
||||
IrisModdedCommands.fail(source, "Failed to write " + workspace.getAbsolutePath() + ": " + e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Workspace regenerated: " + workspace.getAbsolutePath() + " (JSON schemas require the Bukkit studio toolchain).");
|
||||
if (!open) {
|
||||
return 1;
|
||||
}
|
||||
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
|
||||
IrisModdedCommands.fail(source, guiUnavailableMessage());
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
Desktop.getDesktop().open(workspace);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris workspace open failed for {}", workspace, e);
|
||||
IrisModdedCommands.fail(source, "Could not open " + workspace.getName() + ": " + e.getClass().getSimpleName());
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Opening " + workspace.getName() + " in your editor.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static JSONObject workspaceConfig() {
|
||||
JSONObject ws = new JSONObject();
|
||||
JSONArray folders = new JSONArray();
|
||||
JSONObject folder = new JSONObject();
|
||||
folder.put("path", ".");
|
||||
folders.put(folder);
|
||||
ws.put("folders", folders);
|
||||
JSONObject settings = new JSONObject();
|
||||
settings.put("workbench.colorTheme", "Monokai");
|
||||
settings.put("workbench.preferredDarkColorTheme", "Solarized Dark");
|
||||
settings.put("workbench.tips.enabled", false);
|
||||
settings.put("workbench.tree.indent", 24);
|
||||
settings.put("files.autoSave", "onFocusChange");
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("editor.autoIndent", "brackets");
|
||||
json.put("editor.acceptSuggestionOnEnter", "smart");
|
||||
json.put("editor.cursorSmoothCaretAnimation", true);
|
||||
json.put("editor.dragAndDrop", false);
|
||||
json.put("files.trimTrailingWhitespace", true);
|
||||
json.put("diffEditor.ignoreTrimWhitespace", true);
|
||||
json.put("files.trimFinalNewlines", true);
|
||||
json.put("editor.suggest.showKeywords", false);
|
||||
json.put("editor.suggest.showSnippets", false);
|
||||
json.put("editor.suggest.showWords", false);
|
||||
JSONObject quick = new JSONObject();
|
||||
quick.put("strings", true);
|
||||
json.put("editor.quickSuggestions", quick);
|
||||
json.put("editor.suggest.insertMode", "replace");
|
||||
settings.put("[json]", json);
|
||||
settings.put("json.maxItemsComputed", 30000);
|
||||
settings.put("json.schemas", new JSONArray());
|
||||
ws.put("settings", settings);
|
||||
return ws;
|
||||
}
|
||||
|
||||
private static String guiUnavailableMessage() {
|
||||
if (GuiHost.isDesktopSuppressed()) {
|
||||
return "Iris desktop GUIs are disabled in singleplayer/client to avoid crashing the game client; use the in-game map and chat output instead.";
|
||||
}
|
||||
if (!GuiHost.isAvailable()) {
|
||||
return "This server has no display (headless JVM); the Iris desktop GUIs need an AWT-capable session.";
|
||||
}
|
||||
@@ -346,7 +427,7 @@ public final class ModdedStudioCommands {
|
||||
private static void injectConsole(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, long seed) {
|
||||
ModdedDimensionManager.Handle handle;
|
||||
try {
|
||||
handle = ModdedDimensionManager.create(server, dimensionId, pack, seed);
|
||||
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris console studio injection failed for {} ({})", dimensionId, pack, e);
|
||||
IrisModdedCommands.fail(source, "Studio injection failed: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
|
||||
@@ -376,7 +457,7 @@ public final class ModdedStudioCommands {
|
||||
}
|
||||
ModdedDimensionManager.Handle handle;
|
||||
try {
|
||||
handle = ModdedDimensionManager.create(server, dimensionId, pack, seed);
|
||||
handle = ModdedDimensionManager.create(server, dimensionId, pack, pack, seed);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio injection failed for {} ({})", dimensionId, pack, e);
|
||||
IrisModdedCommands.fail(source, "Studio injection failed: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
|
||||
@@ -434,7 +515,7 @@ public final class ModdedStudioCommands {
|
||||
for (ModdedDimensionManager.Handle handle : studios) {
|
||||
UUID owner = ownerOf(handle.dimensionId());
|
||||
String ownerName = owner == null ? "unclaimed" : ownerName(server, owner);
|
||||
IrisModdedCommands.ok(source, " " + handle.dimensionId() + ": pack '" + handle.packKey() + "' seed " + handle.seed() + " owner " + ownerName);
|
||||
IrisModdedCommands.ok(source, " " + handle.dimensionId() + ": pack '" + handle.pack() + "' seed " + handle.seed() + " owner " + ownerName);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -529,7 +610,7 @@ public final class ModdedStudioCommands {
|
||||
return;
|
||||
}
|
||||
}
|
||||
copyProject(templateFolder, target, template, name);
|
||||
IrisProjectCopier.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.");
|
||||
@@ -544,39 +625,6 @@ public final class ModdedStudioCommands {
|
||||
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) {
|
||||
@@ -738,7 +786,7 @@ public final class ModdedStudioCommands {
|
||||
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();
|
||||
})).setOffset(blockX >> 4, blockZ >> 4).drain();
|
||||
executor.complete();
|
||||
burst.close();
|
||||
server.execute(() -> counts.forEach((String key, AtomicInteger count) -> {
|
||||
|
||||
+29
-6
@@ -18,8 +18,10 @@
|
||||
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.gui.GuiHost;
|
||||
import art.arcane.iris.core.gui.GuiMarker;
|
||||
import art.arcane.iris.core.gui.GuiOverlay;
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.render.RenderType;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
@@ -30,19 +32,24 @@ import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class ModdedVisionOverlay implements GuiOverlay {
|
||||
private final MinecraftServer server;
|
||||
private final ServerLevel level;
|
||||
private final Engine engine;
|
||||
private final UUID opener;
|
||||
|
||||
public ModdedVisionOverlay(MinecraftServer server, ServerLevel level, Engine engine) {
|
||||
public ModdedVisionOverlay(MinecraftServer server, ServerLevel level, Engine engine, UUID opener) {
|
||||
this.server = server;
|
||||
this.level = level;
|
||||
this.engine = engine;
|
||||
this.opener = opener;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,11 +83,14 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
||||
int blockX = (int) worldX;
|
||||
int blockZ = (int) worldZ;
|
||||
server.execute(() -> {
|
||||
List<ServerPlayer> players = level.players();
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
ServerPlayer player = opener == null ? null : server.getPlayerList().getPlayer(opener);
|
||||
if (player == null) {
|
||||
List<ServerPlayer> players = level.players();
|
||||
if (players.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
player = players.get(0);
|
||||
}
|
||||
ServerPlayer player = players.get(0);
|
||||
int surfaceY = engine.getMinHeight() + engine.getHeight(blockX, blockZ, false) + 2;
|
||||
int safeY = Math.max(surfaceY, level.getHeight(Heightmap.Types.MOTION_BLOCKING, blockX, blockZ) + 1);
|
||||
player.teleportTo(level, blockX + 0.5D, safeY, blockZ + 0.5D, java.util.Set.of(), player.getYRot(), player.getXRot(), false);
|
||||
@@ -89,6 +99,19 @@ public final class ModdedVisionOverlay implements GuiOverlay {
|
||||
|
||||
@Override
|
||||
public String openInEditor(double worldX, double worldZ, RenderType type) {
|
||||
return null;
|
||||
if (!GuiHost.isAvailable() || !Desktop.isDesktopSupported()) {
|
||||
return null;
|
||||
}
|
||||
IrisComplex complex = engine.getComplex();
|
||||
File file = switch (type) {
|
||||
case BIOME, LAYER_LOAD, DECORATOR_LOAD, OBJECT_LOAD, HEIGHT ->
|
||||
complex.getTrueBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_LAND -> complex.getLandBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case BIOME_SEA -> complex.getSeaBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
case REGION -> complex.getRegionStream().get(worldX, worldZ).openInVSCode();
|
||||
case CAVE_LAND -> complex.getCaveBiomeStream().get(worldX, worldZ).openInVSCode();
|
||||
default -> null;
|
||||
};
|
||||
return file == null ? null : file.getName();
|
||||
}
|
||||
}
|
||||
|
||||
+168
-39
@@ -19,19 +19,25 @@
|
||||
package art.arcane.iris.modded.command;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
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.ModdedDimensionManager;
|
||||
import art.arcane.iris.modded.ModdedEngineBootstrap;
|
||||
import art.arcane.iris.modded.ModdedModConfig;
|
||||
import art.arcane.iris.modded.ModdedPackInstaller;
|
||||
import art.arcane.iris.modded.ModdedPrimaryWorldRouter;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.context.ParsedCommandNode;
|
||||
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.commands.arguments.IdentifierArgument;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import org.slf4j.Logger;
|
||||
@@ -41,12 +47,14 @@ import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public final class ModdedWorldCommands {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final Predicate<CommandSourceStack> GATE = Commands.hasPermission(Commands.LEVEL_GAMEMASTERS);
|
||||
private static final String DEFAULT_NAMESPACE = "irisworldgen";
|
||||
private static final long DEFAULT_SEED = 1337L;
|
||||
private static final SuggestionProvider<CommandSourceStack> LOADED_DIMENSIONS = (CommandContext<CommandSourceStack> context, SuggestionsBuilder builder) -> SharedSuggestionProvider.suggest(loadedIrisDimensions(context.getSource().getServer()), builder);
|
||||
|
||||
private ModdedWorldCommands() {
|
||||
@@ -68,53 +76,68 @@ public final class ModdedWorldCommands {
|
||||
root.then(enableTree("create"));
|
||||
root.then(replaceOverworldTree());
|
||||
|
||||
root.then(disableTree("disable"));
|
||||
root.then(disableTree("remove"));
|
||||
root.then(disableTree("rm"));
|
||||
root.then(disableTree());
|
||||
root.then(deleteTree("delete"));
|
||||
root.then(deleteTree("remove"));
|
||||
root.then(deleteTree("rm"));
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> enableTree(String name) {
|
||||
return Commands.literal(name)
|
||||
.then(Commands.argument("dimension", StringArgumentType.word())
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.then(Commands.argument("dimension", IdentifierArgument.id())
|
||||
.then(Commands.argument("pack", StringArgumentType.string()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
|
||||
StringArgumentType.getString(context, "dimension"),
|
||||
dimensionArgument(context),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
1337L))
|
||||
.then(Commands.argument("packDimension", StringArgumentType.word())
|
||||
null))
|
||||
.then(Commands.argument("seed", StringArgumentType.word())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> enable(context.getSource(),
|
||||
StringArgumentType.getString(context, "dimension"),
|
||||
dimensionArgument(context),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
StringArgumentType.getString(context, "packDimension"),
|
||||
1337L)))));
|
||||
StringArgumentType.getString(context, "seed"))))));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> disableTree(String name) {
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> disableTree() {
|
||||
return Commands.literal("disable")
|
||||
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), dimensionArgument(context), false)));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> deleteTree(String name) {
|
||||
return Commands.literal(name)
|
||||
.then(Commands.argument("dimension", StringArgumentType.word()).suggests(LOADED_DIMENSIONS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), StringArgumentType.getString(context, "dimension"))));
|
||||
.then(Commands.argument("dimension", IdentifierArgument.id()).suggests(LOADED_DIMENSIONS)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> disable(context.getSource(), dimensionArgument(context), true)));
|
||||
}
|
||||
|
||||
private static LiteralArgumentBuilder<CommandSourceStack> replaceOverworldTree() {
|
||||
return Commands.literal("replace-overworld")
|
||||
.then(Commands.argument("pack", StringArgumentType.word()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.then(Commands.argument("pack", StringArgumentType.string()).suggests(IrisModdedCommands.PACK_NAMES)
|
||||
.executes((CommandContext<CommandSourceStack> context) -> replaceOverworld(context.getSource(),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
StringArgumentType.getString(context, "pack")))
|
||||
.then(Commands.argument("packDimension", StringArgumentType.word())
|
||||
null))
|
||||
.then(Commands.argument("seed", StringArgumentType.word())
|
||||
.executes((CommandContext<CommandSourceStack> context) -> replaceOverworld(context.getSource(),
|
||||
StringArgumentType.getString(context, "pack"),
|
||||
StringArgumentType.getString(context, "packDimension")))));
|
||||
StringArgumentType.getString(context, "seed")))));
|
||||
}
|
||||
|
||||
public static int createWorld(CommandSourceStack source, String name, String pack, long seed) {
|
||||
return enable(source, name, pack, pack, seed);
|
||||
String[] packRef = parsePackRef(pack);
|
||||
return enable(source, name, packRef[0], packRef[1], seed);
|
||||
}
|
||||
|
||||
private static int enable(CommandSourceStack source, String targetDimension, String packName, String packDimension, long seed) {
|
||||
private static int enable(CommandSourceStack source, String targetDimension, String packRaw, String seedRaw) {
|
||||
Long seed = parseSeed(source, seedRaw);
|
||||
if (seed == null) {
|
||||
return 0;
|
||||
}
|
||||
String[] packRef = parsePackRef(packRaw);
|
||||
return enable(source, targetDimension, packRef[0], packRef[1], seed);
|
||||
}
|
||||
|
||||
private static int enable(CommandSourceStack source, String targetDimension, String pack, String packDimension, long seed) {
|
||||
MinecraftServer server = source.getServer();
|
||||
String dimensionId;
|
||||
try {
|
||||
@@ -123,58 +146,146 @@ public final class ModdedWorldCommands {
|
||||
IrisModdedCommands.fail(source, e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
if (!loadPackDimension(source, packName, packDimension)) {
|
||||
if (!validPackRef(source, pack, packDimension)) {
|
||||
return 0;
|
||||
}
|
||||
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
|
||||
if (packFolder.isDirectory()) {
|
||||
return enableInstalled(source, server, dimensionId, pack, packDimension, 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;
|
||||
}
|
||||
enableInstalled(source, server, dimensionId, pack, packDimension, seed);
|
||||
});
|
||||
}, "Iris World Pack Download");
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int enableInstalled(CommandSourceStack source, MinecraftServer server, String dimensionId, String pack, String packDimension, long seed) {
|
||||
if (!loadPackDimension(source, pack, packDimension)) {
|
||||
return 0;
|
||||
}
|
||||
if (blockIfPackBroken(source, dimensionId, pack)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
ModdedDimensionManager.createPersistent(server, dimensionId, packDimension, seed);
|
||||
ModdedDimensionManager.createPersistent(server, dimensionId, pack, packDimension, seed);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris world injection failed for {} (pack={} dim={})", dimensionId, packName, packDimension, e);
|
||||
LOGGER.error("Iris world injection failed for {} (pack={} dim={})", dimensionId, pack, packDimension, e);
|
||||
IrisModdedCommands.fail(source, "Failed to inject Iris world '" + dimensionId + "': " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Created Iris world " + dimensionId + " from pack '" + packName + "' dimension '" + packDimension + "' (seed " + seed + ").");
|
||||
IrisModdedCommands.ok(source, "Created Iris world " + dimensionId + " from pack '" + pack + "' dimension '" + packDimension + "' (seed " + seed + ").");
|
||||
IrisModdedCommands.ok(source, "It is live now and re-injected on every startup. Teleport in with /iris world status or a portal; no restart required.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int replaceOverworld(CommandSourceStack source, String packName, String packDimension) {
|
||||
private static int replaceOverworld(CommandSourceStack source, String packRaw, String seedRaw) {
|
||||
MinecraftServer server = source.getServer();
|
||||
Long seed = parseSeed(source, seedRaw);
|
||||
if (seed == null) {
|
||||
return 0;
|
||||
}
|
||||
String[] packRef = parsePackRef(packRaw);
|
||||
String pack = packRef[0];
|
||||
String packDimension = packRef[1];
|
||||
if (!validPackRef(source, pack, packDimension)) {
|
||||
return 0;
|
||||
}
|
||||
String dimensionId = DEFAULT_NAMESPACE + ":primary";
|
||||
if (!loadPackDimension(source, packName, packDimension)) {
|
||||
if (!loadPackDimension(source, pack, packDimension)) {
|
||||
return 0;
|
||||
}
|
||||
if (blockIfPackBroken(source, dimensionId, pack)) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
ModdedDimensionManager.createPersistent(server, dimensionId, packDimension, 1337L);
|
||||
ModdedDimensionManager.createPersistent(server, dimensionId, pack, packDimension, seed);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris primary world injection failed for {} (pack={} dim={})", dimensionId, packName, packDimension, e);
|
||||
LOGGER.error("Iris primary world injection failed for {} (pack={} dim={})", dimensionId, pack, packDimension, e);
|
||||
IrisModdedCommands.fail(source, "Failed to inject Iris primary world: " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
ModdedModConfig.setPrimaryWorld(dimensionId);
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
IrisModdedCommands.ok(source, "Iris primary world set to " + dimensionId + " (pack '" + packName + "' dimension '" + packDimension + "').");
|
||||
IrisModdedCommands.ok(source, "Iris primary world set to " + dimensionId + " (pack '" + pack + "' dimension '" + packDimension + "' seed " + seed + ").");
|
||||
IrisModdedCommands.ok(source, "The vanilla overworld generator cannot be hot-swapped, so this does NOT regenerate the existing overworld.");
|
||||
IrisModdedCommands.ok(source, "Instead, " + dimensionId + " is now the configured primary world: players in the vanilla overworld are routed there on join, and it re-injects on every startup.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static boolean loadPackDimension(CommandSourceStack source, String packName, String packDimension) {
|
||||
File packFolder = new File(ModdedPackCommands.packsRoot(), packName);
|
||||
private static boolean blockIfPackBroken(CommandSourceStack source, String dimensionId, String pack) {
|
||||
PackValidationResult validation = PackValidationRegistry.get(pack);
|
||||
if (validation == null || validation.isLoadable()) {
|
||||
return false;
|
||||
}
|
||||
IrisModdedCommands.fail(source, "Refusing to create world '" + dimensionId + "' using broken pack '" + pack + "':");
|
||||
for (String reason : validation.getBlockingErrors()) {
|
||||
IrisModdedCommands.fail(source, " - " + reason);
|
||||
}
|
||||
IrisModdedCommands.fail(source, "Fix the pack and run /iris pack validate " + pack + " to revalidate.");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean loadPackDimension(CommandSourceStack source, String pack, String packDimension) {
|
||||
File packFolder = new File(ModdedPackCommands.packsRoot(), pack);
|
||||
if (!packFolder.isDirectory()) {
|
||||
IrisModdedCommands.fail(source, "Pack '" + packName + "' was not found under " + ModdedPackCommands.packsRoot().getAbsolutePath());
|
||||
IrisModdedCommands.fail(source, "Pack '" + pack + "' was not found under " + ModdedPackCommands.packsRoot().getAbsolutePath());
|
||||
return false;
|
||||
}
|
||||
IrisData data = IrisData.get(packFolder);
|
||||
IrisDimension dimension = data.getDimensionLoader().load(packDimension);
|
||||
if (dimension == null) {
|
||||
IrisModdedCommands.fail(source, "Pack '" + packName + "' does not contain dimensions/" + packDimension + ".json");
|
||||
IrisModdedCommands.fail(source, "Pack '" + pack + "' does not contain dimensions/" + packDimension + ".json");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int disable(CommandSourceStack source, String targetDimension) {
|
||||
private static String[] parsePackRef(String raw) {
|
||||
String value = raw.trim();
|
||||
int colon = value.indexOf(':');
|
||||
if (colon >= 0) {
|
||||
return new String[]{value.substring(0, colon), value.substring(colon + 1)};
|
||||
}
|
||||
return new String[]{value, value};
|
||||
}
|
||||
|
||||
private static boolean validPackRef(CommandSourceStack source, String pack, String packDimension) {
|
||||
if (pack.matches("[A-Za-z0-9_.-]+") && !pack.contains("..")
|
||||
&& packDimension.matches("[A-Za-z0-9_/.-]+") && !packDimension.contains("..")) {
|
||||
return true;
|
||||
}
|
||||
IrisModdedCommands.fail(source, "Invalid pack reference '" + pack + (pack.equals(packDimension) ? "" : ":" + packDimension) + "'. Use pack or pack:dimensionKey.");
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Long parseSeed(CommandSourceStack source, String seedRaw) {
|
||||
if (seedRaw == null || seedRaw.isBlank()) {
|
||||
return DEFAULT_SEED;
|
||||
}
|
||||
String value = seedRaw.trim();
|
||||
if (value.equalsIgnoreCase("random")) {
|
||||
return ThreadLocalRandom.current().nextLong();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value);
|
||||
} catch (NumberFormatException e) {
|
||||
IrisModdedCommands.fail(source, "Invalid seed '" + seedRaw + "'. Use a number or 'random'.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int disable(CommandSourceStack source, String targetDimension, boolean wipeStorage) {
|
||||
MinecraftServer server = source.getServer();
|
||||
String dimensionId;
|
||||
try {
|
||||
@@ -185,7 +296,7 @@ public final class ModdedWorldCommands {
|
||||
}
|
||||
boolean removed;
|
||||
try {
|
||||
removed = ModdedDimensionManager.removePersistent(server, dimensionId);
|
||||
removed = ModdedDimensionManager.removePersistent(server, dimensionId, wipeStorage);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris world removal failed for {}", dimensionId, e);
|
||||
IrisModdedCommands.fail(source, "Failed to remove Iris world '" + dimensionId + "': " + e.getClass().getSimpleName() + (e.getMessage() == null ? "" : " - " + e.getMessage()));
|
||||
@@ -196,10 +307,19 @@ public final class ModdedWorldCommands {
|
||||
ModdedPrimaryWorldRouter.clear();
|
||||
}
|
||||
if (!removed) {
|
||||
IrisModdedCommands.ok(source, "Iris world '" + dimensionId + "' was not loaded; cleared its persistent registry entry.");
|
||||
if (wipeStorage) {
|
||||
IrisModdedCommands.ok(source, "Iris world '" + dimensionId + "' was not loaded; cleared its persistent registry entry and wiped its stored chunk/mantle data.");
|
||||
} else {
|
||||
IrisModdedCommands.ok(source, "Iris world '" + dimensionId + "' was not loaded; cleared its persistent registry entry. World data on disk is kept.");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
IrisModdedCommands.ok(source, "Removed Iris world '" + dimensionId + "': evacuated, unloaded, region data deleted, and dropped from the startup registry.");
|
||||
if (wipeStorage) {
|
||||
IrisModdedCommands.ok(source, "Deleted Iris world '" + dimensionId + "': evacuated, unloaded, chunk/mantle data wiped, and dropped from the startup registry.");
|
||||
} else {
|
||||
IrisModdedCommands.ok(source, "Disabled Iris world '" + dimensionId + "': evacuated, unloaded, and dropped from the startup registry.");
|
||||
IrisModdedCommands.ok(source, "World data on disk is kept; re-enable with /iris world enable " + dimensionId + " <pack> or delete it with /iris world delete " + dimensionId + ".");
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -209,7 +329,7 @@ public final class ModdedWorldCommands {
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator) {
|
||||
loaded++;
|
||||
IrisModdedCommands.ok(source, "Loaded Iris level: " + level.dimension().identifier() + " -> pack dimension '" + generator.activePackKey() + "'");
|
||||
IrisModdedCommands.ok(source, "Loaded Iris level: " + level.dimension().identifier() + " -> pack '" + generator.activePack() + "' dimension '" + generator.activeDimensionKey() + "'");
|
||||
}
|
||||
}
|
||||
String primary = ModdedModConfig.get().primaryWorld();
|
||||
@@ -244,6 +364,15 @@ public final class ModdedWorldCommands {
|
||||
return dimensions;
|
||||
}
|
||||
|
||||
private static String dimensionArgument(CommandContext<CommandSourceStack> context) {
|
||||
for (ParsedCommandNode<CommandSourceStack> node : context.getNodes()) {
|
||||
if ("dimension".equals(node.getNode().getName())) {
|
||||
return node.getRange().get(context.getInput());
|
||||
}
|
||||
}
|
||||
return IdentifierArgument.getId(context, "dimension").toString();
|
||||
}
|
||||
|
||||
private static String normalizeDimensionId(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException("Missing dimension id.");
|
||||
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* 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.service;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.TileData;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedBlockResolution;
|
||||
import art.arcane.iris.modded.ModdedLootApplier;
|
||||
import art.arcane.iris.modded.ModdedTileData;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.matter.MatterCavern;
|
||||
import art.arcane.volmlib.util.matter.MatterUpdate;
|
||||
import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtUtils;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
public final class ModdedChunkUpdateService implements ModdedTickableService {
|
||||
private static final long PASS_PERIOD_MILLIS = 3_000L;
|
||||
private static final int PLAYER_CHUNK_RADIUS = 1;
|
||||
private static final int SILENT_FLAGS = Block.UPDATE_CLIENTS | Block.UPDATE_KNOWN_SHAPE | Block.UPDATE_SKIP_ON_PLACE;
|
||||
|
||||
private final Set<Long> warmupQueue = ConcurrentHashMap.newKeySet();
|
||||
private volatile ExecutorService warmupExecutor;
|
||||
private long lastPassAt;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (warmupExecutor != null) {
|
||||
return;
|
||||
}
|
||||
warmupExecutor = Executors.newSingleThreadExecutor((Runnable runnable) -> {
|
||||
Thread thread = new Thread(runnable, "Iris Mantle Warmup");
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
});
|
||||
lastPassAt = 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
ExecutorService active = warmupExecutor;
|
||||
warmupExecutor = null;
|
||||
if (active != null) {
|
||||
active.shutdown();
|
||||
}
|
||||
warmupQueue.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick(MinecraftServer server) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastPassAt < PASS_PERIOD_MILLIS) {
|
||||
return;
|
||||
}
|
||||
lastPassAt = now;
|
||||
|
||||
if (!IrisSettings.get().getWorld().isPostLoadBlockUpdates()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ServerLevel level : server.getAllLevels()) {
|
||||
if (!(level.getChunkSource().getGenerator() instanceof IrisModdedChunkGenerator generator)) {
|
||||
continue;
|
||||
}
|
||||
Engine engine = generator.engineIfBound();
|
||||
if (engine == null || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
continue;
|
||||
}
|
||||
if (level.players().isEmpty() || isPregenActive(engine)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
updateNearPlayers(engine, level);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPregenActive(Engine engine) {
|
||||
PregeneratorJob job = PregeneratorJob.getInstance();
|
||||
return job != null && job.targetsWorldName(engine.getWorld().name());
|
||||
}
|
||||
|
||||
private void updateNearPlayers(Engine engine, ServerLevel level) {
|
||||
for (ServerPlayer player : level.players()) {
|
||||
int centerX = player.blockPosition().getX() >> 4;
|
||||
int centerZ = player.blockPosition().getZ() >> 4;
|
||||
for (int dx = -PLAYER_CHUNK_RADIUS; dx <= PLAYER_CHUNK_RADIUS; dx++) {
|
||||
for (int dz = -PLAYER_CHUNK_RADIUS; dz <= PLAYER_CHUNK_RADIUS; dz++) {
|
||||
updateChunk(engine, level, centerX + dx, centerZ + dz);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChunk(Engine engine, ServerLevel level, int chunkX, int chunkZ) {
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
if (level.getChunkSource().getChunkNow(chunkX + x, chunkZ + z) == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Mantle<Matter> mantle = engine.getMantle().getMantle();
|
||||
if (!mantle.isChunkLoaded(chunkX, chunkZ)) {
|
||||
warmupMantleChunk(mantle, chunkX, chunkZ);
|
||||
return;
|
||||
}
|
||||
if (mantle.hasFlag(chunkX, chunkZ, MantleFlag.ETCHED)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
|
||||
try {
|
||||
chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> {
|
||||
chunk.raiseFlagUnchecked(MantleFlag.TILE, () -> runTilePass(engine, level, chunkX, chunkZ, chunk));
|
||||
chunk.raiseFlagUnchecked(MantleFlag.UPDATE, () -> runUpdatePass(engine, level, chunkX, chunkZ, chunk));
|
||||
});
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private void runTilePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
chunk.iterate(TileWrapper.class, (Integer x, Integer yf, Integer z, TileWrapper v) -> {
|
||||
int y = yf + minHeight;
|
||||
if (y < level.getMinY() || y > level.getMaxY()) {
|
||||
return;
|
||||
}
|
||||
applyTile(level, baseX + (x & 15), y, baseZ + (z & 15), v.getData());
|
||||
});
|
||||
}
|
||||
|
||||
private void applyTile(ServerLevel level, int x, int y, int z, TileData tile) {
|
||||
if (!(tile instanceof ModdedTileData moddedTile)) {
|
||||
return;
|
||||
}
|
||||
String snbt = moddedTile.snbt();
|
||||
if (snbt == null || snbt.isBlank()) {
|
||||
return;
|
||||
}
|
||||
BlockPos pos = new BlockPos(x, y, z);
|
||||
BlockState state = level.getBlockState(pos);
|
||||
if (!state.hasBlockEntity()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
CompoundTag tag = NbtUtils.snbtToStructure(snbt);
|
||||
BlockEntity restored = BlockEntity.loadStatic(pos, state, tag, level.registryAccess());
|
||||
if (restored == null) {
|
||||
return;
|
||||
}
|
||||
level.setBlockEntity(restored);
|
||||
restored.setChanged();
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void warmupMantleChunk(Mantle<Matter> mantle, int chunkX, int chunkZ) {
|
||||
ExecutorService active = warmupExecutor;
|
||||
if (active == null) {
|
||||
return;
|
||||
}
|
||||
long key = Cache.key(chunkX, chunkZ);
|
||||
if (!warmupQueue.add(key)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
active.execute(() -> {
|
||||
try {
|
||||
mantle.getChunk(chunkX, chunkZ);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
} finally {
|
||||
warmupQueue.remove(key);
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException rejected) {
|
||||
warmupQueue.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void runUpdatePass(Engine engine, ServerLevel level, int chunkX, int chunkZ, MantleChunk<Matter> chunk) {
|
||||
PrecisionStopwatch stopwatch = PrecisionStopwatch.start();
|
||||
int minHeight = engine.getWorld().minHeight();
|
||||
int baseX = chunkX << 4;
|
||||
int baseZ = chunkZ << 4;
|
||||
int[][] grid = new int[16][16];
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
grid[x][z] = Integer.MIN_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
RNG rng = new RNG(Cache.key(chunkX, chunkZ));
|
||||
chunk.iterate(MatterCavern.class, (Integer x, Integer yf, Integer z, MatterCavern v) -> {
|
||||
int y = yf + minHeight;
|
||||
if (y < level.getMinY() || y > level.getMaxY()) {
|
||||
return;
|
||||
}
|
||||
int lx = x & 15;
|
||||
int lz = z & 15;
|
||||
BlockPos pos = new BlockPos(baseX + lx, y, baseZ + lz);
|
||||
if (!ModdedBlockResolution.isFluid(level.getBlockState(pos))) {
|
||||
return;
|
||||
}
|
||||
boolean exposed = ModdedBlockResolution.isAir(level.getBlockState(pos.below()))
|
||||
|| ModdedBlockResolution.isAir(level.getBlockState(pos.west()))
|
||||
|| ModdedBlockResolution.isAir(level.getBlockState(pos.east()))
|
||||
|| ModdedBlockResolution.isAir(level.getBlockState(pos.south()))
|
||||
|| ModdedBlockResolution.isAir(level.getBlockState(pos.north()));
|
||||
|
||||
if (exposed) {
|
||||
grid[lx][lz] = Math.max(grid[lx][lz], y);
|
||||
}
|
||||
});
|
||||
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
if (grid[x][z] == Integer.MIN_VALUE) {
|
||||
continue;
|
||||
}
|
||||
update(engine, level, x, grid[x][z], z, baseX, baseZ, chunk, rng);
|
||||
}
|
||||
}
|
||||
|
||||
chunk.iterate(MatterUpdate.class, (Integer x, Integer yf, Integer z, MatterUpdate v) -> {
|
||||
if (v != null && v.isUpdate()) {
|
||||
update(engine, level, x, yf + minHeight, z, baseX, baseZ, chunk, rng);
|
||||
}
|
||||
});
|
||||
chunk.deleteSlices(MatterUpdate.class);
|
||||
engine.getMetrics().getUpdates().put(stopwatch.getMilliseconds());
|
||||
}
|
||||
|
||||
private void update(Engine engine, ServerLevel level, int x, int y, int z, int baseX, int baseZ, MantleChunk<Matter> chunk, RNG rng) {
|
||||
if (y < level.getMinY() || y > level.getMaxY()) {
|
||||
return;
|
||||
}
|
||||
BlockPos pos = new BlockPos(baseX + (x & 15), y, baseZ + (z & 15));
|
||||
BlockState state = level.getBlockState(pos);
|
||||
engine.blockUpdatedMetric();
|
||||
if (ModdedBlockResolution.isStorage(state)) {
|
||||
if (!ModdedBlockResolution.isStorageChest(state)) {
|
||||
return;
|
||||
}
|
||||
RNG rx = rng.nextParallelRNG(BlockPosition.toLong(x, y, z));
|
||||
try {
|
||||
ModdedLootApplier.apply(engine, level, pos, state, chunk, rx, x & 15, z & 15);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
} else {
|
||||
level.setBlock(pos, Blocks.AIR.defaultBlockState(), SILENT_FLAGS);
|
||||
level.setBlock(pos, state, Block.UPDATE_ALL);
|
||||
}
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
/*
|
||||
* 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.service;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.pregenerator.MantleHeapPressure;
|
||||
import art.arcane.iris.core.runtime.GoldenHashEngine;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.modded.ModdedWorldEngines;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class ModdedEngineMaintenanceService implements ModdedTickableService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final long TRIM_PERIOD_MILLIS = 2_000L;
|
||||
private static final long SAVE_PERIOD_MILLIS = 60_000L;
|
||||
|
||||
private final AtomicInteger tectonicLimit = new AtomicInteger(30);
|
||||
private final Set<Engine> inFlight = ConcurrentHashMap.newKeySet();
|
||||
private volatile ExecutorService service;
|
||||
private long lastMaintenanceAt;
|
||||
private long lastSaveAt;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (service != null) {
|
||||
return;
|
||||
}
|
||||
IrisSettings.IrisSettingsPerformance settings = IrisSettings.get().getPerformance();
|
||||
IrisSettings.IrisSettingsEngineSVC engineSettings = settings.getEngineSVC();
|
||||
ThreadFactory factory = (engineSettings.isUseVirtualThreads()
|
||||
? Thread.ofVirtual()
|
||||
: Thread.ofPlatform().priority(engineSettings.getPriority()))
|
||||
.name("Iris EngineSVC-", 0)
|
||||
.factory();
|
||||
service = Executors.newThreadPerTaskExecutor(factory);
|
||||
tectonicLimit.set(settings.getTectonicPlateSize());
|
||||
lastMaintenanceAt = 0L;
|
||||
lastSaveAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
ExecutorService active = service;
|
||||
service = null;
|
||||
if (active != null) {
|
||||
active.shutdown();
|
||||
}
|
||||
inFlight.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick(MinecraftServer server) {
|
||||
ExecutorService active = service;
|
||||
if (active == null) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastMaintenanceAt < TRIM_PERIOD_MILLIS) {
|
||||
return;
|
||||
}
|
||||
lastMaintenanceAt = now;
|
||||
boolean flush = now - lastSaveAt >= SAVE_PERIOD_MILLIS;
|
||||
if (flush) {
|
||||
lastSaveAt = now;
|
||||
}
|
||||
Collection<Engine> engines = ModdedWorldEngines.activeEngines();
|
||||
int share = tectonicLimit.get() / Math.max(engines.size(), 1);
|
||||
for (Engine engine : engines) {
|
||||
if (!inFlight.add(engine)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
active.execute(() -> {
|
||||
try {
|
||||
maintain(engine, share, flush);
|
||||
} finally {
|
||||
inFlight.remove(engine);
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException rejected) {
|
||||
inFlight.remove(engine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void maintain(Engine engine, int share, boolean flush) {
|
||||
if (engine == null || engine.isClosed() || engine.getMantle().getMantle().isClosed()) {
|
||||
return;
|
||||
}
|
||||
if (flush) {
|
||||
try {
|
||||
engine.save();
|
||||
} catch (Throwable e) {
|
||||
if (isMantleClosed(e)) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.reportError(e);
|
||||
LOGGER.error("Iris engine save failed for {}", engine.getWorld().name(), e);
|
||||
}
|
||||
}
|
||||
if (!shouldReduce(engine) || shouldSkipForMaintenance(engine) || GoldenHashEngine.isActive()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (pregenTargets(engine) && MantleHeapPressure.overHighWater()) {
|
||||
engine.getMantle().trim(0L, 0);
|
||||
} else {
|
||||
engine.getMantle().trim(TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive()), activeTectonicLimit(engine, share));
|
||||
}
|
||||
long unloadStart = System.currentTimeMillis();
|
||||
boolean heapPressure = pregenTargets(engine) && MantleHeapPressure.overHighWater();
|
||||
int unloadLimit = (heapPressure || IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite) ? 0 : activeTectonicLimit(engine, share);
|
||||
int count = engine.getMantle().unloadTectonicPlate(unloadLimit);
|
||||
if (heapPressure && MantleHeapPressure.overPanicWater()) {
|
||||
MantleHeapPressure.requestPanicReclaim();
|
||||
}
|
||||
if (count > 0) {
|
||||
LOGGER.debug("Iris unloaded {} tectonic plates in {}ms for {}", count, System.currentTimeMillis() - unloadStart, engine.getWorld().name());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
if (isMantleClosed(e)) {
|
||||
return;
|
||||
}
|
||||
IrisLogging.reportError(e);
|
||||
LOGGER.error("Iris engine maintenance failed for {}", engine.getWorld().name(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldReduce(Engine engine) {
|
||||
if (!engine.isStudio() || IrisSettings.get().getPerformance().isTrimMantleInStudio()) {
|
||||
return true;
|
||||
}
|
||||
return pregenTargets(engine);
|
||||
}
|
||||
|
||||
private boolean shouldSkipForMaintenance(Engine engine) {
|
||||
if (engine.getWorld() == null || !IrisToolbelt.isWorldMaintenanceActive(engine.getWorld().name())) {
|
||||
return false;
|
||||
}
|
||||
return !pregenTargets(engine);
|
||||
}
|
||||
|
||||
private boolean pregenTargets(Engine engine) {
|
||||
if (engine.getWorld() == null) {
|
||||
return false;
|
||||
}
|
||||
PregeneratorJob job = PregeneratorJob.getInstance();
|
||||
return job != null && job.targetsWorldName(engine.getWorld().name());
|
||||
}
|
||||
|
||||
private int activeTectonicLimit(Engine engine, int share) {
|
||||
if (!pregenTargets(engine)) {
|
||||
return share;
|
||||
}
|
||||
return Math.max(share, IrisSettings.get().getPregen().getEffectiveResidentTectonicPlates(engine.getHeight()));
|
||||
}
|
||||
|
||||
private static boolean isMantleClosed(Throwable throwable) {
|
||||
Throwable current = throwable;
|
||||
while (current != null) {
|
||||
String message = current.getMessage();
|
||||
if (message != null && message.toLowerCase(Locale.ROOT).contains("mantle is closed")) {
|
||||
return true;
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+28
-16
@@ -29,18 +29,30 @@ import org.apache.logging.log4j.message.Message;
|
||||
import java.util.List;
|
||||
|
||||
public final class ModdedLogFilterService implements ModdedService, Filter {
|
||||
private static final String VANILLA_LOGGER_PREFIX = "net.minecraft";
|
||||
private static final List<String> FILTERS = List.of(
|
||||
"Ignoring heightmap data for chunk",
|
||||
"Could not save data net.minecraft.world.entity.raid.PersistentRaid",
|
||||
"UUID of added entity already exists");
|
||||
|
||||
private boolean installed = false;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (installed) {
|
||||
return;
|
||||
}
|
||||
((Logger) LogManager.getRootLogger()).addFilter(this);
|
||||
installed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (!installed) {
|
||||
return;
|
||||
}
|
||||
((Logger) LogManager.getRootLogger()).get().removeFilter(this);
|
||||
installed = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,76 +94,76 @@ public final class ModdedLogFilterService implements ModdedService, Filter {
|
||||
|
||||
@Override
|
||||
public Result filter(LogEvent event) {
|
||||
return check(event.getMessage().getFormattedMessage());
|
||||
return check(event.getLoggerName(), event.getMessage().getFormattedMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
|
||||
return check(String.valueOf(msg));
|
||||
return check(logger.getName(), String.valueOf(msg));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
|
||||
return check(msg.getFormattedMessage());
|
||||
return check(logger.getName(), msg.getFormattedMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object... params) {
|
||||
return check(message);
|
||||
return check(logger.getName(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0) {
|
||||
return check(message);
|
||||
return check(logger.getName(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1) {
|
||||
return check(message);
|
||||
return check(logger.getName(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2) {
|
||||
return check(message);
|
||||
return check(logger.getName(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3) {
|
||||
return check(message);
|
||||
return check(logger.getName(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4) {
|
||||
return check(message);
|
||||
return check(logger.getName(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5) {
|
||||
return check(message);
|
||||
return check(logger.getName(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6) {
|
||||
return check(message);
|
||||
return check(logger.getName(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7) {
|
||||
return check(message);
|
||||
return check(logger.getName(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8) {
|
||||
return check(message);
|
||||
return check(logger.getName(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result filter(Logger logger, Level level, Marker marker, String message, Object p0, Object p1, Object p2, Object p3, Object p4, Object p5, Object p6, Object p7, Object p8, Object p9) {
|
||||
return check(message);
|
||||
return check(logger.getName(), message);
|
||||
}
|
||||
|
||||
private Result check(String message) {
|
||||
if (message == null) {
|
||||
private Result check(String loggerName, String message) {
|
||||
if (loggerName == null || !loggerName.startsWith(VANILLA_LOGGER_PREFIX) || message == null) {
|
||||
return Result.NEUTRAL;
|
||||
}
|
||||
for (String filter : FILTERS) {
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.service;
|
||||
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public final class ModdedSettingsHotloadService implements ModdedTickableService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final long POLL_PERIOD_MILLIS = 3_000L;
|
||||
|
||||
private long lastPollAt;
|
||||
private long lastModified;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
lastPollAt = 0L;
|
||||
lastModified = settingsFile().lastModified();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick(MinecraftServer server) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastPollAt < POLL_PERIOD_MILLIS) {
|
||||
return;
|
||||
}
|
||||
lastPollAt = now;
|
||||
long modified = settingsFile().lastModified();
|
||||
if (modified == lastModified) {
|
||||
return;
|
||||
}
|
||||
if (IrisSettings.settings != null) {
|
||||
IrisSettings.invalidate();
|
||||
}
|
||||
IrisSettings.get();
|
||||
lastModified = settingsFile().lastModified();
|
||||
LOGGER.info("Hotloaded settings.json");
|
||||
}
|
||||
|
||||
private static File settingsFile() {
|
||||
return IrisPlatforms.get().dataFile("settings.json");
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* 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.service;
|
||||
|
||||
import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.modded.IrisModdedChunkGenerator;
|
||||
import art.arcane.iris.modded.ModdedDimensionManager;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class ModdedStudioHotloadService implements ModdedTickableService {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Iris");
|
||||
private static final String STUDIO_DIMENSION_PREFIX = "irisworldgen:studio_";
|
||||
private static final long POLL_MILLIS = 250L;
|
||||
private static final long CHECK_LATCH_MILLIS = 1_000L;
|
||||
private static final long RECENT_GENERATION_HOLDOFF_MILLIS = 2_000L;
|
||||
|
||||
private final ConcurrentHashMap<String, Watch> watches = new ConcurrentHashMap<>();
|
||||
private volatile ExecutorService executor;
|
||||
private long lastPollAt;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
if (executor != null) {
|
||||
return;
|
||||
}
|
||||
executor = Executors.newSingleThreadExecutor((Runnable task) -> {
|
||||
Thread thread = new Thread(task, "Iris Studio Hotload");
|
||||
thread.setDaemon(true);
|
||||
thread.setPriority(Thread.MIN_PRIORITY);
|
||||
return thread;
|
||||
});
|
||||
lastPollAt = 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
ExecutorService active = executor;
|
||||
executor = null;
|
||||
if (active != null) {
|
||||
active.shutdownNow();
|
||||
}
|
||||
watches.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerTick(MinecraftServer server) {
|
||||
ExecutorService active = executor;
|
||||
if (active == null) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastPollAt < POLL_MILLIS) {
|
||||
return;
|
||||
}
|
||||
lastPollAt = now;
|
||||
List<ModdedDimensionManager.Handle> handles = ModdedDimensionManager.handles();
|
||||
Set<String> seen = null;
|
||||
for (ModdedDimensionManager.Handle handle : handles) {
|
||||
String dimensionId = handle.dimensionId();
|
||||
if (!dimensionId.startsWith(STUDIO_DIMENSION_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
IrisModdedChunkGenerator generator = handle.generator();
|
||||
if (generator == null) {
|
||||
continue;
|
||||
}
|
||||
Engine engine = generator.engineIfBound();
|
||||
if (engine == null || engine.isClosed()) {
|
||||
continue;
|
||||
}
|
||||
if (seen == null) {
|
||||
seen = new HashSet<>();
|
||||
}
|
||||
seen.add(dimensionId);
|
||||
Watch watch = watches.computeIfAbsent(dimensionId, (String key) -> new Watch());
|
||||
if (throttled(generator, engine, now)) {
|
||||
continue;
|
||||
}
|
||||
if (!watch.latch.flip()) {
|
||||
continue;
|
||||
}
|
||||
if (!watch.busy.compareAndSet(false, true)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
active.execute(() -> {
|
||||
try {
|
||||
poll(dimensionId, watch, generator, engine);
|
||||
} finally {
|
||||
watch.busy.set(false);
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException rejected) {
|
||||
watch.busy.set(false);
|
||||
}
|
||||
}
|
||||
if (!watches.isEmpty()) {
|
||||
Set<String> keep = seen == null ? Set.of() : seen;
|
||||
watches.keySet().removeIf((String key) -> !keep.contains(key));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean throttled(IrisModdedChunkGenerator generator, Engine engine, long now) {
|
||||
if (now - generator.lastChunkGenAt() < RECENT_GENERATION_HOLDOFF_MILLIS) {
|
||||
return true;
|
||||
}
|
||||
PregeneratorJob job = PregeneratorJob.getInstance();
|
||||
return job != null && job.targetsWorldName(engine.getWorld().name());
|
||||
}
|
||||
|
||||
private void poll(String dimensionId, Watch watch, IrisModdedChunkGenerator generator, Engine engine) {
|
||||
try {
|
||||
if (watch.engine != engine) {
|
||||
watch.engine = engine;
|
||||
watch.folder = new ReactiveFolder(
|
||||
engine.getData().getDataFolder(),
|
||||
(KList<File> created, KList<File> changed, KList<File> deleted) -> hotload(dimensionId, generator, engine),
|
||||
new KList<>(".iob", ".json"),
|
||||
new KList<>(".iris"),
|
||||
new KList<>());
|
||||
return;
|
||||
}
|
||||
ReactiveFolder folder = watch.folder;
|
||||
if (folder != null) {
|
||||
folder.check();
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio hotload check failed for {}", dimensionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void hotload(String dimensionId, IrisModdedChunkGenerator generator, Engine engine) {
|
||||
if (engine.isClosed()) {
|
||||
return;
|
||||
}
|
||||
long start = System.currentTimeMillis();
|
||||
try {
|
||||
engine.hotloadSilently();
|
||||
generator.onHotload();
|
||||
LOGGER.info("Iris studio hotload {} pack={} {}ms", dimensionId, engine.getDimension().getLoadKey(), System.currentTimeMillis() - start);
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Iris studio hotload failed for {}", dimensionId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Watch {
|
||||
private final ChronoLatch latch = new ChronoLatch(CHECK_LATCH_MILLIS, false);
|
||||
private final AtomicBoolean busy = new AtomicBoolean(false);
|
||||
private volatile Engine engine;
|
||||
private volatile ReactiveFolder folder;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user