mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-13 02:15:46 +00:00
x
This commit is contained in:
Vendored
+1
-1
@@ -1 +1 @@
|
||||
446774718
|
||||
-676456073
|
||||
@@ -146,11 +146,26 @@ public class IrisSettings {
|
||||
public int chunkLoadTimeoutSeconds = 15;
|
||||
public int timeoutWarnIntervalMs = 500;
|
||||
public int saveIntervalMs = 30_000;
|
||||
public int maxResidentTectonicPlates = 96;
|
||||
public int mantleBackpressureWaitMs = 25;
|
||||
public int mantleBackpressureTimeoutMs = 60_000;
|
||||
|
||||
public int getChunkLoadTimeoutSeconds() {
|
||||
return Math.max(5, Math.min(chunkLoadTimeoutSeconds, 120));
|
||||
}
|
||||
|
||||
public int getMaxResidentTectonicPlates() {
|
||||
return Math.max(16, maxResidentTectonicPlates);
|
||||
}
|
||||
|
||||
public int getMantleBackpressureWaitMs() {
|
||||
return Math.max(5, Math.min(mantleBackpressureWaitMs, 1_000));
|
||||
}
|
||||
|
||||
public int getMantleBackpressureTimeoutMs() {
|
||||
return Math.max(5_000, Math.min(mantleBackpressureTimeoutMs, 600_000));
|
||||
}
|
||||
|
||||
public int getTimeoutWarnIntervalMs() {
|
||||
return Math.max(timeoutWarnIntervalMs, 250);
|
||||
}
|
||||
@@ -199,6 +214,7 @@ public class IrisSettings {
|
||||
public boolean useCustomColorsIngame = true;
|
||||
public boolean adjustVanillaHeight = false;
|
||||
public boolean autoIngestDatapacks = true;
|
||||
public boolean autoImportDatapackStructures = true;
|
||||
public String forceMainWorld = "";
|
||||
public int spinh = -20;
|
||||
public int spins = 7;
|
||||
|
||||
@@ -21,7 +21,6 @@ package art.arcane.iris.core.commands;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
@@ -32,12 +31,7 @@ import java.util.List;
|
||||
|
||||
@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports (Modrinth)")
|
||||
public class CommandDatapack implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", aliases = {"pull", "update", "sync"})
|
||||
@Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", aliases = {"pull"})
|
||||
public void ingest(
|
||||
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", defaultValue = "false")
|
||||
boolean restart
|
||||
@@ -47,7 +41,7 @@ public class CommandDatapack implements DirectorExecutor {
|
||||
J.a(() -> DatapackIngestService.ingestAll(sender, restart));
|
||||
}
|
||||
|
||||
@Director(description = "List configured datapack imports and their installed versions", aliases = {"ls", "status"})
|
||||
@Director(description = "List configured datapack imports and their installed versions", aliases = {"ls"})
|
||||
public void list() {
|
||||
VolmitSender sender = sender();
|
||||
KList<String> configured = DatapackIngestService.collectConfiguredImports();
|
||||
@@ -68,7 +62,7 @@ public class CommandDatapack implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", aliases = {"rm", "delete"})
|
||||
@Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", aliases = {"rm"})
|
||||
public void remove(
|
||||
@Param(description = "The datapack id (folder name) shown by /iris datapack list")
|
||||
String id
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,6 @@ import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.engine.object.*;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
@@ -31,13 +30,8 @@ import art.arcane.iris.util.common.format.C;
|
||||
import java.awt.*;
|
||||
|
||||
|
||||
@Director(name = "edit", origin = DirectorOrigin.PLAYER, studio = true, description = "Edit something")
|
||||
@Director(name = "edit", origin = DirectorOrigin.PLAYER, description = "Edit something")
|
||||
public class CommandEdit implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
private boolean noStudio() {
|
||||
if (!sender().isPlayer()) {
|
||||
sender().sendMessage(C.RED + "Players only!");
|
||||
|
||||
@@ -22,6 +22,7 @@ import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.service.ObjectStudioSaveService;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
@@ -29,6 +30,7 @@ import art.arcane.iris.util.common.director.specialhandlers.ObjectHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.StructureHandler;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
@@ -129,6 +131,12 @@ public class CommandFind implements DirectorExecutor {
|
||||
sender().sendMessage(C.RED + "Unknown structure: " + structure);
|
||||
return;
|
||||
}
|
||||
if (!StructureReachability.isReachable(e, structure)) {
|
||||
KList<String> miss = StructureReachability.missingBiomeKeys(e, structure);
|
||||
sender().sendMessage(C.YELLOW + structure + " cannot generate in this world (its required biomes are not produced by this pack"
|
||||
+ (miss.isEmpty() ? "" : ": needs " + String.join("/", miss)) + ").");
|
||||
return;
|
||||
}
|
||||
StructureSearchResult result = target.getWorld().locateNearestStructure(target.getLocation(), match, 100, true);
|
||||
if (result == null || result.getLocation() == null) {
|
||||
sender().sendMessage(C.YELLOW + "No " + structure + " found within range of you.");
|
||||
|
||||
@@ -21,21 +21,17 @@ package art.arcane.iris.core.commands;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.IrisWorlds;
|
||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||
import art.arcane.iris.core.lifecycle.WorldLifecycleService;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.platform.ChunkReplacementListener;
|
||||
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.director.DirectorContext;
|
||||
import art.arcane.volmlib.util.director.DirectorParameterHandler;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
@@ -88,24 +84,8 @@ import static org.bukkit.Bukkit.getServer;
|
||||
|
||||
@Director(name = "iris", aliases = {"ir", "irs"}, description = "Basic Command")
|
||||
public class CommandIris implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@Director(description = "Download/update external datapack imports declared in pack dimensions (alias of /iris datapack ingest)", aliases = {"ingestpacks"})
|
||||
public void ingest(
|
||||
@Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", defaultValue = "false")
|
||||
boolean restart
|
||||
) {
|
||||
VolmitSender ingestSender = sender();
|
||||
ingestSender.sendMessage(C.GRAY + "Starting datapack ingest...");
|
||||
J.a(() -> DatapackIngestService.ingestAll(ingestSender, restart));
|
||||
}
|
||||
|
||||
private CommandStudio studio;
|
||||
private CommandPregen pregen;
|
||||
private CommandSettings settings;
|
||||
private CommandObject object;
|
||||
private CommandStructure structure;
|
||||
private CommandWhat what;
|
||||
@@ -120,7 +100,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
String worldNameToCheck = "YourWorldName";
|
||||
VolmitSender sender = Iris.getSender();
|
||||
|
||||
@Director(description = "Create a new world", aliases = {"+", "c"})
|
||||
@Director(description = "Create a new world", aliases = {"c"})
|
||||
public void create(
|
||||
@Param(aliases = "world-name", description = "The name of the world to create")
|
||||
String name,
|
||||
@@ -527,7 +507,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Remove an Iris world", aliases = {"del", "rm", "delete"}, sync = true)
|
||||
@Director(description = "Remove an Iris world", aliases = {"rm"}, sync = true)
|
||||
public void remove(
|
||||
@Param(description = "The world to remove")
|
||||
World world,
|
||||
|
||||
@@ -36,7 +36,6 @@ import art.arcane.volmlib.util.data.Cuboid;
|
||||
import art.arcane.iris.util.common.data.IrisCustomData;
|
||||
import art.arcane.iris.util.common.data.registry.Materials;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
@@ -61,14 +60,9 @@ import java.io.IOException;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.*;
|
||||
|
||||
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris object manipulation")
|
||||
@Director(name = "object", aliases = "o", origin = DirectorOrigin.PLAYER, description = "Iris object manipulation")
|
||||
public class CommandObject implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", aliases = {"std", "s"}, sync = true)
|
||||
@Director(description = "Open an object studio world (grid of every object; dimension optional, defaults to all packs)", sync = true)
|
||||
public void studio(
|
||||
@Param(defaultValue = "null", description = "Optional dimension whose object pack to lay out; omit to aggregate objects from every pack", aliases = "dim", customHandler = NullableDimensionHandler.class)
|
||||
IrisDimension dimension,
|
||||
@@ -334,7 +328,7 @@ public class CommandObject implements DirectorExecutor {
|
||||
}
|
||||
|
||||
@Director(description = "Make tree leaves vanilla-decay-plausible (every leaf within 6 blocks of a log)",
|
||||
origin = DirectorOrigin.BOTH, studio = false)
|
||||
origin = DirectorOrigin.BOTH)
|
||||
public void plausibilize(
|
||||
@Param(description = "Object key, prefix (trees/), or filesystem path",
|
||||
customHandler = ObjectTargetHandler.class)
|
||||
@@ -536,7 +530,7 @@ public class CommandObject implements DirectorExecutor {
|
||||
|
||||
}
|
||||
|
||||
@Director(description = "Get a powder that reveals objects", studio = true, aliases = "d")
|
||||
@Director(description = "Get a powder that reveals objects", aliases = "d")
|
||||
public void dust() {
|
||||
player().getInventory().addItem(WandSVC.createDust());
|
||||
sender().playSound(Sound.AMBIENT_SOUL_SAND_VALLEY_ADDITIONS, 1f, 1.5f);
|
||||
@@ -720,7 +714,7 @@ public class CommandObject implements DirectorExecutor {
|
||||
sender().sendMessage(C.GREEN + "Successfully object to saved: " + dimension.getLoadKey() + "/objects/" + name);
|
||||
}
|
||||
|
||||
@Director(description = "Shift a selection in your looking direction", aliases = "-")
|
||||
@Director(description = "Shift a selection in your looking direction")
|
||||
public void shift(
|
||||
@Param(description = "The amount to shift by", defaultValue = "1")
|
||||
int amount
|
||||
@@ -751,7 +745,7 @@ public class CommandObject implements DirectorExecutor {
|
||||
sender().playSound(Sound.ENTITY_ITEM_FRAME_ROTATE_ITEM, 1f, 0.55f);
|
||||
}
|
||||
|
||||
@Director(description = "Undo a number of pastes", aliases = "-")
|
||||
@Director(description = "Undo a number of pastes", aliases = "u")
|
||||
public void undo(
|
||||
@Param(description = "The amount of pastes to undo", defaultValue = "1")
|
||||
int amount
|
||||
@@ -762,7 +756,7 @@ public class CommandObject implements DirectorExecutor {
|
||||
sender().sendMessage(C.BLUE + "Reverted " + actualReverts + C.BLUE +" pastes!");
|
||||
}
|
||||
|
||||
@Director(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DirectorOrigin.PLAYER, studio = true)
|
||||
@Director(description = "Gets an object wand and grabs the current WorldEdit selection.", aliases = "we", origin = DirectorOrigin.PLAYER)
|
||||
public void we() {
|
||||
if (!Bukkit.getPluginManager().isPluginEnabled("WorldEdit")) {
|
||||
sender().sendMessage(C.RED + "You can't get a WorldEdit selection without WorldEdit, you know.");
|
||||
|
||||
@@ -23,7 +23,6 @@ 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.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
@@ -33,12 +32,7 @@ import java.io.File;
|
||||
|
||||
@Director(name = "pack", aliases = {"pk"}, description = "Pack validation and maintenance")
|
||||
public class CommandPack implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v", "check"})
|
||||
@Director(description = "Validate a pack (or all packs) and re-publish results", aliases = {"v"})
|
||||
public void validate(
|
||||
@Param(description = "The pack folder name to validate (leave empty for all)", defaultValue = "")
|
||||
String pack
|
||||
@@ -75,7 +69,7 @@ public class CommandPack implements DirectorExecutor {
|
||||
runValidate(s, target);
|
||||
}
|
||||
|
||||
@Director(description = "Restore most recent trashed files for a pack", aliases = {"r", "undelete"})
|
||||
@Director(description = "Restore most recent trashed files for a pack", aliases = {"r"})
|
||||
public void restore(
|
||||
@Param(description = "The pack folder name to restore")
|
||||
String pack
|
||||
@@ -99,7 +93,7 @@ public class CommandPack implements DirectorExecutor {
|
||||
s.sendMessage(C.GRAY + "Re-run /iris pack validate " + pack + " to re-check.");
|
||||
}
|
||||
|
||||
@Director(description = "Show cached validation status for a pack", aliases = {"s", "info"})
|
||||
@Director(description = "Show cached validation status for a pack", aliases = {"s"})
|
||||
public void status(
|
||||
@Param(description = "The pack folder name", defaultValue = "")
|
||||
String pack
|
||||
|
||||
@@ -23,7 +23,6 @@ import art.arcane.iris.core.gui.PregeneratorJob;
|
||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
@@ -33,11 +32,6 @@ import org.bukkit.util.Vector;
|
||||
|
||||
@Director(name = "pregen", aliases = "pregenerate", description = "Pregenerate your Iris worlds!")
|
||||
public class CommandPregen implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@Director(description = "Pregenerate a world")
|
||||
public void start(
|
||||
@Param(description = "The radius of the pregen in blocks", aliases = "size")
|
||||
@@ -81,7 +75,7 @@ public class CommandPregen implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Pause / continue the active pregeneration task", aliases = {"t", "resume", "unpause"})
|
||||
@Director(description = "Pause / continue the active pregeneration task", aliases = {"resume"})
|
||||
public void pause() {
|
||||
if (PregeneratorJob.pauseResume()) {
|
||||
sender().sendMessage(C.GREEN + "Paused/unpaused pregeneration task, now: " + (PregeneratorJob.isPaused() ? "Paused" : "Running") + ".");
|
||||
|
||||
@@ -22,10 +22,11 @@ import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.core.structure.StructureImporter;
|
||||
import art.arcane.iris.core.structure.StructureIndexService;
|
||||
import art.arcane.iris.core.structure.StructureModeHandler;
|
||||
import art.arcane.iris.core.structure.VillageImporter;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.IrisStructureLocator;
|
||||
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||
import art.arcane.iris.engine.framework.StructureAssembler;
|
||||
import art.arcane.iris.engine.framework.StructureReachability;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
@@ -33,7 +34,6 @@ import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
@@ -51,17 +51,14 @@ import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.util.StructureSearchResult;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Director(name = "structure", aliases = {"struct", "str"}, studio = true, description = "Iris structure tools (index, import, info)")
|
||||
@Director(name = "structure", aliases = {"struct", "str"}, description = "Iris structure tools (index, import, info)")
|
||||
public class CommandStructure implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", aliases = {"ls", "index"}, origin = DirectorOrigin.BOTH)
|
||||
@Director(description = "Regenerate structure-index.json listing all vanilla, datapack & iris structures", aliases = {"ls"}, origin = DirectorOrigin.BOTH)
|
||||
public void list(
|
||||
@Param(description = "The dimension whose pack to index", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
@@ -75,106 +72,9 @@ public class CommandStructure implements DirectorExecutor {
|
||||
sender().sendMessage(C.GREEN + "Wrote structure index: " + C.WHITE + file.getPath());
|
||||
}
|
||||
|
||||
@Director(description = "Import a vanilla/datapack structure NBT into a pack as editable Iris resources", aliases = {"imp"}, origin = DirectorOrigin.BOTH)
|
||||
public void importStructure(
|
||||
@Director(name = "import", description = "Import EVERY structure - vanilla AND ingested datapacks - into this pack as editable Iris resources, always overwriting. Rebuilds jigsaw structures (villages, outposts, datapack jigsaws) as editable pool/piece graphs, imports every structure template NBT as an object, and assembles the multi-template structures (shipwrecks, ruined portals, ocean ruins, nether fossils). Run after ingesting a datapack and restarting. Regenerate chunks or use a fresh world for the imported copies to place.", aliases = {"import-all", "reimport", "imp", "all"}, origin = DirectorOrigin.BOTH)
|
||||
public void importAll(
|
||||
@Param(description = "The dimension whose pack to import into", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "Structure key to import, e.g. minecraft:igloo/top or nova_structures:desert_temple")
|
||||
String key,
|
||||
@Param(description = "Name for the imported structure (defaults from the key)", defaultValue = "")
|
||||
String name,
|
||||
@Param(description = "overwrite | add-only | merge", defaultValue = "overwrite", customHandler = StructureModeHandler.class)
|
||||
String mode
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
NamespacedKey nk = NamespacedKey.fromString(key.toLowerCase());
|
||||
if (nk == null) {
|
||||
sender().sendMessage(C.RED + "Invalid structure key: " + key);
|
||||
return;
|
||||
}
|
||||
String n = name == null || name.isEmpty() ? StructureImporter.deriveName(nk) : name;
|
||||
StructureImporter.Result result = StructureImporter.importStructure(data, nk, n, StructureImporter.parseMode(mode));
|
||||
sender().sendMessage((result.success() ? C.GREEN : C.RED) + result.message());
|
||||
if (result.success()) {
|
||||
sender().sendMessage(C.GRAY + "Reference it from a biome/region/dimension structures list as '" + n + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Import a full vanilla/datapack JIGSAW structure (e.g. a village) by reconstructing its template-pool graph into editable Iris pools, pieces & connectors", aliases = {"iv", "village"}, origin = DirectorOrigin.BOTH)
|
||||
public void importVillage(
|
||||
@Param(description = "The dimension whose pack to import into", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "Jigsaw structure key to import, e.g. minecraft:village_plains")
|
||||
String key,
|
||||
@Param(description = "Name for the imported structure (defaults from the key)", defaultValue = "")
|
||||
String name,
|
||||
@Param(description = "overwrite | add-only | merge", defaultValue = "overwrite", customHandler = StructureModeHandler.class)
|
||||
String mode
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
NamespacedKey nk = NamespacedKey.fromString(key.toLowerCase());
|
||||
if (nk == null) {
|
||||
sender().sendMessage(C.RED + "Invalid structure key: " + key);
|
||||
return;
|
||||
}
|
||||
String n = name == null || name.isEmpty() ? StructureImporter.deriveName(nk) : name;
|
||||
VillageImporter.Result result = VillageImporter.importVillage(data, nk, n, StructureImporter.parseMode(mode));
|
||||
sender().sendMessage((result.success() ? C.GREEN : C.RED) + result.message());
|
||||
if (result.success()) {
|
||||
sender().sendMessage(C.GRAY + "Reference it from a biome/region/dimension structures list as '" + n + "'.");
|
||||
sender().sendMessage(C.GRAY + "Inspect the rebuilt jigsaw graph with: /iris structure info " + dimension.getLoadKey() + " " + n);
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Import EVERY registered structure - vanilla AND ingested datapacks - into a pack: jigsaw structures rebuilt as pool graphs, single-template structures imported as objects. Idempotent in add-only mode.", aliases = {"import-all", "ia"}, origin = DirectorOrigin.BOTH)
|
||||
public void importAllVanilla(
|
||||
@Param(description = "The dimension whose pack to import into", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "overwrite | add-only | merge", defaultValue = "add-only", customHandler = StructureModeHandler.class)
|
||||
String mode,
|
||||
@Param(description = "Also import non-jigsaw single-template structures", name = "include-non-jigsaw", aliases = {"single", "nbt"}, defaultValue = "true")
|
||||
boolean includeNonJigsaw
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
BulkStructureImporter.Report report = BulkStructureImporter.importAllVanilla(data, StructureImporter.parseMode(mode), includeNonJigsaw, sender());
|
||||
if (report.imported() > 0) {
|
||||
sender().sendMessage(C.GRAY + "Reference imported structures from a biome/region/dimension structures list, or run /iris structure list " + dimension.getLoadKey() + " to see the refreshed index.");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Import EVERY vanilla structure TEMPLATE (the piece NBTs under minecraft:.../...) as editable Iris objects, including the non-jigsaw templates that import-all cannot reach. Idempotent in add-only mode.", aliases = {"import-templates", "it"}, origin = DirectorOrigin.BOTH)
|
||||
public void importTemplates(
|
||||
@Param(description = "The dimension whose pack to import into", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "overwrite | add-only | merge", defaultValue = "add-only", customHandler = StructureModeHandler.class)
|
||||
String mode
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
BulkStructureImporter.Report report = BulkStructureImporter.importAllTemplates(data, StructureImporter.parseMode(mode), sender());
|
||||
if (report.imported() > 0) {
|
||||
sender().sendMessage(C.GRAY + "Reference imported templates from a biome/region/dimension structures list, or run /iris structure list " + dimension.getLoadKey() + " to see the refreshed index.");
|
||||
}
|
||||
}
|
||||
|
||||
@Director(description = "Re-ingest EVERY registered structure - vanilla AND ingested datapacks - plus every jigsaw template from scratch (overwrite), regenerating all .iob objects so they pick up the latest jigsaw/structure-block conversion. Use this after updating Iris or after ingesting datapacks if imported structures show raw markers.", aliases = {"reimport", "ri"}, origin = DirectorOrigin.BOTH)
|
||||
public void reingest(
|
||||
@Param(description = "The dimension whose pack to re-ingest", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
@@ -182,17 +82,17 @@ public class CommandStructure implements DirectorExecutor {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Re-ingesting all vanilla structures and jigsaws for " + C.WHITE + dimension.getLoadKey() + C.GREEN + " (overwrite)...");
|
||||
sender().sendMessage(C.GREEN + "Importing all vanilla & datapack structures into " + C.WHITE + dimension.getLoadKey() + C.GREEN + " (overwrite)...");
|
||||
BulkStructureImporter.Report jigsaws = BulkStructureImporter.importAllVanilla(data, StructureImporter.Mode.OVERWRITE, true, sender());
|
||||
BulkStructureImporter.Report templates = BulkStructureImporter.importAllTemplates(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
BulkStructureImporter.Report groups = BulkStructureImporter.importTemplateGroups(data, StructureImporter.Mode.OVERWRITE, sender());
|
||||
int imported = jigsaws.imported() + templates.imported() + groups.imported();
|
||||
int failed = jigsaws.failed() + templates.failed() + groups.failed();
|
||||
sender().sendMessage(C.GREEN + "Re-ingest complete: " + C.WHITE + imported + C.GREEN + " objects rewritten, " + C.WHITE + failed + C.GREEN + " failed.");
|
||||
sender().sendMessage(C.GRAY + "Regenerate chunks (or use a fresh world) for structures to place from the rewritten objects. Run /iris structure list " + dimension.getLoadKey() + " to refresh the index.");
|
||||
sender().sendMessage(C.GREEN + "Import complete: " + C.WHITE + imported + C.GREEN + " structures/objects written, " + C.WHITE + failed + C.GREEN + " failed.");
|
||||
sender().sendMessage(C.GRAY + "Reference them from a biome/region/dimension 'structures' list, or run /iris structure list " + dimension.getLoadKey() + " to refresh the index. Regenerate chunks for changes to take effect.");
|
||||
}
|
||||
|
||||
@Director(description = "Locate every vanilla/datapack/iris structure to verify which are locatable in this world. Heavy synchronous search per structure - keep the radius modest. 'Not found' can mean rarer than the radius, a different dimension (nether/end structures never appear in the overworld), or a structure that generates but cannot be located; generation itself happens during chunk decoration, independent of locate.", aliases = {"verify", "test", "locateall"}, origin = DirectorOrigin.BOTH, sync = true)
|
||||
@Director(description = "Locate every vanilla/datapack/iris structure to verify which are locatable in this world. Heavy synchronous search per structure - keep the radius modest. 'Not found' can mean rarer than the radius, a different dimension (nether/end structures never appear in the overworld), or a structure that generates but cannot be located; generation itself happens during chunk decoration, independent of locate.", aliases = {"locateall"}, origin = DirectorOrigin.BOTH, sync = true)
|
||||
public void verify(
|
||||
@Param(description = "The dimension to verify", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@@ -210,18 +110,39 @@ public class CommandStructure implements DirectorExecutor {
|
||||
|
||||
sender().sendMessage(C.GREEN + "Verifying structures in " + C.WHITE + world.getName() + C.GREEN + " from " + center.getBlockX() + "," + center.getBlockZ() + " within " + searchRadius + " chunks...");
|
||||
|
||||
Engine engine = null;
|
||||
PlatformChunkGenerator access = IrisToolbelt.access(world);
|
||||
if (access != null) {
|
||||
engine = access.getEngine();
|
||||
}
|
||||
Set<String> reachable = engine == null ? Collections.emptySet() : StructureReachability.reachableKeys(engine);
|
||||
|
||||
int found = 0;
|
||||
int missing = 0;
|
||||
int unreachable = 0;
|
||||
int irisPlaced = 0;
|
||||
KList<String> notFound = new KList<>();
|
||||
KList<String> cannotGenerate = new KList<>();
|
||||
for (org.bukkit.generator.structure.Structure structure : Registry.STRUCTURE) {
|
||||
NamespacedKey key = structure.getKey();
|
||||
String keyName = key == null ? structure.toString() : key.toString();
|
||||
boolean isIrisPlaced = engine != null && IrisStructureLocator.suppressesVanilla(engine, keyName);
|
||||
boolean isReachable = engine != null && reachable.contains(keyName.toLowerCase());
|
||||
if (engine != null && !isIrisPlaced && !isReachable) {
|
||||
unreachable++;
|
||||
KList<String> miss = StructureReachability.missingBiomeKeys(engine, keyName);
|
||||
cannotGenerate.add(keyName + (miss.isEmpty() ? "" : " (needs " + String.join("/", miss) + ")"));
|
||||
continue;
|
||||
}
|
||||
if (isIrisPlaced) {
|
||||
irisPlaced++;
|
||||
}
|
||||
try {
|
||||
StructureSearchResult result = world.locateNearestStructure(center, structure, searchRadius, true);
|
||||
if (result != null && result.getLocation() != null) {
|
||||
found++;
|
||||
Location l = result.getLocation();
|
||||
sender().sendMessage(C.GREEN + "[ok] " + C.WHITE + keyName + C.GREEN + " @ " + l.getBlockX() + "," + l.getBlockZ());
|
||||
sender().sendMessage((isIrisPlaced ? C.AQUA + "[iris] " : C.GREEN + "[ok] ") + C.WHITE + keyName + C.GREEN + " @ " + l.getBlockX() + "," + l.getBlockZ());
|
||||
} else {
|
||||
missing++;
|
||||
notFound.add(keyName);
|
||||
@@ -232,9 +153,14 @@ public class CommandStructure implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
sender().sendMessage(C.GREEN + "Structure verify: " + C.WHITE + found + C.GREEN + " generate, " + C.WHITE + missing + C.GREEN + " not found within " + searchRadius + " chunks.");
|
||||
sender().sendMessage(C.GREEN + "Structure verify: " + C.WHITE + found + C.GREEN + " located (" + irisPlaced + " iris-placed), "
|
||||
+ C.WHITE + unreachable + C.GREEN + " cannot generate here, "
|
||||
+ C.WHITE + missing + C.GREEN + " reachable-but-not-found within " + searchRadius + " chunks.");
|
||||
if (!cannotGenerate.isEmpty()) {
|
||||
sender().sendMessage(C.RED + "Cannot generate (required biomes absent from this pack): " + C.GRAY + String.join(", ", cannotGenerate));
|
||||
}
|
||||
if (!notFound.isEmpty()) {
|
||||
sender().sendMessage(C.YELLOW + "Not found (rarer than radius, disabled, or non-generating): " + C.GRAY + String.join(", ", notFound));
|
||||
sender().sendMessage(C.YELLOW + "Reachable but not found (rarer than radius): " + C.GRAY + String.join(", ", notFound));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,15 +28,12 @@ import art.arcane.iris.core.service.StudioSVC;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.*;
|
||||
import art.arcane.iris.engine.platform.ChunkReplacementListener;
|
||||
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.collection.KSet;
|
||||
import art.arcane.iris.util.common.director.DirectorContext;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
import art.arcane.iris.util.common.director.handlers.DimensionHandler;
|
||||
import art.arcane.iris.util.common.director.specialhandlers.NullableDimensionHandler;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
@@ -89,13 +86,8 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands", studio = true)
|
||||
@Director(name = "studio", aliases = {"std", "s"}, description = "Studio Commands")
|
||||
public class CommandStudio implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
private CommandEdit edit;
|
||||
//private CommandDeepSearch deepSearch;
|
||||
|
||||
@@ -103,21 +95,6 @@ public class CommandStudio implements DirectorExecutor {
|
||||
return duration.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase();
|
||||
}
|
||||
|
||||
//TODO fix pack trimming
|
||||
@Director(description = "Download a project.", aliases = "dl")
|
||||
public void download(
|
||||
@Param(name = "pack", description = "The pack to download", defaultValue = "overworld", aliases = "project")
|
||||
String pack,
|
||||
@Param(name = "branch", description = "The branch to download from", defaultValue = "stable")
|
||||
String branch,
|
||||
//@Param(name = "trim", description = "Whether or not to download a trimmed version (do not enable when editing)", defaultValue = "false")
|
||||
//boolean trim,
|
||||
@Param(name = "overwrite", description = "Whether or not to overwrite the pack with the downloaded one", aliases = "force", defaultValue = "false")
|
||||
boolean overwrite
|
||||
) {
|
||||
new CommandIris().download(pack, branch, overwrite);
|
||||
}
|
||||
|
||||
@Director(description = "Open a new studio world", aliases = "o", sync = true)
|
||||
public void open(
|
||||
@Param(defaultValue = "default", description = "The dimension to open a studio for", aliases = "dim", customHandler = DimensionHandler.class)
|
||||
@@ -128,7 +105,7 @@ public class CommandStudio implements DirectorExecutor {
|
||||
Iris.service(StudioSVC.class).open(sender(), seed, dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Open VSCode for a dimension", aliases = {"vsc", "edit"})
|
||||
@Director(description = "Open VSCode for a dimension", aliases = {"vsc"})
|
||||
public void vscode(
|
||||
@Param(defaultValue = "default", description = "The dimension to open VSCode for", aliases = "dim", customHandler = DimensionHandler.class)
|
||||
IrisDimension dimension
|
||||
@@ -137,7 +114,7 @@ public class CommandStudio implements DirectorExecutor {
|
||||
Iris.service(StudioSVC.class).openVSCode(sender(), dimension.getLoadKey());
|
||||
}
|
||||
|
||||
@Director(description = "Close an open studio project", aliases = {"x", "c"}, sync = true)
|
||||
@Director(description = "Close an open studio project", aliases = {"x"}, sync = true)
|
||||
public void close() {
|
||||
VolmitSender commandSender = sender();
|
||||
if (!Iris.service(StudioSVC.class).isProjectOpen()) {
|
||||
@@ -191,7 +168,7 @@ public class CommandStudio implements DirectorExecutor {
|
||||
sender().sendMessage(C.GREEN + "The \"" + dimension.getName() + "\" pack has version: " + dimension.getVersion());
|
||||
}
|
||||
|
||||
@Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap", "n", "generator", "gen"})
|
||||
@Director(description = "Open the noise explorer (External GUI)", aliases = {"nmap"})
|
||||
public void noise(
|
||||
@Param(description = "Optional pack generator to preview", defaultValue = "null", contextual = true)
|
||||
IrisGenerator generator,
|
||||
|
||||
@@ -27,7 +27,6 @@ import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import art.arcane.iris.util.common.director.DirectorHelp;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
@@ -44,13 +43,8 @@ import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Director(name = "what", origin = DirectorOrigin.PLAYER, studio = true, description = "Iris What?")
|
||||
@Director(name = "what", origin = DirectorOrigin.PLAYER, description = "Iris What?")
|
||||
public class CommandWhat implements DirectorExecutor {
|
||||
@Director(description = "Show help tree for this command group", aliases = {"?"})
|
||||
public void help() {
|
||||
DirectorHelp.print(sender(), getClass());
|
||||
}
|
||||
|
||||
@Director(description = "What is in my hand?", origin = DirectorOrigin.PLAYER)
|
||||
public void hand() {
|
||||
try {
|
||||
|
||||
@@ -24,7 +24,10 @@ import art.arcane.iris.core.ServerConfigurator;
|
||||
import art.arcane.iris.core.datapack.ModrinthResolver.ResolvedDatapack;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.project.IrisProject;
|
||||
import art.arcane.iris.core.structure.BulkStructureImporter;
|
||||
import art.arcane.iris.core.structure.StructureImporter;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisImportedStructureControl;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -53,10 +56,12 @@ import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public final class DatapackIngestService {
|
||||
private static final String USER_AGENT = "VolmitSoftware/Iris (datapack-ingest)";
|
||||
private static final String OVERRIDES_STRIPPED_MARKER = ".iris-overrides-stripped";
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
private DatapackIngestService() {
|
||||
@@ -78,6 +83,7 @@ public final class DatapackIngestService {
|
||||
}
|
||||
if (!restarting) {
|
||||
refreshWorkspaces();
|
||||
autoImportDatapackStructures();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,12 +120,13 @@ public final class DatapackIngestService {
|
||||
KList<File> worldFolders = ServerConfigurator.getDatapacksFolder();
|
||||
String mcVersion = serverMcVersion();
|
||||
Manifest manifest = readManifest(root);
|
||||
boolean stripOverrides = resolveStripOverrides();
|
||||
|
||||
message(sender, C.GRAY + "Ingesting " + C.WHITE + urls.size() + C.GRAY + " datapack import(s)" + (mcVersion == null ? "" : " for MC " + mcVersion) + "...");
|
||||
message(sender, C.GRAY + "Ingesting " + C.WHITE + urls.size() + C.GRAY + " datapack import(s)" + (mcVersion == null ? "" : " for MC " + mcVersion) + (stripOverrides ? C.GRAY + " (datapackOverrides=false: vanilla-key overrides will be stripped)" : "") + "...");
|
||||
|
||||
for (String url : urls) {
|
||||
try {
|
||||
ingestSingle(sender, url, mcVersion, cacheDir, stagingDir, worldFolders, manifest, report);
|
||||
ingestSingle(sender, url, mcVersion, cacheDir, stagingDir, worldFolders, manifest, report, stripOverrides);
|
||||
} catch (Exception e) {
|
||||
report.failed.add(url + " - " + e.getMessage());
|
||||
message(sender, C.RED + " Failed: " + C.WHITE + url + C.RED + " - " + e.getMessage());
|
||||
@@ -132,8 +139,8 @@ public final class DatapackIngestService {
|
||||
|
||||
if (report.changed()) {
|
||||
message(sender, C.YELLOW + "New datapack structures were installed. A server restart is required for them to register and generate.");
|
||||
message(sender, C.GRAY + "Disable a vanilla structure via the dimension 'importedStructures.disabled' list to let an imported replacement take over, or place any imported key through a 'structures' placement.");
|
||||
message(sender, C.GRAY + "To edit or manually place them as Iris resources, after the restart run /iris structure importAllVanilla <dimension> - it imports vanilla and datapack structures together as editable jigsaw pools, pieces & objects.");
|
||||
message(sender, C.GRAY + "After the restart their jigsaw pools, pieces & objects are imported automatically (set general.autoImportDatapackStructures=false to disable), or run /iris structure import <dimension> to import everything on demand. Reference an imported key from a 'structures' placement to position it manually.");
|
||||
message(sender, C.GRAY + "Datapacks replace matching vanilla structure keys and generate their own structures by default; set the dimension 'importedStructures.datapackOverrides' to false to keep vanilla untouched and stop ALL datapack structures from generating - they stay importable for manual placement only.");
|
||||
if (restart) {
|
||||
ServerConfigurator.restart();
|
||||
} else {
|
||||
@@ -153,12 +160,13 @@ public final class DatapackIngestService {
|
||||
if (staged == null || staged.length == 0) {
|
||||
return;
|
||||
}
|
||||
boolean stripOverrides = resolveStripOverrides();
|
||||
for (File stagedDir : staged) {
|
||||
if (!new File(stagedDir, "pack.mcmeta").isFile()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
install(stagedDir, worldFolders, stagedDir.getName(), false);
|
||||
install(stagedDir, worldFolders, stagedDir.getName(), false, stripOverrides);
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
@@ -210,7 +218,7 @@ public final class DatapackIngestService {
|
||||
return readManifest(Iris.instance.getDataFolder("datapacks")).entries;
|
||||
}
|
||||
|
||||
private static void ingestSingle(VolmitSender sender, String url, String mcVersion, File cacheDir, File stagingDir, KList<File> worldFolders, Manifest manifest, Report report) throws IOException {
|
||||
private static void ingestSingle(VolmitSender sender, String url, String mcVersion, File cacheDir, File stagingDir, KList<File> worldFolders, Manifest manifest, Report report, boolean stripOverrides) throws IOException {
|
||||
ResolvedDatapack resolved = ModrinthResolver.resolve(url, mcVersion);
|
||||
String id = deriveId(resolved);
|
||||
File stagedDir = new File(stagingDir, id);
|
||||
@@ -222,7 +230,7 @@ public final class DatapackIngestService {
|
||||
&& new File(stagedDir, "pack.mcmeta").isFile();
|
||||
|
||||
if (sameVersion) {
|
||||
install(stagedDir, worldFolders, id, false);
|
||||
install(stagedDir, worldFolders, id, false, stripOverrides);
|
||||
report.upToDate.add(id + " (" + safe(resolved.getVersionNumber()) + ")");
|
||||
message(sender, C.GRAY + " Up to date: " + C.WHITE + id + C.GRAY + " " + safe(resolved.getVersionNumber()));
|
||||
return;
|
||||
@@ -247,7 +255,7 @@ public final class DatapackIngestService {
|
||||
throw new IOException(id + " is not a valid datapack (missing pack.mcmeta)");
|
||||
}
|
||||
|
||||
install(stagedDir, worldFolders, id, true);
|
||||
install(stagedDir, worldFolders, id, true, stripOverrides);
|
||||
|
||||
Entry entry = existing != null ? existing : new Entry();
|
||||
entry.url = url;
|
||||
@@ -257,6 +265,7 @@ public final class DatapackIngestService {
|
||||
entry.sha1 = checksum;
|
||||
entry.filename = resolved.getFileName();
|
||||
entry.installedEpoch = System.currentTimeMillis();
|
||||
entry.structuresImported = false;
|
||||
manifest.put(entry);
|
||||
|
||||
report.updated.add(id + " (" + safe(resolved.getVersionNumber()) + ")");
|
||||
@@ -299,10 +308,13 @@ public final class DatapackIngestService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void install(File stagedDir, KList<File> worldFolders, String id, boolean force) throws IOException {
|
||||
private static void install(File stagedDir, KList<File> worldFolders, String id, boolean force, boolean stripOverrides) throws IOException {
|
||||
for (File worldFolder : worldFolders) {
|
||||
File target = new File(worldFolder, id);
|
||||
if (!force && target.isDirectory() && new File(target, "pack.mcmeta").isFile()) {
|
||||
File marker = new File(target, OVERRIDES_STRIPPED_MARKER);
|
||||
boolean installed = target.isDirectory() && new File(target, "pack.mcmeta").isFile();
|
||||
boolean stripStateMatches = marker.isFile() == stripOverrides;
|
||||
if (!force && installed && stripStateMatches) {
|
||||
continue;
|
||||
}
|
||||
if (!worldFolder.exists()) {
|
||||
@@ -310,6 +322,104 @@ public final class DatapackIngestService {
|
||||
}
|
||||
IO.delete(target);
|
||||
IO.copyDirectory(stagedDir.toPath(), target.toPath());
|
||||
if (stripOverrides) {
|
||||
stripVanillaStructureOverrides(target);
|
||||
writeMarker(marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean resolveStripOverrides() {
|
||||
try (Stream<IrisData> stream = ServerConfigurator.allPacks()) {
|
||||
return stream.anyMatch(DatapackIngestService::packDisablesOverrides);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean packDisablesOverrides(IrisData data) {
|
||||
if (data == null || data.getDimensionLoader() == null) {
|
||||
return false;
|
||||
}
|
||||
for (IrisDimension dimension : data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys())) {
|
||||
if (dimension == null) {
|
||||
continue;
|
||||
}
|
||||
IrisImportedStructureControl control = dimension.getImportedStructures();
|
||||
if (control != null && !control.isDatapackOverrides()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void stripVanillaStructureOverrides(File datapackRoot) {
|
||||
File minecraftData = new File(new File(datapackRoot, "data"), "minecraft");
|
||||
if (!minecraftData.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
String[] relativeTrees = {
|
||||
"worldgen" + File.separator + "structure_set",
|
||||
"worldgen" + File.separator + "structure",
|
||||
"worldgen" + File.separator + "template_pool",
|
||||
"structure"
|
||||
};
|
||||
for (String tree : relativeTrees) {
|
||||
File dir = new File(minecraftData, tree);
|
||||
if (dir.exists()) {
|
||||
IO.delete(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeMarker(File marker) {
|
||||
try {
|
||||
Files.writeString(marker.toPath(), "stripped", StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void autoImportDatapackStructures() {
|
||||
if (!IrisSettings.get().getGeneral().autoImportDatapackStructures) {
|
||||
return;
|
||||
}
|
||||
File root = Iris.instance.getDataFolder("datapacks");
|
||||
Manifest manifest = readManifest(root);
|
||||
if (manifest.entries.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
boolean pending = false;
|
||||
for (Entry entry : manifest.entries) {
|
||||
if (!entry.structuresImported) {
|
||||
pending = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
Iris.info("Importing datapack structures (jigsaw pools, pieces & objects) into packs that declare datapackImports...");
|
||||
AtomicInteger packs = new AtomicInteger();
|
||||
try (Stream<IrisData> stream = ServerConfigurator.allPacks()) {
|
||||
stream.forEach(data -> {
|
||||
if (data == null || !hasImports(data)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
BulkStructureImporter.importDatapackStructures(data, StructureImporter.Mode.ADD_ONLY, Iris.getSender());
|
||||
packs.incrementAndGet();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (Entry entry : manifest.entries) {
|
||||
entry.structuresImported = true;
|
||||
}
|
||||
writeManifest(root, manifest);
|
||||
if (packs.get() > 0) {
|
||||
Iris.info("Datapack structure import finished for " + packs.get() + " pack(s). Reference the imported keys from a 'structures' placement to position them manually.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,6 +650,7 @@ public final class DatapackIngestService {
|
||||
public String sha1;
|
||||
public String filename;
|
||||
public long installedEpoch;
|
||||
public boolean structuresImported;
|
||||
}
|
||||
|
||||
private static final class Manifest {
|
||||
|
||||
@@ -169,6 +169,15 @@ public class PregeneratorJob implements PregenListener {
|
||||
return targetName != null && targetName.equalsIgnoreCase(world.getName());
|
||||
}
|
||||
|
||||
public boolean targetsWorldName(String worldName) {
|
||||
if (worldName == null || engine == null || engine.getWorld() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String targetName = engine.getWorld().name();
|
||||
return targetName != null && targetName.equalsIgnoreCase(worldName);
|
||||
}
|
||||
|
||||
private static Color parseColor(String c) {
|
||||
String v = (c.startsWith("#") ? c : "#" + c).trim();
|
||||
try {
|
||||
|
||||
@@ -97,6 +97,18 @@ public interface INMSBinding {
|
||||
return new KList<>();
|
||||
}
|
||||
|
||||
default KList<String> getReachableStructureKeys(World world) {
|
||||
return new KList<>();
|
||||
}
|
||||
|
||||
default KList<String> getStructureBiomeKeys(String structureKey) {
|
||||
return new KList<>();
|
||||
}
|
||||
|
||||
default KList<String> getPossibleBiomeKeys(World world) {
|
||||
return new KList<>();
|
||||
}
|
||||
|
||||
boolean isBukkit();
|
||||
|
||||
int getBiomeId(Biome biome);
|
||||
@@ -161,6 +173,10 @@ public interface INMSBinding {
|
||||
return 441;
|
||||
}
|
||||
|
||||
default boolean purgeChunk(World world, int x, int z) {
|
||||
throw new UnsupportedOperationException("The active NMS binding does not support chunk purge (regen).");
|
||||
}
|
||||
|
||||
boolean missingDimensionTypes(String... keys);
|
||||
|
||||
default boolean injectBukkit() {
|
||||
|
||||
@@ -117,6 +117,11 @@ public class NMSBinding1X implements INMSBinding {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean purgeChunk(World world, int x, int z) {
|
||||
throw new UnsupportedOperationException("Regen is unavailable: Iris is running in Bukkit fallback mode (NMS disabled). Regen requires the native chunk system.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMap<Material, List<BlockProperty>> getBlockProperties() {
|
||||
KMap<Material, List<BlockProperty>> map = new KMap<>();
|
||||
|
||||
@@ -284,6 +284,7 @@ public class IrisPregenerator {
|
||||
Mantle mantle = getMantle();
|
||||
if (mantle != null) {
|
||||
mantle.trim(0, 0);
|
||||
mantle.unloadTectonicPlate(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,12 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
private final AtomicLong lastChunkCleanup = new AtomicLong(M.ms());
|
||||
private final Object permitMonitor = new Object();
|
||||
private volatile Engine metricsEngine;
|
||||
private volatile Mantle cachedMantle;
|
||||
private final int maxResidentTectonicPlates;
|
||||
private final int mantleBackpressureWaitMs;
|
||||
private final long mantleBackpressureTimeoutMs;
|
||||
private final long mantleReclaimIntervalMs;
|
||||
private final AtomicLong lastMantleReclaim = new AtomicLong(0L);
|
||||
|
||||
public AsyncPregenMethod(World world, int unusedThreads) {
|
||||
if (!PaperLib.isPaper()) {
|
||||
@@ -136,6 +142,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
this.lastUse = new ConcurrentHashMap<>();
|
||||
this.adaptiveInFlightLimit = new AtomicInteger(this.threads);
|
||||
this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4)));
|
||||
this.maxResidentTectonicPlates = pregen.getMaxResidentTectonicPlates();
|
||||
this.mantleBackpressureWaitMs = pregen.getMantleBackpressureWaitMs();
|
||||
this.mantleBackpressureTimeoutMs = pregen.getMantleBackpressureTimeoutMs();
|
||||
this.mantleReclaimIntervalMs = 1_000L;
|
||||
}
|
||||
|
||||
private IrisPaperLikeBackendMode resolvePaperLikeBackendMode(IrisSettings.IrisSettingsPregen pregen) {
|
||||
@@ -467,6 +477,75 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
}
|
||||
}
|
||||
|
||||
private Mantle resolveMantle() {
|
||||
Mantle cached = cachedMantle;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
Mantle resolved = getMantle();
|
||||
if (resolved != null) {
|
||||
cachedMantle = resolved;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private void enforceMantleBudget() {
|
||||
int cap = maxResidentTectonicPlates;
|
||||
if (cap <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mantle mantle = resolveMantle();
|
||||
if (mantle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
long now = M.ms();
|
||||
long lastReclaimAt = lastMantleReclaim.get();
|
||||
if (now - lastReclaimAt >= mantleReclaimIntervalMs && lastMantleReclaim.compareAndSet(lastReclaimAt, now)) {
|
||||
mantle.trim(0L, 0);
|
||||
mantle.unloadTectonicPlate(0);
|
||||
}
|
||||
|
||||
if (mantle.getLoadedRegionCount() <= cap) {
|
||||
return;
|
||||
}
|
||||
|
||||
long waitStart = M.ms();
|
||||
long lastLog = 0L;
|
||||
while (mantle.getLoadedRegionCount() > cap) {
|
||||
mantle.trim(0L, 0);
|
||||
int freed = mantle.unloadTectonicPlate(0);
|
||||
int resident = mantle.getLoadedRegionCount();
|
||||
if (resident <= cap) {
|
||||
break;
|
||||
}
|
||||
|
||||
long elapsed = M.ms() - waitStart;
|
||||
if (elapsed >= mantleBackpressureTimeoutMs) {
|
||||
Iris.warn("Pregen mantle backpressure exceeded " + mantleBackpressureTimeoutMs + "ms with " + resident
|
||||
+ " tectonic plates resident (cap " + cap + "); proceeding to avoid deadlock. "
|
||||
+ "Raise pregen.maxResidentTectonicPlates if this persists. " + metricsSnapshot());
|
||||
return;
|
||||
}
|
||||
|
||||
long logNow = M.ms();
|
||||
if (logNow - lastLog >= 5_000L) {
|
||||
lastLog = logNow;
|
||||
Iris.warn("Pregen mantle backpressure: " + resident + " tectonic plates resident (cap " + cap
|
||||
+ "), freed " + freed + " last pass, waited " + elapsed + "ms.");
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(mantleBackpressureWaitMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
Iris.info("Async pregen init: world=" + world.getName()
|
||||
@@ -522,6 +601,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
|
||||
public void generateChunk(int x, int z, PregenListener listener) {
|
||||
listener.onChunkGenerating(x, z);
|
||||
periodicChunkCleanup();
|
||||
enforceMantleBudget();
|
||||
try {
|
||||
long waitStart = M.ms();
|
||||
synchronized (permitMonitor) {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.pregenerator.methods;
|
||||
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.pregenerator.PregenListener;
|
||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import org.bukkit.World;
|
||||
|
||||
public class RegenPregenMethod implements PregeneratorMethod {
|
||||
private final World world;
|
||||
private final AsyncPregenMethod delegate;
|
||||
|
||||
public RegenPregenMethod(World world) {
|
||||
this.world = world;
|
||||
this.delegate = new AsyncPregenMethod(world, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
delegate.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
delegate.save();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRegions(int x, int z, PregenListener listener) {
|
||||
return delegate.supportsRegions(x, z, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethod(int x, int z) {
|
||||
return "Regen";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAsyncChunkMode() {
|
||||
return delegate.isAsyncChunkMode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateRegion(int x, int z, PregenListener listener) {
|
||||
delegate.generateRegion(x, z, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateChunk(int x, int z, PregenListener listener) {
|
||||
purge(x, z);
|
||||
delegate.generateChunk(x, z, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mantle getMantle() {
|
||||
return delegate.getMantle();
|
||||
}
|
||||
|
||||
private void purge(int x, int z) {
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int radius = Math.max(0, engine.getMantle().getRealRadius());
|
||||
Mantle mantle = engine.getMantle().getMantle();
|
||||
for (int dx = -radius; dx <= radius; dx++) {
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
mantle.deleteChunk(x + dx, z + dz);
|
||||
}
|
||||
}
|
||||
|
||||
INMS.get().purgeChunk(world, x, z);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||
import art.arcane.iris.core.structure.StructureSchemaKeys;
|
||||
import art.arcane.iris.engine.object.annotations.*;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
@@ -43,6 +44,7 @@ import java.awt.*;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InaccessibleObjectException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -245,6 +247,22 @@ public class SchemaBuilder {
|
||||
} else {
|
||||
Iris.error("Cannot find Registry Loader for type " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName());
|
||||
}
|
||||
} else if (k.isAnnotationPresent(RegistryListStructure.class)) {
|
||||
String key = "enum-iris-structure-placement";
|
||||
|
||||
if (!definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("enum", new JSONArray(StructureSchemaKeys.collect(
|
||||
INMS.get().getStructureKeys(),
|
||||
Arrays.asList(data.getStructureLoader().getPossibleKeys()),
|
||||
Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0])));
|
||||
definitions.put(key, j);
|
||||
}
|
||||
|
||||
fancyType = "Structure";
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or imported Iris structure (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListBlockType.class)) {
|
||||
String key = "enum-block-type";
|
||||
|
||||
@@ -522,6 +540,23 @@ public class SchemaBuilder {
|
||||
} else {
|
||||
Iris.error("Cannot find Registry Loader for type (list schema) " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName());
|
||||
}
|
||||
} else if (k.isAnnotationPresent(RegistryListStructure.class)) {
|
||||
fancyType = "List<Structure>";
|
||||
String key = "enum-iris-structure-placement";
|
||||
|
||||
if (!definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("enum", new JSONArray(StructureSchemaKeys.collect(
|
||||
INMS.get().getStructureKeys(),
|
||||
Arrays.asList(data.getStructureLoader().getPossibleKeys()),
|
||||
Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0])));
|
||||
definitions.put(key, j);
|
||||
}
|
||||
|
||||
JSONObject items = new JSONObject();
|
||||
items.put("$ref", "#/definitions/" + key);
|
||||
prop.put("items", items);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or imported Iris structure (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListBlockType.class)) {
|
||||
fancyType = "List of Block Types";
|
||||
String key = "enum-block-type";
|
||||
|
||||
@@ -308,7 +308,7 @@ public class IrisEngineSVC implements IrisService {
|
||||
}
|
||||
|
||||
try {
|
||||
engine.getMantle().trim(activeIdleDuration(engineWorld), activeTectonicLimit(engineWorld));
|
||||
engine.getMantle().trim(activeIdleDuration(engine), activeTectonicLimit(engine));
|
||||
} catch (Throwable e) {
|
||||
if (isMantleClosed(e)) {
|
||||
close();
|
||||
@@ -336,7 +336,7 @@ public class IrisEngineSVC implements IrisService {
|
||||
|
||||
try {
|
||||
long unloadStart = System.currentTimeMillis();
|
||||
int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : activeTectonicLimit(engineWorld));
|
||||
int count = engine.getMantle().unloadTectonicPlate(IrisSettings.get().getPerformance().getEngineSVC().forceMulticoreWrite ? 0 : activeTectonicLimit(engine));
|
||||
if (count > 0) {
|
||||
Iris.debug(C.GOLD + "Unloaded " + C.YELLOW + count + " TectonicPlates in " + C.RED + Form.duration(System.currentTimeMillis() - unloadStart, 2));
|
||||
}
|
||||
@@ -357,27 +357,26 @@ public class IrisEngineSVC implements IrisService {
|
||||
return tectonicLimit.get() / Math.max(worlds.size(), 1);
|
||||
}
|
||||
|
||||
private int activeTectonicLimit(@Nullable World world) {
|
||||
int limit = tectonicLimit();
|
||||
if (world == null) {
|
||||
return limit;
|
||||
private boolean pregenTargets(Engine engine) {
|
||||
if (engine == null || engine.getWorld() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) {
|
||||
return pregeneratorJob != null && pregeneratorJob.targetsWorldName(engine.getWorld().name());
|
||||
}
|
||||
|
||||
private int activeTectonicLimit(Engine engine) {
|
||||
int limit = tectonicLimit();
|
||||
if (!pregenTargets(engine)) {
|
||||
return limit;
|
||||
}
|
||||
|
||||
return Math.max(1, Math.min(limit, Math.max(2, limit / 8)));
|
||||
}
|
||||
|
||||
private long activeIdleDuration(@Nullable World world) {
|
||||
if (world == null) {
|
||||
return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive());
|
||||
}
|
||||
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
if (pregeneratorJob == null || !pregeneratorJob.targetsWorld(world)) {
|
||||
private long activeIdleDuration(Engine engine) {
|
||||
if (!pregenTargets(engine)) {
|
||||
return TimeUnit.SECONDS.toMillis(IrisSettings.get().getPerformance().getMantleKeepAlive());
|
||||
}
|
||||
|
||||
@@ -411,13 +410,7 @@ public class IrisEngineSVC implements IrisService {
|
||||
return true;
|
||||
}
|
||||
|
||||
World world = engine.getWorld().realWorld();
|
||||
if (world == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PregeneratorJob pregeneratorJob = PregeneratorJob.getInstance();
|
||||
return pregeneratorJob != null && pregeneratorJob.targetsWorld(world);
|
||||
return pregenTargets(engine);
|
||||
}
|
||||
|
||||
private boolean shouldSkipForMaintenance(@Nullable World world) {
|
||||
|
||||
@@ -157,31 +157,31 @@ public final class BulkStructureImporter {
|
||||
try {
|
||||
templateKeys = enumerateTemplateKeys();
|
||||
} catch (Throwable e) {
|
||||
sender.sendMessage(C.RED + "Failed to enumerate vanilla structure templates via the server ResourceManager: " + e);
|
||||
sender.sendMessage(C.RED + "Failed to enumerate structure templates via the server ResourceManager: " + e);
|
||||
return new Report(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
List<String> vanilla = new ArrayList<>();
|
||||
List<String> all = new ArrayList<>();
|
||||
for (String key : templateKeys) {
|
||||
if (key != null && key.startsWith("minecraft:")) {
|
||||
vanilla.add(key);
|
||||
if (key != null && !key.isBlank()) {
|
||||
all.add(key);
|
||||
}
|
||||
}
|
||||
Collections.sort(vanilla);
|
||||
Collections.sort(all);
|
||||
|
||||
int total = vanilla.size();
|
||||
int total = all.size();
|
||||
int imported = 0;
|
||||
int skipped = 0;
|
||||
int failed = 0;
|
||||
|
||||
if (total == 0) {
|
||||
sender.sendMessage(C.YELLOW + "No vanilla structure templates were found under the 'structure' resource path.");
|
||||
sender.sendMessage(C.YELLOW + "No structure templates were found under the 'structure' resource path.");
|
||||
return new Report(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
sender.sendMessage(C.GREEN + "Importing " + C.WHITE + total + C.GREEN + " vanilla structure templates (mode=" + mode + ")...");
|
||||
sender.sendMessage(C.GREEN + "Importing " + C.WHITE + total + C.GREEN + " structure templates (mode=" + mode + ")...");
|
||||
|
||||
for (String keyString : vanilla) {
|
||||
for (String keyString : all) {
|
||||
NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase());
|
||||
if (nk == null) {
|
||||
failed++;
|
||||
@@ -217,7 +217,117 @@ public final class BulkStructureImporter {
|
||||
return new Report(total, imported, skipped, failed);
|
||||
}
|
||||
|
||||
private static String templateNameFor(String key) {
|
||||
public static Report importDatapackStructures(IrisData data, StructureImporter.Mode mode, VolmitSender sender) {
|
||||
KList<String> keys = INMS.get().getStructureKeys();
|
||||
List<String> datapack = new ArrayList<>();
|
||||
for (String k : keys) {
|
||||
if (k != null && !k.isBlank() && !k.startsWith("minecraft:")) {
|
||||
datapack.add(k);
|
||||
}
|
||||
}
|
||||
Collections.sort(datapack);
|
||||
|
||||
int total = datapack.size();
|
||||
int imported = 0;
|
||||
int skipped = 0;
|
||||
int failed = 0;
|
||||
|
||||
if (total == 0) {
|
||||
sender.sendMessage(C.YELLOW + "No datapack (non-minecraft) structures are registered. Ingest a datapack and restart first, then run this again.");
|
||||
} else {
|
||||
sender.sendMessage(C.GREEN + "Importing " + C.WHITE + total + C.GREEN + " datapack structures (mode=" + mode + ")...");
|
||||
for (String keyString : datapack) {
|
||||
NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase());
|
||||
if (nk == null) {
|
||||
failed++;
|
||||
sender.sendMessage(C.RED + "[fail] " + keyString + ": invalid key");
|
||||
continue;
|
||||
}
|
||||
String name = StructureImporter.deriveName(nk);
|
||||
|
||||
try {
|
||||
VillageImporter.Result jigsaw = VillageImporter.importVillage(data, nk, name, mode);
|
||||
if (jigsaw.success()) {
|
||||
imported++;
|
||||
sender.sendMessage(C.GRAY + "[jigsaw] " + keyString + " -> " + name);
|
||||
continue;
|
||||
}
|
||||
|
||||
String message = jigsaw.message() == null ? "" : jigsaw.message();
|
||||
if (message.contains("is not a jigsaw structure")) {
|
||||
StructureImporter.Result single = StructureImporter.importStructure(data, nk, name, mode);
|
||||
if (single.success()) {
|
||||
imported++;
|
||||
sender.sendMessage(C.GRAY + "[single] " + keyString + " -> " + name);
|
||||
} else if (single.message() != null && single.message().startsWith("Skipped")) {
|
||||
skipped++;
|
||||
} else if (single.message() != null && single.message().contains("No loadable structure NBT")) {
|
||||
skipped++;
|
||||
} else {
|
||||
failed++;
|
||||
sender.sendMessage(C.RED + "[fail] " + keyString + ": " + single.message());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.startsWith("Skipped")) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
failed++;
|
||||
sender.sendMessage(C.RED + "[fail] " + keyString + ": " + message);
|
||||
} catch (Throwable e) {
|
||||
failed++;
|
||||
sender.sendMessage(C.RED + "[fail] " + keyString + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
List<String> templateKeys = enumerateTemplateKeys();
|
||||
List<String> datapackTemplates = new ArrayList<>();
|
||||
for (String key : templateKeys) {
|
||||
if (key != null && !key.startsWith("minecraft:")) {
|
||||
datapackTemplates.add(key);
|
||||
}
|
||||
}
|
||||
Collections.sort(datapackTemplates);
|
||||
if (!datapackTemplates.isEmpty()) {
|
||||
sender.sendMessage(C.GREEN + "Importing " + C.WHITE + datapackTemplates.size() + C.GREEN + " datapack structure templates...");
|
||||
for (String keyString : datapackTemplates) {
|
||||
NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase());
|
||||
if (nk == null) {
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
String name = templateNameFor(keyString);
|
||||
try {
|
||||
StructureImporter.Result result = StructureImporter.importStructure(data, nk, name, mode, true);
|
||||
if (result.success()) {
|
||||
imported++;
|
||||
} else if (result.message() != null && result.message().startsWith("Skipped")) {
|
||||
skipped++;
|
||||
} else {
|
||||
failed++;
|
||||
sender.sendMessage(C.RED + "[fail] " + keyString + ": " + result.message());
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
failed++;
|
||||
sender.sendMessage(C.RED + "[fail] " + keyString + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
sender.sendMessage(C.YELLOW + "Could not enumerate datapack templates via the server ResourceManager: " + e.getMessage());
|
||||
}
|
||||
|
||||
StructureIndexService.write(data);
|
||||
sender.sendMessage(C.GREEN + "Datapack structure import complete: " + C.WHITE + imported + C.GREEN + " imported, " + C.WHITE + skipped + C.GREEN + " skipped, " + C.WHITE + failed + C.GREEN + " failed.");
|
||||
return new Report(total, imported, skipped, failed);
|
||||
}
|
||||
|
||||
static String templateNameFor(String key) {
|
||||
int colon = key.indexOf(':');
|
||||
String namespace = colon >= 0 ? key.substring(0, colon) : "minecraft";
|
||||
String path = colon >= 0 ? key.substring(colon + 1) : key;
|
||||
|
||||
@@ -21,6 +21,7 @@ package art.arcane.iris.core.structure;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.LegacyTileData;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
@@ -150,8 +151,8 @@ public final class StructureImporter {
|
||||
object.write(objectFile);
|
||||
writeJson(pieceFile, pieceJson(name));
|
||||
if (objectOnly) {
|
||||
poolFile.delete();
|
||||
structureFile.delete();
|
||||
IO.deleteUp(poolFile);
|
||||
IO.deleteUp(structureFile);
|
||||
} else {
|
||||
writeJson(poolFile, poolJson(name));
|
||||
writeJson(structureFile, structureJson(name, key.toString(), Math.max(w, d)));
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.structure;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.director.DirectorParameterHandler;
|
||||
|
||||
public class StructureModeHandler implements DirectorParameterHandler<String> {
|
||||
@Override
|
||||
public KList<String> getPossibilities() {
|
||||
KList<String> options = new KList<>();
|
||||
options.add("overwrite");
|
||||
options.add("add-only");
|
||||
options.add("merge");
|
||||
return options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String parse(String in, boolean force) {
|
||||
return in == null || in.isBlank() ? "overwrite" : in.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> type) {
|
||||
return type.equals(String.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRandomDefault() {
|
||||
return "overwrite";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.structure;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public final class StructureSchemaKeys {
|
||||
private StructureSchemaKeys() {
|
||||
}
|
||||
|
||||
public static KList<String> collect(Collection<String> vanillaStructureKeys, Collection<String> importedStructureKeys, Collection<String> jigsawPieceKeys) {
|
||||
Set<String> pieces = new HashSet<>();
|
||||
if (jigsawPieceKeys != null) {
|
||||
for (String piece : jigsawPieceKeys) {
|
||||
if (piece != null && !piece.isBlank()) {
|
||||
pieces.add(piece);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TreeSet<String> merged = new TreeSet<>();
|
||||
if (vanillaStructureKeys != null) {
|
||||
for (String key : vanillaStructureKeys) {
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
merged.add(StructureImporter.deriveName(key));
|
||||
}
|
||||
}
|
||||
|
||||
if (importedStructureKeys != null) {
|
||||
for (String key : importedStructureKeys) {
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (pieces.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
merged.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return new KList<>(merged);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ package art.arcane.iris.core.structure;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.volmlib.util.io.IO;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
@@ -606,8 +607,8 @@ public final class VillageImporter {
|
||||
}
|
||||
|
||||
private static void removeStrayPieceArtifacts(IrisData data, String pieceName) {
|
||||
new File(data.getDataFolder(), "jigsaw-pools/" + pieceName + ".json").delete();
|
||||
new File(data.getDataFolder(), "structures/" + pieceName + ".json").delete();
|
||||
IO.deleteUp(new File(data.getDataFolder(), "jigsaw-pools/" + pieceName + ".json"));
|
||||
IO.deleteUp(new File(data.getDataFolder(), "structures/" + pieceName + ".json"));
|
||||
}
|
||||
|
||||
private static boolean writeEmptyPiece(IrisData data, String pieceName) {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.engine.framework;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.bukkit.World;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
/**
|
||||
* Determines which vanilla/datapack structures can ever generate in a world, using the same biome
|
||||
* gate vanilla applies: a structure is reachable when its biome filter intersects the set of vanilla
|
||||
* biomes the pack actually produces (its {@code possibleBiomes}). Structures whose required biomes are
|
||||
* never produced cannot generate and must not be scanned for by {@code /locate} or {@code /iris find}
|
||||
* (an unbounded scan for an absent biome is what stalls the server).
|
||||
*
|
||||
* <p>The reachable set is fixed per pack/world, so it is cached per {@link IrisData}.
|
||||
*/
|
||||
public final class StructureReachability {
|
||||
private static final Map<IrisData, Set<String>> REACHABLE_CACHE = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private StructureReachability() {
|
||||
}
|
||||
|
||||
public static Set<String> reachableKeys(Engine engine) {
|
||||
if (engine == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
IrisData data = engine.getData();
|
||||
if (data == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> cached = REACHABLE_CACHE.get(data);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
Set<String> built = build(engine);
|
||||
REACHABLE_CACHE.put(data, built);
|
||||
return built;
|
||||
}
|
||||
|
||||
public static boolean isReachable(Engine engine, String structureKey) {
|
||||
if (structureKey == null || structureKey.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return reachableKeys(engine).contains(structureKey.toLowerCase());
|
||||
}
|
||||
|
||||
public static void invalidate(Engine engine) {
|
||||
if (engine == null) {
|
||||
return;
|
||||
}
|
||||
IrisData data = engine.getData();
|
||||
if (data != null) {
|
||||
REACHABLE_CACHE.remove(data);
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> build(Engine engine) {
|
||||
World world = engine.getWorld() == null ? null : engine.getWorld().realWorld();
|
||||
if (world == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
Set<String> reachable = new LinkedHashSet<>();
|
||||
for (String key : INMS.get().getReachableStructureKeys(world)) {
|
||||
if (key != null && !key.isEmpty()) {
|
||||
reachable.add(key.toLowerCase());
|
||||
}
|
||||
}
|
||||
return reachable;
|
||||
}
|
||||
|
||||
/**
|
||||
* The vanilla biome keys (e.g. {@code minecraft:taiga}) from a structure's filter that the pack
|
||||
* does not produce. An empty list means the structure is reachable. Used by the structure verify
|
||||
* diagnostic to explain why a structure cannot generate.
|
||||
*/
|
||||
public static KList<String> missingBiomeKeys(Engine engine, String structureKey) {
|
||||
KList<String> missing = new KList<>();
|
||||
if (engine == null || structureKey == null || structureKey.isEmpty()) {
|
||||
return missing;
|
||||
}
|
||||
World world = engine.getWorld() == null ? null : engine.getWorld().realWorld();
|
||||
if (world == null) {
|
||||
return missing;
|
||||
}
|
||||
Set<String> possible = new LinkedHashSet<>();
|
||||
for (String key : INMS.get().getPossibleBiomeKeys(world)) {
|
||||
if (key != null) {
|
||||
possible.add(key.toLowerCase());
|
||||
}
|
||||
}
|
||||
for (String biomeKey : INMS.get().getStructureBiomeKeys(structureKey)) {
|
||||
if (biomeKey != null && !possible.contains(biomeKey.toLowerCase())) {
|
||||
missing.add(biomeKey);
|
||||
}
|
||||
}
|
||||
return missing;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +1,24 @@
|
||||
package art.arcane.iris.engine.mantle;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.IrisSettings;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.util.common.misc.RegenRuntime;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
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.matter.Matter;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public interface MatterGenerator {
|
||||
long REGEN_PASS_CACHE_TTL_MS = 600000L;
|
||||
Executor DISPATCHER = MultiBurst.burst;
|
||||
ConcurrentHashMap<String, Set<Long>> REGEN_GENERATED_CHUNKS_BY_PASS = new ConcurrentHashMap<>();
|
||||
ConcurrentHashMap<String, Set<Long>> REGEN_CLEARED_CHUNKS_BY_PASS = new ConcurrentHashMap<>();
|
||||
ConcurrentHashMap<String, Set<Long>> REGEN_PLANNED_CHUNKS_BY_PASS = new ConcurrentHashMap<>();
|
||||
ConcurrentHashMap<String, Long> REGEN_PASS_TOUCHED_MS = new ConcurrentHashMap<>();
|
||||
|
||||
Engine getEngine();
|
||||
|
||||
@@ -50,84 +36,22 @@ public interface MatterGenerator {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean useMulticore = multicore;
|
||||
String threadName = Thread.currentThread().getName();
|
||||
boolean regenThread = threadName.startsWith("Iris-Regen-");
|
||||
boolean traceRegen = regenThread && IrisSettings.get().getGeneral().isDebug();
|
||||
boolean forceRegen = regenThread;
|
||||
String regenPassKey = forceRegen ? resolveRegenPassKey(threadName) : null;
|
||||
boolean optimizedRegen = forceRegen && !IrisSettings.get().getGeneral().isDebug() && regenPassKey != null;
|
||||
int writeRadius = optimizedRegen ? Math.min(getRadius(), getRealRadius()) : getRadius();
|
||||
Set<Long> clearedChunks = optimizedRegen ? getRegenPassSet(REGEN_CLEARED_CHUNKS_BY_PASS, regenPassKey) : new HashSet<>();
|
||||
Set<Long> partialChunks = forceRegen ? null : new HashSet<>();
|
||||
Set<Long> plannedChunks = optimizedRegen ? getRegenPassSet(REGEN_PLANNED_CHUNKS_BY_PASS, regenPassKey) : null;
|
||||
int writeRadius = getRadius();
|
||||
Set<Long> partialChunks = new HashSet<>();
|
||||
|
||||
if (optimizedRegen) {
|
||||
touchRegenPass(regenPassKey);
|
||||
}
|
||||
|
||||
if (traceRegen) {
|
||||
Iris.info("Regen matter start: center=" + x + "," + z
|
||||
+ " radius=" + getRadius()
|
||||
+ " realRadius=" + getRealRadius()
|
||||
+ " writeRadius=" + writeRadius
|
||||
+ " multicore=" + useMulticore
|
||||
+ " components=" + getComponents().size()
|
||||
+ " optimized=" + optimizedRegen
|
||||
+ " passKey=" + (regenPassKey == null ? "none" : regenPassKey)
|
||||
+ " thread=" + threadName);
|
||||
}
|
||||
|
||||
try (MantleWriter writer = new MantleWriter(getEngine().getMantle(), getMantle(), x, z, writeRadius, useMulticore)) {
|
||||
try (MantleWriter writer = new MantleWriter(getEngine().getMantle(), getMantle(), x, z, writeRadius, multicore)) {
|
||||
for (Pair<List<MantleComponent>, Integer> pair : getComponents()) {
|
||||
int rawPassRadius = pair.getB();
|
||||
int passRadius = optimizedRegen ? Math.min(rawPassRadius, writeRadius) : rawPassRadius;
|
||||
String passFlags = pair.getA().stream().map(component -> component.getFlag().toString()).collect(Collectors.joining(","));
|
||||
String passFlagKey = optimizedRegen ? regenPassKey + "|" + passFlags : null;
|
||||
Set<Long> generatedChunks = passFlagKey == null ? null : getRegenPassSet(REGEN_GENERATED_CHUNKS_BY_PASS, passFlagKey);
|
||||
int visitedChunks = 0;
|
||||
int clearedCount = 0;
|
||||
int plannedSkipped = 0;
|
||||
int componentSkipped = 0;
|
||||
int componentForcedReset = 0;
|
||||
int launchedLayers = 0;
|
||||
int dedupSkipped = 0;
|
||||
List<CompletableFuture<Void>> launchedTasks = useMulticore ? new ArrayList<>() : null;
|
||||
|
||||
if (passFlagKey != null) {
|
||||
touchRegenPass(passFlagKey);
|
||||
}
|
||||
if (traceRegen) {
|
||||
Iris.info("Regen matter pass start: center=" + x + "," + z
|
||||
+ " passRadius=" + passRadius
|
||||
+ " rawPassRadius=" + rawPassRadius
|
||||
+ " flags=[" + passFlags + "]");
|
||||
}
|
||||
int passRadius = pair.getB();
|
||||
List<CompletableFuture<Void>> launchedTasks = multicore ? new ArrayList<>() : null;
|
||||
|
||||
for (int i = -passRadius; i <= passRadius; i++) {
|
||||
for (int j = -passRadius; j <= passRadius; j++) {
|
||||
int passX = x + i;
|
||||
int passZ = z + j;
|
||||
visitedChunks++;
|
||||
long passKey = chunkKey(passX, passZ);
|
||||
if (generatedChunks != null && !generatedChunks.add(passKey)) {
|
||||
dedupSkipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
MantleChunk<Matter> chunk = writer.acquireChunk(passX, passZ);
|
||||
if (forceRegen) {
|
||||
if (clearedChunks.add(passKey)) {
|
||||
chunk.deleteSlices(BlockData.class);
|
||||
chunk.deleteSlices(String.class);
|
||||
chunk.deleteSlices(TileWrapper.class);
|
||||
chunk.flag(MantleFlag.PLANNED, false);
|
||||
clearedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!forceRegen && chunk.isFlagged(MantleFlag.PLANNED)) {
|
||||
plannedSkipped++;
|
||||
if (chunk.isFlagged(MantleFlag.PLANNED)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -136,9 +60,7 @@ public interface MatterGenerator {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean componentAlreadyGenerated = !forceRegen && chunk.isFlagged(component.getFlag());
|
||||
if (componentAlreadyGenerated) {
|
||||
componentSkipped++;
|
||||
if (chunk.isFlagged(component.getFlag())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -166,12 +88,6 @@ public interface MatterGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
if (forceRegen && chunk.isFlagged(component.getFlag())) {
|
||||
chunk.flag(component.getFlag(), false);
|
||||
componentForcedReset++;
|
||||
}
|
||||
|
||||
launchedLayers++;
|
||||
int finalPassX = passX;
|
||||
int finalPassZ = passZ;
|
||||
MantleChunk<Matter> finalChunk = chunk;
|
||||
@@ -179,7 +95,7 @@ public interface MatterGenerator {
|
||||
Runnable task = () -> finalChunk.raiseFlagUnchecked(finalComponent.getFlag(),
|
||||
() -> finalComponent.generateLayer(writer, finalPassX, finalPassZ, context));
|
||||
|
||||
if (useMulticore) {
|
||||
if (multicore) {
|
||||
launchedTasks.add(CompletableFuture.runAsync(task, DISPATCHER));
|
||||
} else {
|
||||
task.run();
|
||||
@@ -188,25 +104,11 @@ public interface MatterGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
if (useMulticore) {
|
||||
if (multicore) {
|
||||
for (CompletableFuture<Void> launchedTask : launchedTasks) {
|
||||
launchedTask.join();
|
||||
}
|
||||
}
|
||||
|
||||
if (traceRegen) {
|
||||
Iris.info("Regen matter pass done: center=" + x + "," + z
|
||||
+ " passRadius=" + passRadius
|
||||
+ " rawPassRadius=" + rawPassRadius
|
||||
+ " visited=" + visitedChunks
|
||||
+ " cleared=" + clearedCount
|
||||
+ " dedupSkipped=" + dedupSkipped
|
||||
+ " plannedSkipped=" + plannedSkipped
|
||||
+ " componentSkipped=" + componentSkipped
|
||||
+ " componentForcedReset=" + componentForcedReset
|
||||
+ " launchedLayers=" + launchedLayers
|
||||
+ " flags=[" + passFlags + "]");
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = -getRealRadius(); i <= getRealRadius(); i++) {
|
||||
@@ -214,69 +116,16 @@ public interface MatterGenerator {
|
||||
int realX = x + i;
|
||||
int realZ = z + j;
|
||||
long realKey = chunkKey(realX, realZ);
|
||||
if (plannedChunks != null && !plannedChunks.add(realKey)) {
|
||||
continue;
|
||||
}
|
||||
if (partialChunks != null && partialChunks.contains(realKey)) {
|
||||
if (partialChunks.contains(realKey)) {
|
||||
continue;
|
||||
}
|
||||
writer.acquireChunk(realX, realZ).flag(MantleFlag.PLANNED, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (traceRegen) {
|
||||
Iris.info("Regen matter done: center=" + x + "," + z
|
||||
+ " markedRealRadius=" + getRealRadius()
|
||||
+ " forceRegen=" + forceRegen);
|
||||
}
|
||||
}
|
||||
|
||||
private static long chunkKey(int x, int z) {
|
||||
return (((long) x) << 32) ^ (z & 0xffffffffL);
|
||||
}
|
||||
|
||||
private static Set<Long> getRegenPassSet(ConcurrentHashMap<String, Set<Long>> store, String passKey) {
|
||||
return store.computeIfAbsent(passKey, key -> ConcurrentHashMap.newKeySet());
|
||||
}
|
||||
|
||||
private static String resolveRegenPassKey(String threadName) {
|
||||
String runtimeKey = RegenRuntime.getRunId();
|
||||
if (runtimeKey != null && !runtimeKey.isBlank()) {
|
||||
return runtimeKey;
|
||||
}
|
||||
|
||||
if (!threadName.startsWith("Iris-Regen-")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String suffix = threadName.substring("Iris-Regen-".length());
|
||||
int lastDash = suffix.lastIndexOf('-');
|
||||
if (lastDash <= 0) {
|
||||
return suffix;
|
||||
}
|
||||
return suffix.substring(0, lastDash);
|
||||
}
|
||||
|
||||
private static void touchRegenPass(String passKey) {
|
||||
long now = System.currentTimeMillis();
|
||||
REGEN_PASS_TOUCHED_MS.put(passKey, now);
|
||||
if (REGEN_PASS_TOUCHED_MS.size() <= 64) {
|
||||
return;
|
||||
}
|
||||
|
||||
Iterator<Map.Entry<String, Long>> iterator = REGEN_PASS_TOUCHED_MS.entrySet().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Map.Entry<String, Long> entry = iterator.next();
|
||||
if (now - entry.getValue() <= REGEN_PASS_CACHE_TTL_MS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String key = entry.getKey();
|
||||
iterator.remove();
|
||||
REGEN_GENERATED_CHUNKS_BY_PASS.remove(key);
|
||||
REGEN_CLEARED_CHUNKS_BY_PASS.remove(key);
|
||||
REGEN_PLANNED_CHUNKS_BY_PASS.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
||||
public void onModify(int x, int z, Hunk<BlockData> output, boolean multicore, ChunkContext context) {
|
||||
PrecisionStopwatch p = PrecisionStopwatch.start();
|
||||
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
|
||||
MantleChunk<Matter> mc = mantle.getChunk(x, z).use();
|
||||
IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State();
|
||||
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache = new Long2ObjectOpenHashMap<>(2048);
|
||||
CarveScratch scratch = SCRATCH.get();
|
||||
@@ -101,6 +100,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<BlockData> {
|
||||
}
|
||||
}
|
||||
|
||||
MantleChunk<Matter> mc = mantle.getChunk(x, z).use();
|
||||
try {
|
||||
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
|
||||
mc.iterate(MatterCavern.class, (xx, yy, zz, c) -> {
|
||||
|
||||
@@ -57,22 +57,25 @@ public class IrisDepositModifier extends EngineAssignedModifier<BlockData> {
|
||||
long seed = x * 341873128712L + z * 132897987541L;
|
||||
long mask = 0;
|
||||
MantleChunk chunk = getEngine().getMantle().getMantle().getChunk(x, z).use();
|
||||
for (IrisDepositGenerator k : getDimension().getDeposits()) {
|
||||
long finalSeed = seed * ++mask;
|
||||
burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context));
|
||||
}
|
||||
try {
|
||||
for (IrisDepositGenerator k : getDimension().getDeposits()) {
|
||||
long finalSeed = seed * ++mask;
|
||||
burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context));
|
||||
}
|
||||
|
||||
for (IrisDepositGenerator k : region.getDeposits()) {
|
||||
long finalSeed = seed * ++mask;
|
||||
burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context));
|
||||
}
|
||||
for (IrisDepositGenerator k : region.getDeposits()) {
|
||||
long finalSeed = seed * ++mask;
|
||||
burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context));
|
||||
}
|
||||
|
||||
for (IrisDepositGenerator k : biome.getDeposits()) {
|
||||
long finalSeed = seed * ++mask;
|
||||
burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context));
|
||||
for (IrisDepositGenerator k : biome.getDeposits()) {
|
||||
long finalSeed = seed * ++mask;
|
||||
burst.queue(() -> generate(k, chunk, terrain, rng.nextParallelRNG(finalSeed), x, z, false, context));
|
||||
}
|
||||
burst.complete();
|
||||
} finally {
|
||||
chunk.release();
|
||||
}
|
||||
burst.complete();
|
||||
chunk.release();
|
||||
}
|
||||
|
||||
public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<BlockData> data, RNG rng, int cx, int cz, boolean safe, ChunkContext context) {
|
||||
|
||||
@@ -53,17 +53,27 @@ public class IrisImportedStructureControl {
|
||||
@Desc("Vertical block offset applied only to UNDERGROUND vanilla structures (the UNDERGROUND_STRUCTURES and STRONGHOLDS generation steps: strongholds, trial chambers, mineshafts, ancient cities, etc.). Surface structures (villages, outposts, etc.) are never shifted. Use a negative value to push deep structures lower when your dimension's sea/terrain level differs from vanilla's (e.g. -64 if you lowered the fluid height to 0). 0 = no shift.")
|
||||
private int undergroundYShift = 0;
|
||||
|
||||
@Desc("When true (the default), ingested datapacks generate normally: a datapack that redefines a VANILLA structure key (e.g. an ancient-city datapack overriding 'minecraft:ancient_city') REPLACES the vanilla placement, structure definition, jigsaw pools and pieces exactly as the datapack intends, and the datapack's OWN new structures generate too. When false, datapack structures do NOT generate at all - Iris strips the vanilla-key overrides from the installed datapack copy so the original vanilla structures generate untouched, and holds every datapack-namespaced structure out of natural generation so it never appears over or beside the vanilla one. Datapacks stay installed and importable either way, so when false the only way to get a datapack structure is to run '/iris structure import' and place it manually from a biome/region/dimension 'structures' list. Resolved globally across every loaded pack: if ANY dimension sets this false, vanilla-key overrides are stripped for every installed datapack.")
|
||||
private boolean datapackOverrides = true;
|
||||
|
||||
public boolean active() {
|
||||
return mode == VanillaStructureMode.ALL_ON || !enabled.isEmpty();
|
||||
}
|
||||
|
||||
public boolean shouldGenerate(String key) {
|
||||
if (!datapackOverrides && isDatapackKey(key)) {
|
||||
return false;
|
||||
}
|
||||
if (mode == VanillaStructureMode.ALL_ON) {
|
||||
return !matches(disabled, key);
|
||||
}
|
||||
return matches(enabled, key);
|
||||
}
|
||||
|
||||
private static boolean isDatapackKey(String key) {
|
||||
return key != null && key.contains(":") && !key.startsWith("minecraft:");
|
||||
}
|
||||
|
||||
private boolean matches(KList<String> list, String key) {
|
||||
if (key == null) {
|
||||
return false;
|
||||
|
||||
@@ -22,7 +22,7 @@ import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListStructure;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@@ -36,8 +36,8 @@ import lombok.experimental.Accessors;
|
||||
@Data
|
||||
public class IrisStructurePlacement {
|
||||
@ArrayType(type = String.class, min = 1)
|
||||
@RegistryListResource(IrisStructure.class)
|
||||
@Desc("Iris structures to place here. Use this for imported or hand-authored structures with full block control.")
|
||||
@RegistryListStructure
|
||||
@Desc("Structures to place here. Any vanilla, datapack, or imported Iris structure key (e.g. minecraft_village_plains, minecraft_stronghold). Jigsaw component pieces are not valid here.")
|
||||
private KList<String> structures = new KList<>();
|
||||
|
||||
@Desc("How start positions are scattered.")
|
||||
|
||||
+9
-3
@@ -16,10 +16,16 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.commands;
|
||||
package art.arcane.iris.engine.object.annotations;
|
||||
|
||||
import art.arcane.iris.util.common.director.DirectorExecutor;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
public class CommandSettings implements DirectorExecutor {
|
||||
import static java.lang.annotation.ElementType.*;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Retention(RUNTIME)
|
||||
@Target({PARAMETER, TYPE, FIELD})
|
||||
public @interface RegistryListStructure {
|
||||
|
||||
}
|
||||
@@ -37,14 +37,10 @@ import art.arcane.iris.engine.object.VanillaStructureMode;
|
||||
import art.arcane.iris.engine.object.IrisWorld;
|
||||
import art.arcane.iris.engine.object.StudioMode;
|
||||
import art.arcane.iris.engine.platform.studio.StudioGenerator;
|
||||
import art.arcane.iris.util.project.matter.TileWrapper;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.iris.util.project.hunk.view.ChunkDataHunkHolder;
|
||||
import art.arcane.volmlib.util.io.ReactiveFolder;
|
||||
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
|
||||
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.scheduling.ChronoLatch;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import art.arcane.volmlib.util.scheduling.Looper;
|
||||
@@ -55,8 +51,6 @@ import lombok.Setter;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
@@ -273,289 +267,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectChunkReplacement(
|
||||
World world,
|
||||
int x,
|
||||
int z,
|
||||
Executor syncExecutor,
|
||||
ChunkReplacementOptions options,
|
||||
ChunkReplacementListener listener
|
||||
) {
|
||||
boolean acquired = false;
|
||||
ChunkReplacementOptions effectiveOptions = Objects.requireNonNull(options, "options");
|
||||
ChunkReplacementListener effectiveListener = Objects.requireNonNull(listener, "listener");
|
||||
AtomicReference<String> phaseRef = new AtomicReference<>("start");
|
||||
try {
|
||||
setChunkReplacementPhase(phaseRef, effectiveListener, "acquire-load-lock", x, z);
|
||||
long acquireStart = System.currentTimeMillis();
|
||||
while (!loadLock.tryAcquire(5, TimeUnit.SECONDS)) {
|
||||
Iris.warn("Chunk replacement waiting for load lock at " + x + "," + z
|
||||
+ " for " + (System.currentTimeMillis() - acquireStart) + "ms.");
|
||||
effectiveListener.onPhase(phaseRef.get(), x, z, System.currentTimeMillis());
|
||||
}
|
||||
acquired = true;
|
||||
long acquireWait = System.currentTimeMillis() - acquireStart;
|
||||
if (acquireWait >= 5000L) {
|
||||
Iris.warn("Chunk replacement waited " + acquireWait + "ms for load lock at " + x + "," + z + ".");
|
||||
}
|
||||
TerrainChunk tc = TerrainChunk.create(world);
|
||||
this.world.bind(world);
|
||||
|
||||
if (effectiveOptions.isFullMode()) {
|
||||
setChunkReplacementPhase(phaseRef, effectiveListener, "reset-mantle", x, z);
|
||||
resetMantleChunkForFullRegen(x, z);
|
||||
}
|
||||
|
||||
setChunkReplacementPhase(phaseRef, effectiveListener, "generate", x, z);
|
||||
long generateStart = System.currentTimeMillis();
|
||||
boolean useMulticore = false;
|
||||
AtomicBoolean generateDone = new AtomicBoolean(false);
|
||||
AtomicLong generationWatchdogStart = new AtomicLong(System.currentTimeMillis());
|
||||
Thread generateThread = Thread.currentThread();
|
||||
J.a(() -> {
|
||||
while (!generateDone.get()) {
|
||||
if (!J.sleep(5000)) {
|
||||
return;
|
||||
}
|
||||
if (generateDone.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Iris.warn("Chunk replacement still generating at " + x + "," + z
|
||||
+ " for " + (System.currentTimeMillis() - generationWatchdogStart.get()) + "ms"
|
||||
+ " thread=" + generateThread.getName()
|
||||
+ " state=" + generateThread.getState());
|
||||
effectiveListener.onPhase(phaseRef.get(), x, z, System.currentTimeMillis());
|
||||
}
|
||||
});
|
||||
try {
|
||||
getEngine().generate(x << 4, z << 4, tc, useMulticore);
|
||||
} finally {
|
||||
generateDone.set(true);
|
||||
}
|
||||
long generateTook = System.currentTimeMillis() - generateStart;
|
||||
if (generateTook >= 5000L) {
|
||||
Iris.warn("Chunk replacement terrain generation took " + generateTook + "ms at " + x + "," + z + ".");
|
||||
}
|
||||
|
||||
if (J.isFolia()) {
|
||||
setChunkReplacementPhase(phaseRef, effectiveListener, "folia-run-region", x, z);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Throwable[] failure = new Throwable[1];
|
||||
long regionScheduleStart = System.currentTimeMillis();
|
||||
if (!J.runRegion(world, x, z, () -> {
|
||||
try {
|
||||
setChunkReplacementPhase(phaseRef, effectiveListener, "apply-terrain", x, z);
|
||||
phaseUnsafeSet("folia-region-run", x, z);
|
||||
Chunk c = world.getChunkAt(x, z);
|
||||
Iris.tickets.addTicket(c);
|
||||
try {
|
||||
for (Entity ee : c.getEntities()) {
|
||||
if (ee instanceof Player) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ee.remove();
|
||||
}
|
||||
|
||||
for (int i = getEngine().getHeight() >> 4; i >= 0; i--) {
|
||||
int finalI = i << 4;
|
||||
for (int xx = 0; xx < 16; xx++) {
|
||||
for (int yy = 0; yy < 16; yy++) {
|
||||
for (int zz = 0; zz < 16; zz++) {
|
||||
if (yy + finalI >= engine.getHeight() || yy + finalI < 0) {
|
||||
continue;
|
||||
}
|
||||
int y = yy + finalI + world.getMinHeight();
|
||||
c.getBlock(xx, y, zz).setBlockData(tc.getBlockData(xx, y, zz), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (effectiveOptions.isFullMode()) {
|
||||
setChunkReplacementPhase(phaseRef, effectiveListener, "overlay", x, z);
|
||||
OverlayMetrics overlayMetrics = applyMantleOverlay(c, world, x, z);
|
||||
effectiveListener.onOverlay(x, z, overlayMetrics.appliedBlocks(), overlayMetrics.objectKeys(), System.currentTimeMillis());
|
||||
}
|
||||
|
||||
setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z);
|
||||
engine.getWorldManager().onChunkLoad(c, true);
|
||||
world.refreshChunk(c.getX(), c.getZ());
|
||||
} finally {
|
||||
Iris.tickets.removeTicket(c);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
failure[0] = e;
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
})) {
|
||||
throw new IllegalStateException("Failed to schedule region task for chunk replacement at " + x + "," + z);
|
||||
}
|
||||
long regionScheduleTook = System.currentTimeMillis() - regionScheduleStart;
|
||||
if (regionScheduleTook >= 1000L) {
|
||||
Iris.verbose("Chunk replacement region task scheduling took " + regionScheduleTook + "ms at " + x + "," + z + ".");
|
||||
}
|
||||
|
||||
long regionWaitStart = System.currentTimeMillis();
|
||||
while (!latch.await(5, TimeUnit.SECONDS)) {
|
||||
Iris.warn("Chunk replacement waiting on region task at " + x + "," + z
|
||||
+ " for " + (System.currentTimeMillis() - regionWaitStart) + "ms.");
|
||||
effectiveListener.onPhase(phaseRef.get(), x, z, System.currentTimeMillis());
|
||||
}
|
||||
long regionWaitTook = System.currentTimeMillis() - regionWaitStart;
|
||||
if (regionWaitTook >= 5000L) {
|
||||
Iris.warn("Chunk replacement region task completed after " + regionWaitTook + "ms at " + x + "," + z + ".");
|
||||
}
|
||||
if (failure[0] != null) {
|
||||
effectiveListener.onFailurePhase(phaseRef.get(), x, z, failure[0], System.currentTimeMillis());
|
||||
throw failure[0];
|
||||
}
|
||||
} else {
|
||||
setChunkReplacementPhase(phaseRef, effectiveListener, "paperlib-async-load", x, z);
|
||||
long loadChunkStart = System.currentTimeMillis();
|
||||
Chunk c = PaperLib.getChunkAtAsync(world, x, z).get();
|
||||
long loadChunkTook = System.currentTimeMillis() - loadChunkStart;
|
||||
if (loadChunkTook >= 5000L) {
|
||||
Iris.warn("Chunk replacement chunk load took " + loadChunkTook + "ms at " + x + "," + z + ".");
|
||||
}
|
||||
|
||||
setChunkReplacementPhase(phaseRef, effectiveListener, "apply-terrain", x, z);
|
||||
Iris.tickets.addTicket(c);
|
||||
try {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
for (Entity ee : c.getEntities()) {
|
||||
if (ee instanceof Player) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ee.remove();
|
||||
}
|
||||
}, syncExecutor).get();
|
||||
|
||||
KList<CompletableFuture<?>> futures = new KList<>(1 + getEngine().getHeight() >> 4);
|
||||
for (int i = getEngine().getHeight() >> 4; i >= 0; i--) {
|
||||
int finalI = i << 4;
|
||||
futures.add(CompletableFuture.runAsync(() -> {
|
||||
for (int xx = 0; xx < 16; xx++) {
|
||||
for (int yy = 0; yy < 16; yy++) {
|
||||
for (int zz = 0; zz < 16; zz++) {
|
||||
if (yy + finalI >= engine.getHeight() || yy + finalI < 0) {
|
||||
continue;
|
||||
}
|
||||
int y = yy + finalI + world.getMinHeight();
|
||||
c.getBlock(xx, y, zz).setBlockData(tc.getBlockData(xx, y, zz), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, syncExecutor));
|
||||
}
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get();
|
||||
if (effectiveOptions.isFullMode()) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
setChunkReplacementPhase(phaseRef, effectiveListener, "overlay", x, z);
|
||||
OverlayMetrics overlayMetrics = applyMantleOverlay(c, world, x, z);
|
||||
effectiveListener.onOverlay(x, z, overlayMetrics.appliedBlocks(), overlayMetrics.objectKeys(), System.currentTimeMillis());
|
||||
}, syncExecutor).get();
|
||||
}
|
||||
CompletableFuture.runAsync(() -> {
|
||||
setChunkReplacementPhase(phaseRef, effectiveListener, "chunk-load-callback", x, z);
|
||||
engine.getWorldManager().onChunkLoad(c, true);
|
||||
world.refreshChunk(c.getX(), c.getZ());
|
||||
}, syncExecutor).get();
|
||||
} finally {
|
||||
Iris.tickets.removeTicket(c);
|
||||
}
|
||||
}
|
||||
|
||||
Iris.debug("Regenerated " + x + " " + z);
|
||||
} catch (Throwable e) {
|
||||
effectiveListener.onFailurePhase(phaseRef.get(), x, z, e, System.currentTimeMillis());
|
||||
Iris.error("======================================");
|
||||
Iris.error("Chunk replacement failed at phase=" + phaseRef.get() + " chunk=" + x + "," + z);
|
||||
e.printStackTrace();
|
||||
Iris.reportErrorChunk(x, z, e, "CHUNK");
|
||||
Iris.error("======================================");
|
||||
throw new IllegalStateException("Chunk replacement failed at phase=" + phaseRef.get() + " chunk=" + x + "," + z, e);
|
||||
} finally {
|
||||
if (acquired) {
|
||||
loadLock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void phaseUnsafeSet(String phase, int x, int z) {
|
||||
Iris.verbose("Chunk replacement phase=" + phase + " chunk=" + x + "," + z);
|
||||
}
|
||||
|
||||
private static void setChunkReplacementPhase(
|
||||
AtomicReference<String> phaseRef,
|
||||
ChunkReplacementListener listener,
|
||||
String phase,
|
||||
int x,
|
||||
int z
|
||||
) {
|
||||
phaseRef.set(phase);
|
||||
listener.onPhase(phase, x, z, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
private void resetMantleChunkForFullRegen(int chunkX, int chunkZ) {
|
||||
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle().getChunk(chunkX, chunkZ).use();
|
||||
try {
|
||||
mantleChunk.deleteSlices(BlockData.class);
|
||||
mantleChunk.deleteSlices(String.class);
|
||||
mantleChunk.deleteSlices(TileWrapper.class);
|
||||
mantleChunk.flag(MantleFlag.PLANNED, false);
|
||||
mantleChunk.flag(MantleFlag.OBJECT, false);
|
||||
mantleChunk.flag(MantleFlag.REAL, false);
|
||||
} finally {
|
||||
mantleChunk.release();
|
||||
}
|
||||
}
|
||||
|
||||
private OverlayMetrics applyMantleOverlay(Chunk chunk, World world, int chunkX, int chunkZ) {
|
||||
int minWorldY = world.getMinHeight();
|
||||
int maxWorldY = world.getMaxHeight();
|
||||
AtomicInteger appliedBlocks = new AtomicInteger();
|
||||
AtomicInteger objectKeys = new AtomicInteger();
|
||||
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle().getChunk(chunkX, chunkZ).use();
|
||||
try {
|
||||
mantleChunk.iterate(String.class, (x, y, z, value) -> {
|
||||
if (value != null && !value.isEmpty() && value.indexOf('@') > 0) {
|
||||
objectKeys.incrementAndGet();
|
||||
}
|
||||
});
|
||||
mantleChunk.iterate(BlockData.class, (x, y, z, blockData) -> {
|
||||
if (!shouldApplyMantleOverlayBlock(blockData)) {
|
||||
return;
|
||||
}
|
||||
int worldY = y + minWorldY;
|
||||
if (worldY < minWorldY || worldY >= maxWorldY) {
|
||||
return;
|
||||
}
|
||||
chunk.getBlock(x & 15, worldY, z & 15).setBlockData(blockData, false);
|
||||
appliedBlocks.incrementAndGet();
|
||||
});
|
||||
} finally {
|
||||
mantleChunk.release();
|
||||
}
|
||||
return new OverlayMetrics(appliedBlocks.get(), objectKeys.get());
|
||||
}
|
||||
|
||||
static boolean shouldApplyMantleOverlayBlock(BlockData blockData) {
|
||||
if (blockData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Material material = blockData.getMaterial();
|
||||
return material != null && material != Material.AIR && material != Material.CAVE_AIR && material != Material.VOID_AIR;
|
||||
}
|
||||
|
||||
private record OverlayMetrics(int appliedBlocks, int objectKeys) {
|
||||
}
|
||||
|
||||
private Engine getEngine(WorldInfo world) {
|
||||
if (setup.get()) {
|
||||
return getEngine();
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.engine.platform;
|
||||
|
||||
public interface ChunkReplacementListener {
|
||||
ChunkReplacementListener NO_OP = new ChunkReplacementListener() {
|
||||
};
|
||||
|
||||
default void onPhase(String phase, int chunkX, int chunkZ, long timestampMs) {
|
||||
}
|
||||
|
||||
default void onOverlay(int chunkX, int chunkZ, int appliedBlocks, int objectKeys, long timestampMs) {
|
||||
}
|
||||
|
||||
default void onFailurePhase(String phase, int chunkX, int chunkZ, Throwable error, long timestampMs) {
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.engine.platform;
|
||||
|
||||
public final class ChunkReplacementOptions {
|
||||
private final String runId;
|
||||
private final boolean fullMode;
|
||||
private final boolean diagnostics;
|
||||
|
||||
private ChunkReplacementOptions(String runId, boolean fullMode, boolean diagnostics) {
|
||||
this.runId = runId == null ? "unknown" : runId;
|
||||
this.fullMode = fullMode;
|
||||
this.diagnostics = diagnostics;
|
||||
}
|
||||
|
||||
public static ChunkReplacementOptions terrain(String runId, boolean diagnostics) {
|
||||
return new ChunkReplacementOptions(runId, false, diagnostics);
|
||||
}
|
||||
|
||||
public static ChunkReplacementOptions full(String runId, boolean diagnostics) {
|
||||
return new ChunkReplacementOptions(runId, true, diagnostics);
|
||||
}
|
||||
|
||||
public String runId() {
|
||||
return runId;
|
||||
}
|
||||
|
||||
public boolean isFullMode() {
|
||||
return fullMode;
|
||||
}
|
||||
|
||||
public boolean diagnostics() {
|
||||
return diagnostics;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
|
||||
@Nullable
|
||||
@@ -43,15 +42,6 @@ public interface PlatformChunkGenerator extends Hotloadable, DataProvider {
|
||||
@NotNull
|
||||
EngineTarget getTarget();
|
||||
|
||||
void injectChunkReplacement(
|
||||
World world,
|
||||
int x,
|
||||
int z,
|
||||
Executor syncExecutor,
|
||||
ChunkReplacementOptions options,
|
||||
ChunkReplacementListener listener
|
||||
);
|
||||
|
||||
void close();
|
||||
|
||||
default CompletableFuture<Void> closeAsync() {
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.util.common.director;
|
||||
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public final class DirectorHelp {
|
||||
private DirectorHelp() {
|
||||
}
|
||||
|
||||
public static void print(VolmitSender sender, Class<?> commandRoot) {
|
||||
Director rootAnnotation = commandRoot.getAnnotation(Director.class);
|
||||
String rootName = rootAnnotation == null || rootAnnotation.name().isEmpty()
|
||||
? lowercaseDefault(commandRoot.getSimpleName())
|
||||
: rootAnnotation.name();
|
||||
String rootDesc = rootAnnotation == null ? "" : rootAnnotation.description();
|
||||
|
||||
sender.sendMessage(C.IRIS + "/" + rootName + C.GRAY + " — " + rootDesc);
|
||||
|
||||
List<Method> methods = new ArrayList<>();
|
||||
for (Method m : commandRoot.getDeclaredMethods()) {
|
||||
if (m.isAnnotationPresent(Director.class)) {
|
||||
methods.add(m);
|
||||
}
|
||||
}
|
||||
methods.sort(Comparator.comparing(m -> methodName(m)));
|
||||
|
||||
for (Method m : methods) {
|
||||
Director d = m.getAnnotation(Director.class);
|
||||
String name = methodName(m);
|
||||
String aliases = formatAliases(d.aliases());
|
||||
sender.sendMessage(C.WHITE + " " + name + aliases + C.GRAY + " — " + d.description());
|
||||
for (Parameter p : m.getParameters()) {
|
||||
Param pa = p.getAnnotation(Param.class);
|
||||
if (pa == null) continue;
|
||||
String key = pa.name().isEmpty() ? p.getName() : pa.name();
|
||||
String type = simpleTypeName(p.getType());
|
||||
String def = pa.defaultValue().isEmpty() ? "" : C.GRAY + " (default: " + pa.defaultValue() + ")";
|
||||
String pAliases = formatAliases(pa.aliases());
|
||||
sender.sendMessage(C.GRAY + " " + C.AQUA + key + "=" + C.YELLOW + "<" + type + ">"
|
||||
+ C.GRAY + pAliases + C.GRAY + " — " + pa.description() + def);
|
||||
}
|
||||
}
|
||||
|
||||
List<Field> subGroups = new ArrayList<>();
|
||||
for (Field f : commandRoot.getDeclaredFields()) {
|
||||
if (f.getType().isAnnotationPresent(Director.class)) {
|
||||
subGroups.add(f);
|
||||
}
|
||||
}
|
||||
if (!subGroups.isEmpty()) {
|
||||
sender.sendMessage(C.IRIS + " Subcommand groups:");
|
||||
for (Field f : subGroups) {
|
||||
Director sub = f.getType().getAnnotation(Director.class);
|
||||
String subName = sub.name().isEmpty() ? lowercaseDefault(f.getType().getSimpleName()) : sub.name();
|
||||
sender.sendMessage(C.WHITE + " /" + rootName + " " + subName
|
||||
+ C.GRAY + " — " + sub.description() + C.GRAY + " (try: /" + rootName + " " + subName + " help)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String methodName(Method m) {
|
||||
Director d = m.getAnnotation(Director.class);
|
||||
if (d != null && !d.name().isEmpty()) return d.name();
|
||||
return m.getName();
|
||||
}
|
||||
|
||||
private static String formatAliases(String[] aliases) {
|
||||
if (aliases == null || aliases.length == 0) return "";
|
||||
List<String> valid = new ArrayList<>();
|
||||
for (String a : aliases) {
|
||||
if (a != null && !a.isEmpty()) valid.add(a);
|
||||
}
|
||||
if (valid.isEmpty()) return "";
|
||||
return C.GRAY + " [" + String.join(", ", valid) + "]";
|
||||
}
|
||||
|
||||
private static String simpleTypeName(Class<?> type) {
|
||||
if (type.isEnum()) {
|
||||
Object[] constants = type.getEnumConstants();
|
||||
List<String> names = new ArrayList<>();
|
||||
for (Object c : constants) names.add(((Enum<?>) c).name());
|
||||
return String.join("|", names);
|
||||
}
|
||||
return type.getSimpleName();
|
||||
}
|
||||
|
||||
private static String lowercaseDefault(String simpleName) {
|
||||
String s = simpleName.startsWith("Command") ? simpleName.substring("Command".length()) : simpleName;
|
||||
return s.isEmpty() ? simpleName.toLowerCase() : s.toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package art.arcane.iris.util.common.misc;
|
||||
|
||||
public final class RegenRuntime {
|
||||
private static final ThreadLocal<String> RUN_ID = new ThreadLocal<>();
|
||||
|
||||
private RegenRuntime() {
|
||||
}
|
||||
|
||||
public static void setRunId(String runId) {
|
||||
RUN_ID.set(runId);
|
||||
}
|
||||
|
||||
public static String getRunId() {
|
||||
return RUN_ID.get();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
RUN_ID.remove();
|
||||
}
|
||||
}
|
||||
-7
@@ -4,8 +4,6 @@ import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.INMSBinding;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.platform.ChunkReplacementListener;
|
||||
import art.arcane.iris.engine.platform.ChunkReplacementOptions;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
@@ -15,7 +13,6 @@ import org.junit.Test;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -108,10 +105,6 @@ public class WorldLifecycleRuntimeLevelStemTest {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectChunkReplacement(World world, int x, int z, Executor syncExecutor, ChunkReplacementOptions options, ChunkReplacementListener listener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.structure;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class BulkStructureImporterTemplateNameTest {
|
||||
@Test
|
||||
public void minecraftKeysDropTheNamespacePrefix() {
|
||||
assertEquals("village/plains", BulkStructureImporter.templateNameFor("minecraft:village/plains"));
|
||||
assertEquals("igloo/top", BulkStructureImporter.templateNameFor("minecraft:igloo/top"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void datapackKeysKeepNamespaceAsDirectory() {
|
||||
assertEquals("nova_structures/temple/large", BulkStructureImporter.templateNameFor("nova_structures:temple/large"));
|
||||
assertEquals("aquaculture/treasure", BulkStructureImporter.templateNameFor("aquaculture:treasure"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void keyWithoutNamespaceDefaultsToMinecraft() {
|
||||
assertEquals("village", BulkStructureImporter.templateNameFor("village"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pathIsLowercasedAndSanitized() {
|
||||
assertEquals("nova/foo_bar", BulkStructureImporter.templateNameFor("nova:Foo Bar"));
|
||||
assertEquals("nova/weird_name", BulkStructureImporter.templateNameFor("nova:weird@name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void duplicateSlashesAreCollapsedAndLeadingSlashStripped() {
|
||||
assertEquals("nova/a/b", BulkStructureImporter.templateNameFor("nova:a//b"));
|
||||
assertEquals("nova/leading", BulkStructureImporter.templateNameFor("nova:/leading"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.structure;
|
||||
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class StructureImporterModeTest {
|
||||
@Test
|
||||
public void parseModeDefaultsToOverwriteForNull() {
|
||||
assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseModeDefaultsToOverwriteForUnknownAndEmpty() {
|
||||
assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode(""));
|
||||
assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode("garbage"));
|
||||
assertEquals(StructureImporter.Mode.OVERWRITE, StructureImporter.parseMode("overwrite"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseModeRecognizesAllAddOnlyAliases() {
|
||||
assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode("add"));
|
||||
assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode("addonly"));
|
||||
assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode("add_only"));
|
||||
assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode("add-only"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void parseModeIsCaseInsensitive() {
|
||||
assertEquals(StructureImporter.Mode.ADD_ONLY, StructureImporter.parseMode("ADD"));
|
||||
assertEquals(StructureImporter.Mode.MERGE, StructureImporter.parseMode("MERGE"));
|
||||
assertEquals(StructureImporter.Mode.MERGE, StructureImporter.parseMode("merge"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deriveNameFromStringLowercasesAndNormalizesSeparators() {
|
||||
assertEquals("minecraft_village_plains", StructureImporter.deriveName("minecraft:village/plains"));
|
||||
assertEquals("nova_structures_temple", StructureImporter.deriveName("Nova_Structures:Temple"));
|
||||
assertEquals("minecraft_ancient_city", StructureImporter.deriveName("minecraft:ancient_city"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deriveNameFromNamespacedKeyJoinsNamespaceAndPath() {
|
||||
assertEquals("minecraft_village", StructureImporter.deriveName(NamespacedKey.minecraft("village")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deriveNameFromNamespacedKeyNormalizesSlashesInPath() {
|
||||
NamespacedKey key = NamespacedKey.fromString("nova_structures:temple/large");
|
||||
assertEquals("nova_structures_temple_large", StructureImporter.deriveName(key));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.core.structure;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class StructureSchemaKeysTest {
|
||||
@Test
|
||||
public void vanillaKeysAreNormalizedToIrisLoadKeyForm() {
|
||||
KList<String> result = StructureSchemaKeys.collect(
|
||||
Arrays.asList("minecraft:village_plains", "minecraft:stronghold", "minecraft:ancient_city"),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
assertTrue(result.contains("minecraft_village_plains"));
|
||||
assertTrue(result.contains("minecraft_stronghold"));
|
||||
assertTrue(result.contains("minecraft_ancient_city"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void strongholdIsAlwaysOfferedEvenWithoutAnImportedFile() {
|
||||
KList<String> result = StructureSchemaKeys.collect(
|
||||
Arrays.asList("minecraft:stronghold", "minecraft:fortress", "minecraft:monument"),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
assertTrue(result.contains("minecraft_stronghold"));
|
||||
assertTrue(result.contains("minecraft_fortress"));
|
||||
assertTrue(result.contains("minecraft_monument"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void datapackStructuresAreNormalizedAndIncluded() {
|
||||
KList<String> result = StructureSchemaKeys.collect(
|
||||
Arrays.asList("nova_structures:temple", "Nova_Structures:Big/Castle"),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
assertTrue(result.contains("nova_structures_temple"));
|
||||
assertTrue(result.contains("nova_structures_big_castle"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void jigsawComponentPiecesAreExcluded() {
|
||||
List<String> pieces = Arrays.asList(
|
||||
"village/taiga/houses/taiga_small_house_5",
|
||||
"bastion/units/rampart_plates/plate_0",
|
||||
"village/common/animals/cat_black",
|
||||
"trial_chambers/corridor/straight_1",
|
||||
"end_city/third_floor_1",
|
||||
"igloo/middle");
|
||||
|
||||
KList<String> result = StructureSchemaKeys.collect(
|
||||
Arrays.asList("minecraft:village_taiga", "minecraft:bastion_remnant"),
|
||||
pieces,
|
||||
pieces);
|
||||
|
||||
for (String piece : pieces) {
|
||||
assertFalse("component piece must not be a valid structure key: " + piece, result.contains(piece));
|
||||
}
|
||||
assertTrue(result.contains("minecraft_village_taiga"));
|
||||
assertTrue(result.contains("minecraft_bastion_remnant"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void importedCustomStructuresAreKept() {
|
||||
KList<String> result = StructureSchemaKeys.collect(
|
||||
Collections.emptyList(),
|
||||
Arrays.asList("mypack_castle", "custom/cool_tower"),
|
||||
Collections.emptyList());
|
||||
|
||||
assertTrue(result.contains("mypack_castle"));
|
||||
assertTrue(result.contains("custom/cool_tower"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void importedStructureThatCollidesWithAPieceIsDropped() {
|
||||
KList<String> result = StructureSchemaKeys.collect(
|
||||
Collections.emptyList(),
|
||||
Arrays.asList("village/taiga/houses/taiga_small_house_5", "minecraft_village_plains"),
|
||||
Collections.singletonList("village/taiga/houses/taiga_small_house_5"));
|
||||
|
||||
assertFalse(result.contains("village/taiga/houses/taiga_small_house_5"));
|
||||
assertTrue(result.contains("minecraft_village_plains"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void vanillaAndImportedEquivalentsDeduplicate() {
|
||||
KList<String> result = StructureSchemaKeys.collect(
|
||||
Collections.singletonList("minecraft:ancient_city"),
|
||||
Collections.singletonList("minecraft_ancient_city"),
|
||||
Collections.emptyList());
|
||||
|
||||
int occurrences = 0;
|
||||
for (String key : result) {
|
||||
if (key.equals("minecraft_ancient_city")) {
|
||||
occurrences++;
|
||||
}
|
||||
}
|
||||
assertEquals(1, occurrences);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resultIsSorted() {
|
||||
KList<String> result = StructureSchemaKeys.collect(
|
||||
Arrays.asList("minecraft:zombie_village", "minecraft:ancient_city", "minecraft:monument"),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
KList<String> sorted = new KList<>(result);
|
||||
Collections.sort(sorted);
|
||||
assertEquals(sorted, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullCollectionsAreHandled() {
|
||||
KList<String> result = StructureSchemaKeys.collect(null, null, null);
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void blankAndNullEntriesAreIgnored() {
|
||||
KList<String> result = StructureSchemaKeys.collect(
|
||||
Arrays.asList("minecraft:village_plains", "", null, " "),
|
||||
Arrays.asList("custom_one", "", null),
|
||||
Arrays.asList("", null));
|
||||
|
||||
assertTrue(result.contains("minecraft_village_plains"));
|
||||
assertTrue(result.contains("custom_one"));
|
||||
assertEquals(2, result.size());
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.engine.framework;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class IrisStructureLocatorContractTest {
|
||||
@Test
|
||||
public void placedKeysIsEmptyForNullEngine() {
|
||||
assertTrue(IrisStructureLocator.placedKeys(null).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isPlacedIsFalseForNullEngine() {
|
||||
assertFalse(IrisStructureLocator.isPlaced(null, "minecraft:ancient_city"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isPlacedIsFalseForNullOrEmptyKey() {
|
||||
Engine engine = mock(Engine.class);
|
||||
assertFalse(IrisStructureLocator.isPlaced(engine, null));
|
||||
assertFalse(IrisStructureLocator.isPlaced(engine, ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void suppressesVanillaIsFalseForNullEngine() {
|
||||
assertFalse(IrisStructureLocator.suppressesVanilla(null, "minecraft:ancient_city"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void suppressesVanillaIsFalseForNullOrEmptyKey() {
|
||||
Engine engine = mock(Engine.class);
|
||||
assertFalse(IrisStructureLocator.suppressesVanilla(engine, null));
|
||||
assertFalse(IrisStructureLocator.suppressesVanilla(engine, ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void startsInChunkIsFalseForNullEngine() {
|
||||
assertFalse(IrisStructureLocator.startsInChunk(null, "minecraft:ancient_city", 0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void locateReturnsNullForNullEngine() {
|
||||
assertNull(IrisStructureLocator.locate(null, "minecraft:village_taiga", 0, 0, 100));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void locateReturnsNullForNullOrEmptyKey() {
|
||||
Engine engine = mock(Engine.class);
|
||||
assertNull(IrisStructureLocator.locate(engine, null, 0, 0, 100));
|
||||
assertNull(IrisStructureLocator.locate(engine, "", 0, 0, 100));
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.engine.framework;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class StructureReachabilityContractTest {
|
||||
@Test
|
||||
public void reachableKeysIsEmptyForNullEngine() {
|
||||
assertTrue(StructureReachability.reachableKeys(null).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reachableKeysIsEmptyWhenEngineHasNoData() {
|
||||
Engine engine = mock(Engine.class);
|
||||
assertTrue(StructureReachability.reachableKeys(engine).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isReachableIsFalseForNullEngine() {
|
||||
assertFalse(StructureReachability.isReachable(null, "minecraft:village_taiga"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isReachableIsFalseForNullOrEmptyKey() {
|
||||
Engine engine = mock(Engine.class);
|
||||
assertFalse(StructureReachability.isReachable(engine, null));
|
||||
assertFalse(StructureReachability.isReachable(engine, ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isReachableIsFalseWhenNothingIsReachable() {
|
||||
Engine engine = mock(Engine.class);
|
||||
assertFalse(StructureReachability.isReachable(engine, "minecraft:village_taiga"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingBiomeKeysIsEmptyForNullEngine() {
|
||||
assertTrue(StructureReachability.missingBiomeKeys(null, "minecraft:village_taiga").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingBiomeKeysIsEmptyForNullKey() {
|
||||
Engine engine = mock(Engine.class);
|
||||
assertTrue(StructureReachability.missingBiomeKeys(engine, null).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingBiomeKeysIsEmptyWhenWorldUnavailable() {
|
||||
Engine engine = mock(Engine.class);
|
||||
assertTrue(StructureReachability.missingBiomeKeys(engine, "minecraft:village_taiga").isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidateIsNullSafe() {
|
||||
StructureReachability.invalidate(null);
|
||||
StructureReachability.invalidate(mock(Engine.class));
|
||||
}
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 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.engine.object;
|
||||
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class IrisImportedStructureControlTest {
|
||||
private static KList<String> keys(String... values) {
|
||||
KList<String> list = new KList<>();
|
||||
for (String value : values) {
|
||||
list.add(value);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultModeGeneratesEveryVanillaStructure() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl();
|
||||
assertTrue(control.shouldGenerate("minecraft:village_plains"));
|
||||
assertTrue(control.shouldGenerate("minecraft:stronghold"));
|
||||
assertTrue(control.shouldGenerate("minecraft:ocean_monument"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultModeGeneratesDatapackStructures() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl();
|
||||
assertTrue(control.isDatapackOverrides());
|
||||
assertTrue(control.shouldGenerate("nova_structures:desert_temple"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledBlacklistMatchesExactKey() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setDisabled(keys("minecraft:stronghold"));
|
||||
assertFalse(control.shouldGenerate("minecraft:stronghold"));
|
||||
assertTrue(control.shouldGenerate("minecraft:village_plains"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledBlacklistMatchesNamespacePrefix() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setDisabled(keys("minecraft:village"));
|
||||
assertFalse(control.shouldGenerate("minecraft:village_plains"));
|
||||
assertFalse(control.shouldGenerate("minecraft:village_taiga"));
|
||||
assertFalse(control.shouldGenerate("minecraft:village_snowy"));
|
||||
assertTrue(control.shouldGenerate("minecraft:stronghold"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allOffWhitelistMatchesExactKey() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setMode(VanillaStructureMode.ALL_OFF)
|
||||
.setEnabled(keys("minecraft:village_plains"));
|
||||
assertTrue(control.shouldGenerate("minecraft:village_plains"));
|
||||
assertFalse(control.shouldGenerate("minecraft:village_taiga"));
|
||||
assertFalse(control.shouldGenerate("minecraft:stronghold"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allOffWhitelistMatchesNamespacePrefix() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setMode(VanillaStructureMode.ALL_OFF)
|
||||
.setEnabled(keys("minecraft:village"));
|
||||
assertTrue(control.shouldGenerate("minecraft:village_taiga"));
|
||||
assertTrue(control.shouldGenerate("minecraft:village_desert"));
|
||||
assertFalse(control.shouldGenerate("minecraft:stronghold"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customModeBehavesLikeWhitelist() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setMode(VanillaStructureMode.CUSTOM)
|
||||
.setEnabled(keys("minecraft:village_plains"));
|
||||
assertTrue(control.shouldGenerate("minecraft:village_plains"));
|
||||
assertFalse(control.shouldGenerate("minecraft:stronghold"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void datapackOverridesFalseBlocksDatapackKeysWhileAllOn() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setDatapackOverrides(false);
|
||||
assertFalse(control.shouldGenerate("nova_structures:desert_temple"));
|
||||
assertFalse(control.shouldGenerate("aquaculture:treasure_vault"));
|
||||
assertTrue(control.shouldGenerate("minecraft:village_plains"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void datapackOverridesFalseBlocksDatapackKeyEvenWhenWhitelisted() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setMode(VanillaStructureMode.ALL_OFF)
|
||||
.setEnabled(keys("nova_structures:desert_temple", "minecraft:village_plains"))
|
||||
.setDatapackOverrides(false);
|
||||
assertFalse(control.shouldGenerate("nova_structures:desert_temple"));
|
||||
assertTrue(control.shouldGenerate("minecraft:village_plains"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void datapackOverridesTrueAllowsDatapackKeysWhileAllOn() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setDatapackOverrides(true);
|
||||
assertTrue(control.shouldGenerate("nova_structures:desert_temple"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void keyWithoutNamespaceIsNotTreatedAsDatapack() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setDatapackOverrides(false);
|
||||
assertTrue(control.shouldGenerate("village_plains"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void minecraftKeyIsNeverTreatedAsDatapack() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setDatapackOverrides(false);
|
||||
assertTrue(control.shouldGenerate("minecraft:ancient_city"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activeTrueWhenAllOn() {
|
||||
assertTrue(new IrisImportedStructureControl().active());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activeFalseWhenAllOffWithoutWhitelist() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setMode(VanillaStructureMode.ALL_OFF);
|
||||
assertFalse(control.active());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activeTrueWhenAllOffWithWhitelist() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setMode(VanillaStructureMode.ALL_OFF)
|
||||
.setEnabled(keys("minecraft:village_plains"));
|
||||
assertTrue(control.active());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activeFalseWhenCustomWithoutWhitelist() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setMode(VanillaStructureMode.CUSTOM);
|
||||
assertFalse(control.active());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void emptyOrNullListEntriesNeverMatchEveryKey() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setDisabled(keys("", null));
|
||||
assertTrue(control.shouldGenerate("minecraft:village_plains"));
|
||||
assertTrue(control.shouldGenerate("minecraft:stronghold"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullKeyIsNullSafeAndNotWhitelisted() {
|
||||
IrisImportedStructureControl control = new IrisImportedStructureControl()
|
||||
.setMode(VanillaStructureMode.ALL_OFF)
|
||||
.setEnabled(keys("minecraft:village_plains"));
|
||||
assertFalse(control.shouldGenerate(null));
|
||||
}
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
package art.arcane.iris.engine.platform;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class BukkitChunkGeneratorOverlayPolicyTest {
|
||||
@Test
|
||||
public void skipsAirOverlayBlocks() {
|
||||
BlockData air = mock(BlockData.class);
|
||||
doReturn(Material.AIR).when(air).getMaterial();
|
||||
|
||||
assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(air));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipsCaveAirOverlayBlocks() {
|
||||
BlockData air = mock(BlockData.class);
|
||||
doReturn(Material.CAVE_AIR).when(air).getMaterial();
|
||||
|
||||
assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(air));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipsVoidAirOverlayBlocks() {
|
||||
BlockData air = mock(BlockData.class);
|
||||
doReturn(Material.VOID_AIR).when(air).getMaterial();
|
||||
|
||||
assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(air));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appliesSolidOverlayBlocks() {
|
||||
BlockData stone = mock(BlockData.class);
|
||||
doReturn(Material.STONE).when(stone).getMaterial();
|
||||
|
||||
assertTrue(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(stone));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipsNullOverlayBlocks() {
|
||||
assertFalse(BukkitChunkGenerator.shouldApplyMantleOverlayBlock(null));
|
||||
}
|
||||
}
|
||||
+96
-26
@@ -42,6 +42,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
private final KMap<String, Holder<Biome>> customBiomes;
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
|
||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||
this.engine = engine;
|
||||
@@ -173,13 +174,33 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
|
||||
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
|
||||
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
structureBiomeCache.clear();
|
||||
}
|
||||
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveNoiseBiomeHolder(x, y, z);
|
||||
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
|
||||
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
@@ -192,15 +213,66 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveNoiseBiomeHolder(int x, int y, int z) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
if (engine.getComplex() == null) {
|
||||
if (resolution.irisBiome.isCustom()) {
|
||||
return resolveCustomHolder(resolution);
|
||||
}
|
||||
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, resolution.irisBiome.getVanillaDerivative());
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
if (resolution.irisBiome.isCustom()) {
|
||||
return resolveCustomHolder(resolution);
|
||||
}
|
||||
|
||||
org.bukkit.block.Biome vanillaBiome = resolution.underground
|
||||
? resolution.irisBiome.getGroundBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ)
|
||||
: resolution.irisBiome.getSkyBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
if (customBiome != null) {
|
||||
Holder<Biome> holder = customBiomes.get(customBiome.getId());
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (engine.getComplex() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
int blockY = y << 2;
|
||||
@@ -218,7 +290,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
return getFallbackBiome();
|
||||
return null;
|
||||
}
|
||||
|
||||
RNG noiseRng = new RNG(seed
|
||||
@@ -226,27 +298,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
^ (((long) blockY) * 132897987541L)
|
||||
^ (((long) blockZ) * 42317861L));
|
||||
|
||||
if (irisBiome.isCustom()) {
|
||||
IrisBiomeCustom customBiome = irisBiome.getCustomBiome(noiseRng, blockX, blockY, blockZ);
|
||||
if (customBiome != null) {
|
||||
Holder<Biome> holder = customBiomes.get(customBiome.getId());
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
org.bukkit.block.Biome vanillaBiome = underground
|
||||
? irisBiome.getGroundBiome(noiseRng, blockX, blockY, blockZ)
|
||||
: irisBiome.getSkyBiome(noiseRng, blockX, blockY, blockZ);
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
|
||||
}
|
||||
|
||||
private Holder<Biome> getFallbackBiome() {
|
||||
@@ -328,4 +380,22 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class BiomeResolution {
|
||||
private final IrisBiome irisBiome;
|
||||
private final boolean underground;
|
||||
private final int blockX;
|
||||
private final int blockY;
|
||||
private final int blockZ;
|
||||
private final RNG rng;
|
||||
|
||||
private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) {
|
||||
this.irisBiome = irisBiome;
|
||||
this.underground = underground;
|
||||
this.blockX = blockX;
|
||||
this.blockY = blockY;
|
||||
this.blockZ = blockZ;
|
||||
this.rng = rng;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-3
@@ -50,11 +50,18 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
|
||||
private final ChunkGenerator delegate;
|
||||
private final Engine engine;
|
||||
private final CustomBiomeSource customBiomeSource;
|
||||
private volatile Set<String> reachableStructureKeysCache;
|
||||
|
||||
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
|
||||
super(((CraftWorld) world).getHandle(), edit(delegate, new CustomBiomeSource(seed, engine, world)), null);
|
||||
this(delegate, engine, world, new CustomBiomeSource(seed, engine, world));
|
||||
}
|
||||
|
||||
private IrisChunkGenerator(ChunkGenerator delegate, Engine engine, World world, CustomBiomeSource customBiomeSource) {
|
||||
super(((CraftWorld) world).getHandle(), edit(delegate, customBiomeSource), null);
|
||||
this.delegate = delegate;
|
||||
this.engine = engine;
|
||||
this.customBiomeSource = customBiomeSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,13 +98,40 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (!importedControl().active()) {
|
||||
return null;
|
||||
}
|
||||
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
|
||||
if (reachable == null || reachable.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored);
|
||||
return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
|
||||
Set<String> reachable = reachableStructureKeysCache;
|
||||
if (reachable == null) {
|
||||
reachable = VanillaStructureBiomes.reachableStructureKeys(level, delegate.getBiomeSource());
|
||||
reachableStructureKeysCache = reachable;
|
||||
}
|
||||
if (reachable.isEmpty()) {
|
||||
return holders;
|
||||
}
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<Holder<Structure>> kept = new ArrayList<>();
|
||||
for (Holder<Structure> holder : holders) {
|
||||
Object id = registry.getKey(holder.value());
|
||||
if (id != null && reachable.contains(id.toString())) {
|
||||
kept.add(holder);
|
||||
}
|
||||
}
|
||||
if (kept.size() == holders.size()) {
|
||||
return holders;
|
||||
}
|
||||
return HolderSet.direct(kept);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return MapCodec.unit(null);
|
||||
@@ -144,7 +178,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
return delegate.createBiomes(randomstate, blender, structuremanager, ichunkaccess);
|
||||
ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler());
|
||||
return CompletableFuture.completedFuture(ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -44,6 +44,7 @@ import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
@@ -89,9 +90,12 @@ import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
|
||||
public class NMSBinding implements INMSBinding {
|
||||
private final KMap<Biome, Object> baseBiomeCache = new KMap<>();
|
||||
@@ -412,6 +416,43 @@ public class NMSBinding implements INMSBinding {
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getReachableStructureKeys(World world) {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
|
||||
keys.addAll(VanillaStructureBiomes.reachableStructureKeys(level, source));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getStructureBiomeKeys(String structureKey) {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
keys.addAll(VanillaStructureBiomes.structureBiomeKeys(registry(), structureKey));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getPossibleBiomeKeys(World world) {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
|
||||
keys.addAll(VanillaStructureBiomes.possibleBiomeKeys(source));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBukkit() {
|
||||
return true;
|
||||
@@ -555,6 +596,42 @@ public class NMSBinding implements INMSBinding {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean purgeChunk(World world, int x, int z) {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
try {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Runnable unload = () -> {
|
||||
try {
|
||||
if (world.isChunkLoaded(x, z)) {
|
||||
world.unloadChunk(x, z, false);
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
};
|
||||
if (!J.runRegion(world, x, z, unload)) {
|
||||
return false;
|
||||
}
|
||||
if (!latch.await(15, TimeUnit.SECONDS)) {
|
||||
Iris.warn("Chunk purge timed out waiting to unload " + x + "," + z + " in " + world.getName() + ".");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (MoonriseRegionFileIO.RegionFileType type : MoonriseRegionFileIO.RegionFileType.values()) {
|
||||
MoonriseRegionFileIO.scheduleSave(level, x, z, null, type);
|
||||
}
|
||||
MoonriseRegionFileIO.flush(level);
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException {
|
||||
if (customNbt != null && !customNbt.isEmpty()) {
|
||||
net.minecraft.world.item.ItemStack s = CraftItemStack.asNMSCopy(itemStack);
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package art.arcane.iris.core.nms.v1_21_R7;
|
||||
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
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.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
final class VanillaStructureBiomes {
|
||||
private VanillaStructureBiomes() {
|
||||
}
|
||||
|
||||
static Set<String> possibleBiomeKeys(BiomeSource source) {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
if (source == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Holder<Biome> holder : source.possibleBiomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
if (access == null || structureKey == null || structureKey.isEmpty()) {
|
||||
return keys;
|
||||
}
|
||||
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
|
||||
Structure structure = registry.getValue(Identifier.parse(structureKey));
|
||||
if (structure == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Holder<Biome> holder : structure.biomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
|
||||
Set<String> reachable = new LinkedHashSet<>();
|
||||
if (level == null || source == null) {
|
||||
return reachable;
|
||||
}
|
||||
Set<String> possible = possibleBiomeKeys(source);
|
||||
if (possible.isEmpty()) {
|
||||
return reachable;
|
||||
}
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
|
||||
for (Holder<Biome> holder : entry.getValue().biomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent() && possible.contains(key.get().identifier().toString())) {
|
||||
reachable.add(entry.getKey().identifier().toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return reachable;
|
||||
}
|
||||
}
|
||||
+96
-26
@@ -42,6 +42,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
private final KMap<String, Holder<Biome>> customBiomes;
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> structureBiomeCache = new ConcurrentHashMap<>();
|
||||
|
||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||
this.engine = engine;
|
||||
@@ -173,13 +174,33 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = structureBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveStructureBiomeHolder(x, y, z);
|
||||
Holder<Biome> existingHolder = structureBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
|
||||
if (structureBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
structureBiomeCache.clear();
|
||||
}
|
||||
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
public Holder<Biome> getVisibleNoiseBiome(int x, int y, int z, Climate.Sampler sampler) {
|
||||
long cacheKey = packNoiseKey(x, y, z);
|
||||
Holder<Biome> cachedHolder = noiseBiomeCache.get(cacheKey);
|
||||
if (cachedHolder != null) {
|
||||
return cachedHolder;
|
||||
}
|
||||
|
||||
Holder<Biome> resolvedHolder = resolveNoiseBiomeHolder(x, y, z);
|
||||
Holder<Biome> resolvedHolder = resolveVisibleBiomeHolder(x, y, z);
|
||||
Holder<Biome> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
@@ -192,15 +213,66 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveNoiseBiomeHolder(int x, int y, int z) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
private Holder<Biome> resolveStructureBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
if (engine.getComplex() == null) {
|
||||
if (resolution.irisBiome.isCustom()) {
|
||||
return resolveCustomHolder(resolution);
|
||||
}
|
||||
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, resolution.irisBiome.getVanillaDerivative());
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveVisibleBiomeHolder(int x, int y, int z) {
|
||||
BiomeResolution resolution = resolveBiomeResolution(x, y, z);
|
||||
if (resolution == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
if (resolution.irisBiome.isCustom()) {
|
||||
return resolveCustomHolder(resolution);
|
||||
}
|
||||
|
||||
org.bukkit.block.Biome vanillaBiome = resolution.underground
|
||||
? resolution.irisBiome.getGroundBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ)
|
||||
: resolution.irisBiome.getSkyBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveCustomHolder(BiomeResolution resolution) {
|
||||
IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, resolution.blockX, resolution.blockY, resolution.blockZ);
|
||||
if (customBiome != null) {
|
||||
Holder<Biome> holder = customBiomes.get(customBiome.getId());
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
private BiomeResolution resolveBiomeResolution(int x, int y, int z) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (engine.getComplex() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
int blockY = y << 2;
|
||||
@@ -218,7 +290,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
return getFallbackBiome();
|
||||
return null;
|
||||
}
|
||||
|
||||
RNG noiseRng = new RNG(seed
|
||||
@@ -226,27 +298,7 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
^ (((long) blockY) * 132897987541L)
|
||||
^ (((long) blockZ) * 42317861L));
|
||||
|
||||
if (irisBiome.isCustom()) {
|
||||
IrisBiomeCustom customBiome = irisBiome.getCustomBiome(noiseRng, blockX, blockY, blockZ);
|
||||
if (customBiome != null) {
|
||||
Holder<Biome> holder = customBiomes.get(customBiome.getId());
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
org.bukkit.block.Biome vanillaBiome = underground
|
||||
? irisBiome.getGroundBiome(noiseRng, blockX, blockY, blockZ)
|
||||
: irisBiome.getSkyBiome(noiseRng, blockX, blockY, blockZ);
|
||||
Holder<Biome> holder = NMSBinding.biomeToBiomeBase(biomeRegistry, vanillaBiome);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
return getFallbackBiome();
|
||||
return new BiomeResolution(irisBiome, underground, blockX, blockY, blockZ, noiseRng);
|
||||
}
|
||||
|
||||
private Holder<Biome> getFallbackBiome() {
|
||||
@@ -328,4 +380,22 @@ public class CustomBiomeSource extends BiomeSource {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static final class BiomeResolution {
|
||||
private final IrisBiome irisBiome;
|
||||
private final boolean underground;
|
||||
private final int blockX;
|
||||
private final int blockY;
|
||||
private final int blockZ;
|
||||
private final RNG rng;
|
||||
|
||||
private BiomeResolution(IrisBiome irisBiome, boolean underground, int blockX, int blockY, int blockZ, RNG rng) {
|
||||
this.irisBiome = irisBiome;
|
||||
this.underground = underground;
|
||||
this.blockX = blockX;
|
||||
this.blockY = blockY;
|
||||
this.blockZ = blockZ;
|
||||
this.rng = rng;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-3
@@ -50,11 +50,18 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
|
||||
private final ChunkGenerator delegate;
|
||||
private final Engine engine;
|
||||
private final CustomBiomeSource customBiomeSource;
|
||||
private volatile Set<String> reachableStructureKeysCache;
|
||||
|
||||
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
|
||||
super(((CraftWorld) world).getHandle(), edit(delegate, new CustomBiomeSource(seed, engine, world)), null);
|
||||
this(delegate, engine, world, new CustomBiomeSource(seed, engine, world));
|
||||
}
|
||||
|
||||
private IrisChunkGenerator(ChunkGenerator delegate, Engine engine, World world, CustomBiomeSource customBiomeSource) {
|
||||
super(((CraftWorld) world).getHandle(), edit(delegate, customBiomeSource), null);
|
||||
this.delegate = delegate;
|
||||
this.engine = engine;
|
||||
this.customBiomeSource = customBiomeSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,13 +98,40 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
if (!importedControl().active()) {
|
||||
return null;
|
||||
}
|
||||
HolderSet<Structure> reachable = filterReachableStructures(level, holders);
|
||||
if (reachable == null || reachable.size() == 0) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored);
|
||||
return delegate.findNearestMapStructure(level, reachable, pos, radius, findUnexplored);
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private HolderSet<Structure> filterReachableStructures(ServerLevel level, HolderSet<Structure> holders) {
|
||||
Set<String> reachable = reachableStructureKeysCache;
|
||||
if (reachable == null) {
|
||||
reachable = VanillaStructureBiomes.reachableStructureKeys(level, delegate.getBiomeSource());
|
||||
reachableStructureKeysCache = reachable;
|
||||
}
|
||||
if (reachable.isEmpty()) {
|
||||
return holders;
|
||||
}
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
List<Holder<Structure>> kept = new ArrayList<>();
|
||||
for (Holder<Structure> holder : holders) {
|
||||
Object id = registry.getKey(holder.value());
|
||||
if (id != null && reachable.contains(id.toString())) {
|
||||
kept.add(holder);
|
||||
}
|
||||
}
|
||||
if (kept.size() == holders.size()) {
|
||||
return holders;
|
||||
}
|
||||
return HolderSet.direct(kept);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return MapCodec.unit(null);
|
||||
@@ -144,7 +178,8 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
return delegate.createBiomes(randomstate, blender, structuremanager, ichunkaccess);
|
||||
ichunkaccess.fillBiomesFromNoise(customBiomeSource::getVisibleNoiseBiome, randomstate.sampler());
|
||||
return CompletableFuture.completedFuture(ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -44,6 +44,7 @@ import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.entity.EntityType;
|
||||
import net.minecraft.world.item.component.CustomData;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
@@ -89,9 +90,12 @@ import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
|
||||
public class NMSBinding implements INMSBinding {
|
||||
private final KMap<Biome, Object> baseBiomeCache = new KMap<>();
|
||||
@@ -412,6 +416,43 @@ public class NMSBinding implements INMSBinding {
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getReachableStructureKeys(World world) {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
|
||||
keys.addAll(VanillaStructureBiomes.reachableStructureKeys(level, source));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getStructureBiomeKeys(String structureKey) {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
keys.addAll(VanillaStructureBiomes.structureBiomeKeys(registry(), structureKey));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getPossibleBiomeKeys(World world) {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
BiomeSource source = level.getChunkSource().getGenerator().getBiomeSource();
|
||||
keys.addAll(VanillaStructureBiomes.possibleBiomeKeys(source));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBukkit() {
|
||||
return true;
|
||||
@@ -555,6 +596,42 @@ public class NMSBinding implements INMSBinding {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean purgeChunk(World world, int x, int z) {
|
||||
ServerLevel level = ((CraftWorld) world).getHandle();
|
||||
try {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Runnable unload = () -> {
|
||||
try {
|
||||
if (world.isChunkLoaded(x, z)) {
|
||||
world.unloadChunk(x, z, false);
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
};
|
||||
if (!J.runRegion(world, x, z, unload)) {
|
||||
return false;
|
||||
}
|
||||
if (!latch.await(15, TimeUnit.SECONDS)) {
|
||||
Iris.warn("Chunk purge timed out waiting to unload " + x + "," + z + " in " + world.getName() + ".");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (MoonriseRegionFileIO.RegionFileType type : MoonriseRegionFileIO.RegionFileType.values()) {
|
||||
MoonriseRegionFileIO.scheduleSave(level, x, z, null, type);
|
||||
}
|
||||
MoonriseRegionFileIO.flush(level);
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException {
|
||||
if (customNbt != null && !customNbt.isEmpty()) {
|
||||
net.minecraft.world.item.ItemStack s = CraftItemStack.asNMSCopy(itemStack);
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package art.arcane.iris.core.nms.v26_1_R1;
|
||||
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
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.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
final class VanillaStructureBiomes {
|
||||
private VanillaStructureBiomes() {
|
||||
}
|
||||
|
||||
static Set<String> possibleBiomeKeys(BiomeSource source) {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
if (source == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Holder<Biome> holder : source.possibleBiomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static Set<String> structureBiomeKeys(RegistryAccess access, String structureKey) {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
if (access == null || structureKey == null || structureKey.isEmpty()) {
|
||||
return keys;
|
||||
}
|
||||
Registry<Structure> registry = access.lookupOrThrow(Registries.STRUCTURE);
|
||||
Structure structure = registry.getValue(Identifier.parse(structureKey));
|
||||
if (structure == null) {
|
||||
return keys;
|
||||
}
|
||||
for (Holder<Biome> holder : structure.biomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent()) {
|
||||
keys.add(key.get().identifier().toString());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
static Set<String> reachableStructureKeys(ServerLevel level, BiomeSource source) {
|
||||
Set<String> reachable = new LinkedHashSet<>();
|
||||
if (level == null || source == null) {
|
||||
return reachable;
|
||||
}
|
||||
Set<String> possible = possibleBiomeKeys(source);
|
||||
if (possible.isEmpty()) {
|
||||
return reachable;
|
||||
}
|
||||
Registry<Structure> registry = level.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
for (Map.Entry<ResourceKey<Structure>, Structure> entry : registry.entrySet()) {
|
||||
for (Holder<Biome> holder : entry.getValue().biomes()) {
|
||||
Optional<ResourceKey<Biome>> key = holder.unwrapKey();
|
||||
if (key.isPresent() && possible.contains(key.get().identifier().toString())) {
|
||||
reachable.add(entry.getKey().identifier().toString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return reachable;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user