mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-07-12 18:04:01 +00:00
redesign for Structures
This commit is contained in:
@@ -68,6 +68,7 @@ registerCustomOutputTaskUnix('CrazyDev22LT', '/home/julian/Desktop/server/plugin
|
||||
|
||||
def nmsBindings = [
|
||||
v1_21_R7: '1.21.11-R0.1-SNAPSHOT',
|
||||
v26_1_R1: '26.1.2.build.66-stable',
|
||||
]
|
||||
Class nmsTypeClass = Class.forName('NMSBinding$Type')
|
||||
nmsBindings.each { key, value ->
|
||||
|
||||
@@ -21,5 +21,5 @@ repositories {
|
||||
dependencies {
|
||||
implementation('org.ow2.asm:asm:9.8')
|
||||
implementation('com.github.VolmitSoftware:NMSTools:c88961416f')
|
||||
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.18')
|
||||
implementation('io.papermc.paperweight:paperweight-userdev:2.0.0-beta.21')
|
||||
}
|
||||
|
||||
@@ -139,7 +139,10 @@ public class NMSBinding implements Plugin<Project> {
|
||||
}
|
||||
|
||||
String[] parts = trimmed.split("\\.");
|
||||
return new int[]{Integer.parseInt(parts[1]), Integer.parseInt(parts[2])};
|
||||
if ("1".equals(parts[0])) {
|
||||
return new int[]{Integer.parseInt(parts[1]), Integer.parseInt(parts[2])};
|
||||
}
|
||||
return new int[]{Integer.parseInt(parts[0]), Integer.parseInt(parts[1])};
|
||||
}
|
||||
|
||||
private static <T extends Named> T named(ObjectFactory objects, Class<T> type, String name) {
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ plugins {
|
||||
alias(libs.plugins.kotlin.lombok)
|
||||
}
|
||||
|
||||
def apiVersion = '1.21'
|
||||
def apiVersion = '26.1'
|
||||
def mainClass = 'art.arcane.iris.Iris'
|
||||
def lib = 'art.arcane.iris.util'
|
||||
String volmLibCoordinate = providers.gradleProperty('volmLibCoordinate')
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
1435163759
|
||||
446774718
|
||||
@@ -97,6 +97,7 @@ public class CommandIris implements DirectorExecutor {
|
||||
private CommandPregen pregen;
|
||||
private CommandSettings settings;
|
||||
private CommandObject object;
|
||||
private CommandStructure structure;
|
||||
private CommandWhat what;
|
||||
private CommandEdit edit;
|
||||
private CommandDeveloper developer;
|
||||
@@ -592,11 +593,8 @@ public class CommandIris implements DirectorExecutor {
|
||||
}
|
||||
|
||||
@Director(description = "Toggle debug")
|
||||
public void debug(
|
||||
@Param(name = "on", description = "Whether or not debug should be on", defaultValue = "other")
|
||||
Boolean on
|
||||
) {
|
||||
boolean to = on == null ? !IrisSettings.get().getGeneral().isDebug() : on;
|
||||
public void debug() {
|
||||
boolean to = !IrisSettings.get().getGeneral().isDebug();
|
||||
IrisSettings.get().getGeneral().setDebug(to);
|
||||
IrisSettings.get().forceSave();
|
||||
sender().sendMessage(C.GREEN + "Set debug to: " + to);
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* 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.commands;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.structure.StructureImporter;
|
||||
import art.arcane.iris.core.structure.StructureIndexService;
|
||||
import art.arcane.iris.core.structure.VillageImporter;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||
import art.arcane.iris.engine.framework.StructureAssembler;
|
||||
import art.arcane.iris.engine.framework.StructurePlacementGrid;
|
||||
import art.arcane.iris.engine.object.IObjectPlacer;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
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.engine.data.cache.Cache;
|
||||
import art.arcane.volmlib.util.director.DirectorOrigin;
|
||||
import art.arcane.volmlib.util.director.annotations.Director;
|
||||
import art.arcane.volmlib.util.director.annotations.Param;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Director(name = "structure", aliases = {"struct", "str"}, studio = true, 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)
|
||||
public void list(
|
||||
@Param(description = "The dimension whose pack to index", aliases = "dim")
|
||||
IrisDimension dimension
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
File file = StructureIndexService.write(data);
|
||||
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(
|
||||
@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")
|
||||
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")
|
||||
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 = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH)
|
||||
public void info(
|
||||
@Param(description = "The dimension whose pack holds the structure", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "The iris structure key to inspect")
|
||||
String structure
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
IrisStructure s = IrisData.loadAnyStructure(structure, data);
|
||||
if (s == null) {
|
||||
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
|
||||
return;
|
||||
}
|
||||
StructureAssembler assembler = new StructureAssembler(data, s, 0, 64, 0);
|
||||
KList<PlacedStructurePiece> pieces = assembler.assemble(new RNG(1234));
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces (check startPool '" + s.getStartPool() + "')");
|
||||
return;
|
||||
}
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
minX = Math.min(minX, p.getMinX());
|
||||
minZ = Math.min(minZ, p.getMinZ());
|
||||
maxX = Math.max(maxX, p.getMaxX());
|
||||
maxZ = Math.max(maxZ, p.getMaxZ());
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Structure '" + structure + "': " + C.WHITE + pieces.size() + C.GREEN + " pieces, footprint " + C.WHITE + (maxX - minX + 1) + "x" + (maxZ - minZ + 1) + C.GREEN + " blocks (sample seed 1234)");
|
||||
}
|
||||
|
||||
@Director(description = "Assemble and place an iris structure at your location (studio testing)", aliases = {"p"}, origin = DirectorOrigin.PLAYER, sync = true)
|
||||
public void place(
|
||||
@Param(description = "The dimension whose pack holds the structure", aliases = "dim")
|
||||
IrisDimension dimension,
|
||||
@Param(description = "The iris structure key to place")
|
||||
String structure
|
||||
) {
|
||||
IrisData data = dimension.getLoader();
|
||||
if (data == null) {
|
||||
sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
|
||||
return;
|
||||
}
|
||||
IrisStructure s = IrisData.loadAnyStructure(structure, data);
|
||||
if (s == null) {
|
||||
sender().sendMessage(C.RED + "No iris structure '" + structure + "' in this pack");
|
||||
return;
|
||||
}
|
||||
Location loc = player().getLocation();
|
||||
StructureAssembler assembler = new StructureAssembler(data, s, loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
|
||||
RNG rng = new RNG((long) loc.getBlockX() * 341873128712L + loc.getBlockZ());
|
||||
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
sender().sendMessage(C.RED + "Structure '" + structure + "' assembled 0 pieces");
|
||||
return;
|
||||
}
|
||||
Map<Block, BlockData> future = new HashMap<>();
|
||||
IObjectPlacer placer = CommandObject.createPlacer(player().getWorld(), future);
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
IrisObjectPlacement config = new IrisObjectPlacement();
|
||||
config.setMode(ObjectPlaceMode.STRUCTURE_PIECE);
|
||||
config.setRotation(p.getRotation());
|
||||
config.getPlace().add(p.getObject().getLoadKey());
|
||||
if (!s.getEdit().isEmpty()) {
|
||||
config.setEdit(s.getEdit());
|
||||
}
|
||||
p.getObject().place(p.getX(), p.getY(), p.getZ(), placer, config, rng, null, null, data);
|
||||
}
|
||||
sender().sendMessage(C.GREEN + "Placed '" + structure + "' (" + pieces.size() + " pieces) at your location.");
|
||||
}
|
||||
|
||||
@Director(description = "Find the nearest placement of an iris structure around you", aliases = {"l"}, origin = DirectorOrigin.PLAYER)
|
||||
public void locate(
|
||||
@Param(description = "The iris structure key to find")
|
||||
String structure,
|
||||
@Param(description = "Search radius in chunks", defaultValue = "96")
|
||||
int radius
|
||||
) {
|
||||
World world = player().getWorld();
|
||||
if (!IrisToolbelt.isIrisWorld(world)) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world");
|
||||
return;
|
||||
}
|
||||
Engine engine = IrisToolbelt.access(world).getEngine();
|
||||
IrisData data = engine.getData();
|
||||
long seed = engine.getSeedManager().getMantle();
|
||||
int pcx = player().getLocation().getBlockX() >> 4;
|
||||
int pcz = player().getLocation().getBlockZ() >> 4;
|
||||
int max = Math.max(1, Math.min(radius, 512));
|
||||
for (int r = 0; r <= max; r++) {
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
for (int dz = -r; dz <= r; dz++) {
|
||||
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) {
|
||||
continue;
|
||||
}
|
||||
int cx = pcx + dx;
|
||||
int cz = pcz + dz;
|
||||
if (chunkStarts(engine, seed, structure, cx, cz)) {
|
||||
sender().sendMessage(C.GREEN + "Nearest '" + structure + "' starts near chunk " + cx + ", " + cz + " (block " + (cx << 4) + ", " + (cz << 4) + ")");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sender().sendMessage(C.YELLOW + "No '" + structure + "' placement found within " + max + " chunks.");
|
||||
}
|
||||
|
||||
private boolean chunkStarts(Engine engine, long seed, String structure, int cx, int cz) {
|
||||
int bx = 8 + (cx << 4);
|
||||
int bz = 8 + (cz << 4);
|
||||
IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(bx, bz);
|
||||
IrisRegion region = engine.getComplex().getRegionStream().get(bx, bz);
|
||||
KList<IrisStructurePlacement> placements = new KList<>();
|
||||
if (biome != null) {
|
||||
placements.addAll(biome.getStructures());
|
||||
}
|
||||
if (region != null) {
|
||||
placements.addAll(region.getStructures());
|
||||
}
|
||||
placements.addAll(engine.getDimension().getStructures());
|
||||
RNG rng = new RNG(Cache.key(cx, cz) + seed);
|
||||
for (IrisStructurePlacement placement : placements) {
|
||||
if (!placement.getStructures().contains(structure)) {
|
||||
continue;
|
||||
}
|
||||
if (StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, rng)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -294,12 +294,6 @@ public class CommandStudio implements DirectorExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
@Director(description = "Get all structures in a radius of chunks", aliases = "dist", origin = DirectorOrigin.PLAYER)
|
||||
public void distances(@Param(description = "The radius in chunks") int radius) {
|
||||
sender().sendMessage(C.YELLOW + "Structure distance sampling for legacy structure data has been removed.");
|
||||
}
|
||||
|
||||
|
||||
@Director(description = "Render a world map (External GUI)", aliases = "render")
|
||||
public void map(
|
||||
@Param(name = "world", description = "The world to open the generator for", contextual = true)
|
||||
@@ -616,7 +610,7 @@ public class CommandStudio implements DirectorExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
@Director(aliases = "find-objects", description = "Get information about nearby structures")
|
||||
@Director(aliases = "find-objects", description = "Capture an IGenData chunk report for nearby chunks")
|
||||
public void objects() {
|
||||
if (!IrisToolbelt.isIrisWorld(player().getWorld())) {
|
||||
sender().sendMessage(C.RED + "You must be in an Iris world");
|
||||
|
||||
@@ -19,9 +19,13 @@
|
||||
package art.arcane.iris.core.edit;
|
||||
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.BlockPosition;
|
||||
import art.arcane.volmlib.util.math.M;
|
||||
@@ -32,7 +36,15 @@ import lombok.Data;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.Block;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
@Data
|
||||
@@ -111,6 +123,8 @@ public class DustRevealer {
|
||||
Engine access = generator.getEngine();
|
||||
|
||||
if (access != null) {
|
||||
describe(access, world, block, sender);
|
||||
|
||||
String a = access.getObjectPlacementKey(block.getX(), block.getY() - block.getWorld().getMinHeight(), block.getZ());
|
||||
if (a != null) {
|
||||
world.playSound(block.getLocation(), Sound.ITEM_LODESTONE_COMPASS_LOCK, 1f, 0.1f);
|
||||
@@ -123,6 +137,144 @@ public class DustRevealer {
|
||||
}
|
||||
}
|
||||
|
||||
private static void describe(Engine engine, World world, Block block, VolmitSender sender) {
|
||||
int x = block.getX();
|
||||
int y = block.getY();
|
||||
int z = block.getZ();
|
||||
int minHeight = world.getMinHeight();
|
||||
int relativeY = y - minHeight;
|
||||
int surfaceRelative = engine.getHeight(x, z, true);
|
||||
int surfaceY = surfaceRelative + minHeight;
|
||||
int offset = y - surfaceY;
|
||||
|
||||
String objectKey = safe(() -> engine.getObjectPlacementKey(x, relativeY, z));
|
||||
IrisBiome surfaceBiome = safe(() -> engine.getSurfaceBiome(x, z));
|
||||
IrisBiome biomeHere = safe(() -> engine.getBiome(x, relativeY, z));
|
||||
IrisBiome caveBiome = safe(() -> engine.getCaveOrMantleBiome(x, relativeY, z));
|
||||
IrisRegion region = safe(() -> engine.getRegion(x, z));
|
||||
|
||||
KList<String> lines = new KList<>();
|
||||
lines.add("Iris Dust @ " + x + ", " + y + ", " + z);
|
||||
lines.add("Block: " + block.getType().name());
|
||||
if (offset > 0) {
|
||||
lines.add("Position: +" + offset + " ABOVE surface (surface Y=" + surfaceY + ")");
|
||||
} else if (offset < 0) {
|
||||
lines.add("Position: " + (-offset) + " below surface (surface Y=" + surfaceY + ")");
|
||||
} else {
|
||||
lines.add("Position: at surface (Y=" + surfaceY + ")");
|
||||
}
|
||||
|
||||
String placedBy;
|
||||
if (offset > 0) {
|
||||
placedBy = objectKey != null ? "object/stilt '" + objectKey + "' (above surface)" : "decoration/object/stilt (above surface)";
|
||||
} else if (objectKey != null) {
|
||||
placedBy = "buried object '" + objectKey + "'";
|
||||
} else {
|
||||
placedBy = "terrain layer (depth " + Math.max(0, surfaceRelative - relativeY) + " below surface)";
|
||||
}
|
||||
lines.add("Placed by: " + placedBy);
|
||||
lines.add("Object @block: " + (objectKey == null ? "none" : objectKey));
|
||||
if (objectKey == null) {
|
||||
String columnObject = findColumnObject(engine, x, relativeY, z, minHeight);
|
||||
lines.add("Column object: " + (columnObject == null
|
||||
? "none within 64 (decorator or terrain, NOT an object stilt)"
|
||||
: columnObject + " -> this block is likely that object's stilt"));
|
||||
}
|
||||
|
||||
if (surfaceBiome != null) {
|
||||
lines.add("Surface biome: " + surfaceBiome.getLoadKey() + " (" + biomeKey(surfaceBiome.getDerivative()) + ")");
|
||||
}
|
||||
if (biomeHere != null && (surfaceBiome == null || !biomeHere.getLoadKey().equals(surfaceBiome.getLoadKey()))) {
|
||||
lines.add("Biome @Y: " + biomeHere.getLoadKey());
|
||||
}
|
||||
if (caveBiome != null && (surfaceBiome == null || !caveBiome.getLoadKey().equals(surfaceBiome.getLoadKey()))) {
|
||||
lines.add("Cave/Mantle biome: " + caveBiome.getLoadKey());
|
||||
}
|
||||
|
||||
try {
|
||||
lines.add("Server biome: " + INMS.get().getTrueBiomeBaseKey(block.getLocation())
|
||||
+ " (ID: " + INMS.get().getTrueBiomeBaseId(INMS.get().getTrueBiomeBase(block.getLocation())) + ")");
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
|
||||
if (region != null) {
|
||||
lines.add("Region: " + region.getLoadKey() + " (" + region.getName() + ")");
|
||||
}
|
||||
|
||||
Set<String> objects = safe(() -> engine.getObjectsAt(x >> 4, z >> 4));
|
||||
if (objects != null && !objects.isEmpty()) {
|
||||
lines.add("Objects in chunk: " + objects);
|
||||
}
|
||||
|
||||
sender.sendMessage(C.IRIS + "--- " + lines.get(0) + " ---");
|
||||
for (int i = 1; i < lines.size(); i++) {
|
||||
String line = lines.get(i);
|
||||
String color = (line.startsWith("Position:") || line.startsWith("Placed by:")) ? C.YELLOW.toString() : C.WHITE.toString();
|
||||
sender.sendMessage(color + line);
|
||||
}
|
||||
sendCopyButton(sender, String.join("\n", lines));
|
||||
}
|
||||
|
||||
private static String findColumnObject(Engine engine, int x, int relativeY, int z, int minHeight) {
|
||||
for (int dy = 1; dy <= 64; dy++) {
|
||||
if (relativeY + dy >= 0) {
|
||||
try {
|
||||
String up = engine.getObjectPlacementKey(x, relativeY + dy, z);
|
||||
if (up != null) {
|
||||
return up + " @Y=" + (relativeY + dy + minHeight) + " (above)";
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
if (relativeY - dy >= 0) {
|
||||
try {
|
||||
String down = engine.getObjectPlacementKey(x, relativeY - dy, z);
|
||||
if (down != null) {
|
||||
return down + " @Y=" + (relativeY - dy + minHeight) + " (below)";
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String biomeKey(Biome biome) {
|
||||
if (biome == null) {
|
||||
return "none";
|
||||
}
|
||||
try {
|
||||
return biome.getKey().getKey();
|
||||
} catch (Throwable ignored) {
|
||||
return biome.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> T safe(Supplier<T> supplier) {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendCopyButton(VolmitSender sender, String payload) {
|
||||
if (!sender.isPlayer()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Component button = Component.text("[Click to copy these stats]")
|
||||
.color(NamedTextColor.GREEN)
|
||||
.clickEvent(ClickEvent.copyToClipboard(payload))
|
||||
.hoverEvent(HoverEvent.showText(Component.text("Copy block stats to clipboard")));
|
||||
Iris.audiences.player(sender.player()).sendMessage(button);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean is(BlockPosition a) {
|
||||
if (a.getY() < world.getMinHeight() || a.getY() >= world.getMaxHeight()) {
|
||||
return false;
|
||||
|
||||
@@ -7,11 +7,13 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class WorldLifecycleStaging {
|
||||
private static final Map<String, ChunkGenerator> stagedGenerators = new ConcurrentHashMap<>();
|
||||
private static final Map<String, BiomeProvider> stagedBiomeProviders = new ConcurrentHashMap<>();
|
||||
private static final Map<String, ChunkGenerator> stagedStemGenerators = new ConcurrentHashMap<>();
|
||||
private static final AtomicReference<ChunkGenerator> pendingStemGenerator = new AtomicReference<>();
|
||||
|
||||
private WorldLifecycleStaging() {
|
||||
}
|
||||
@@ -27,6 +29,7 @@ public final class WorldLifecycleStaging {
|
||||
|
||||
public static void stageStemGenerator(@NotNull String worldName, @NotNull ChunkGenerator generator) {
|
||||
stagedStemGenerators.put(worldName, generator);
|
||||
pendingStemGenerator.set(generator);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -41,7 +44,16 @@ public final class WorldLifecycleStaging {
|
||||
|
||||
@Nullable
|
||||
public static ChunkGenerator consumeStemGenerator(@NotNull String worldName) {
|
||||
return stagedStemGenerators.remove(worldName);
|
||||
ChunkGenerator generator = stagedStemGenerators.remove(worldName);
|
||||
if (generator != null) {
|
||||
pendingStemGenerator.compareAndSet(generator, null);
|
||||
return generator;
|
||||
}
|
||||
ChunkGenerator pending = pendingStemGenerator.getAndSet(null);
|
||||
if (pending != null) {
|
||||
stagedStemGenerators.values().remove(pending);
|
||||
}
|
||||
return pending;
|
||||
}
|
||||
|
||||
public static void clearGenerator(@NotNull String worldName) {
|
||||
@@ -50,7 +62,10 @@ public final class WorldLifecycleStaging {
|
||||
}
|
||||
|
||||
public static void clearStem(@NotNull String worldName) {
|
||||
stagedStemGenerators.remove(worldName);
|
||||
ChunkGenerator generator = stagedStemGenerators.remove(worldName);
|
||||
if (generator != null) {
|
||||
pendingStemGenerator.compareAndSet(generator, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearAll(@NotNull String worldName) {
|
||||
|
||||
@@ -70,6 +70,9 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
private ResourceLoader<IrisMatterObject> matterLoader;
|
||||
private ResourceLoader<IrisImage> imageLoader;
|
||||
private ResourceLoader<IrisMatterObject> matterObjectLoader;
|
||||
private ResourceLoader<IrisStructure> structureLoader;
|
||||
private ResourceLoader<IrisJigsawPool> jigsawPoolLoader;
|
||||
private ResourceLoader<IrisJigsawPiece> jigsawPieceLoader;
|
||||
private KMap<String, KList<String>> possibleSnippets;
|
||||
private Gson gson;
|
||||
private Gson snippetLoader;
|
||||
@@ -167,6 +170,18 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
return loadAny(IrisGenerator.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisStructure loadAnyStructure(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisStructure.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisJigsawPool loadAnyJigsawPool(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisJigsawPool.class, key, nearest);
|
||||
}
|
||||
|
||||
public static IrisJigsawPiece loadAnyJigsawPiece(String key, @Nullable IrisData nearest) {
|
||||
return loadAny(IrisJigsawPiece.class, key, nearest);
|
||||
}
|
||||
|
||||
public static <T extends IrisRegistrant> T loadAny(Class<T> type, String key, @Nullable IrisData nearest) {
|
||||
try {
|
||||
if (nearest != null) {
|
||||
@@ -297,6 +312,9 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
this.objectLoader = registerLoader(IrisObject.class);
|
||||
this.imageLoader = registerLoader(IrisImage.class);
|
||||
this.matterObjectLoader = registerLoader(IrisMatterObject.class);
|
||||
this.structureLoader = registerLoader(IrisStructure.class);
|
||||
this.jigsawPoolLoader = registerLoader(IrisJigsawPool.class);
|
||||
this.jigsawPieceLoader = registerLoader(IrisJigsawPiece.class);
|
||||
builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter);
|
||||
|
||||
gson = builder.create();
|
||||
|
||||
@@ -114,7 +114,7 @@ public class ResourceLoader<T extends IrisRegistrant> implements MeteredCache {
|
||||
o.put("url", "./.iris/schema/" + getFolderName() + "-schema.json");
|
||||
File a = new File(getManager().getDataFolder(), ".iris/schema/" + getFolderName() + "-schema.json");
|
||||
String schemaPath = a.getAbsolutePath();
|
||||
if (!a.exists() && schemaBuildQueue.add(schemaPath)) {
|
||||
if (schemaBuildQueue.add(schemaPath)) {
|
||||
schemaBuildExecutor.execute(() -> {
|
||||
try {
|
||||
IO.writeAll(a, new SchemaBuilder(objectClass, manager).construct().toString(4));
|
||||
|
||||
@@ -30,9 +30,10 @@ import java.util.Set;
|
||||
public class INMS {
|
||||
private static final Version CURRENT = Boolean.getBoolean("iris.no-version-limit") ?
|
||||
new Version(Integer.MAX_VALUE, Integer.MAX_VALUE, null) :
|
||||
new Version(21, 11, null);
|
||||
new Version(26, 1, null);
|
||||
|
||||
private static final List<Version> REVISION = List.of(
|
||||
new Version(26, 1, "v26_1_R1"),
|
||||
new Version(21, 11, "v1_21_R7")
|
||||
);
|
||||
|
||||
|
||||
@@ -89,6 +89,14 @@ public interface INMSBinding {
|
||||
|
||||
KList<Biome> getBiomes();
|
||||
|
||||
default KList<String> getStructureKeys() {
|
||||
return new KList<>();
|
||||
}
|
||||
|
||||
default KList<String> getStructureSetKeys() {
|
||||
return new KList<>();
|
||||
}
|
||||
|
||||
boolean isBukkit();
|
||||
|
||||
int getBiomeId(Biome biome);
|
||||
|
||||
@@ -79,13 +79,24 @@ final class MinecraftVersion {
|
||||
}
|
||||
|
||||
String[] parts = input.split("\\.");
|
||||
if (parts.length < 2 || !"1".equals(parts[0])) {
|
||||
if (parts.length < 2 || parts.length > 3) {
|
||||
return null;
|
||||
}
|
||||
for (String part : parts) {
|
||||
if (part.isEmpty() || !part.chars().allMatch(Character::isDigit)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
int major = Integer.parseInt(parts[1]);
|
||||
int minor = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
|
||||
if ("1".equals(parts[0])) {
|
||||
int major = Integer.parseInt(parts[1]);
|
||||
int minor = parts.length > 2 ? Integer.parseInt(parts[2]) : 0;
|
||||
return new MinecraftVersion(input, major, minor);
|
||||
}
|
||||
|
||||
int major = Integer.parseInt(parts[0]);
|
||||
int minor = Integer.parseInt(parts[1]);
|
||||
return new MinecraftVersion(input, major, minor);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
|
||||
@@ -19,7 +19,8 @@ public enum DataVersion {
|
||||
V1_19_2("1.19.2", 10, DataFixerV1192::new),
|
||||
V1_20_5("1.20.6", 41, DataFixerV1206::new),
|
||||
V1_21_3("1.21.3", 57, DataFixerV1213::new),
|
||||
V1_21_11("1.21.11", 75, DataFixerV1217::new);
|
||||
V1_21_11("1.21.11", 75, DataFixerV1217::new),
|
||||
V26_1_2("26.1.2", 101, DataFixerV1217::new);
|
||||
private static final KMap<DataVersion, IDataFixer> cache = new KMap<>();
|
||||
@Getter(AccessLevel.NONE)
|
||||
private final Supplier<IDataFixer> constructor;
|
||||
|
||||
@@ -26,6 +26,7 @@ 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.engine.object.annotations.*;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.data.B;
|
||||
@@ -267,6 +268,44 @@ public class SchemaBuilder {
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Block Type (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) {
|
||||
String key = "enum-vanilla-structure";
|
||||
|
||||
if (!definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
JSONArray ja = new JSONArray();
|
||||
|
||||
for (String i : INMS.get().getStructureKeys()) {
|
||||
ja.put(i);
|
||||
}
|
||||
|
||||
j.put("enum", ja);
|
||||
definitions.put(key, j);
|
||||
}
|
||||
|
||||
fancyType = "Vanilla Structure";
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) {
|
||||
String key = "enum-vanilla-structure-set";
|
||||
|
||||
if (!definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
JSONArray ja = new JSONArray();
|
||||
|
||||
for (String i : INMS.get().getStructureSetKeys()) {
|
||||
ja.put(i);
|
||||
}
|
||||
|
||||
j.put("enum", ja);
|
||||
definitions.put(key, j);
|
||||
}
|
||||
|
||||
fancyType = "Vanilla Structure Set";
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure SET key (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListItemType.class)) {
|
||||
String key = "enum-item-type";
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* 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.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.LegacyTileData;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.structure.Palette;
|
||||
import org.bukkit.structure.Structure;
|
||||
import org.bukkit.util.BlockVector;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class StructureImporter {
|
||||
public enum Mode {
|
||||
OVERWRITE,
|
||||
ADD_ONLY,
|
||||
MERGE
|
||||
}
|
||||
|
||||
public record Result(boolean success, String message, int blocks) {
|
||||
}
|
||||
|
||||
private StructureImporter() {
|
||||
}
|
||||
|
||||
public static Mode parseMode(String s) {
|
||||
if (s == null) {
|
||||
return Mode.OVERWRITE;
|
||||
}
|
||||
return switch (s.toLowerCase().replace('-', '_')) {
|
||||
case "add_only", "addonly", "add" -> Mode.ADD_ONLY;
|
||||
case "merge" -> Mode.MERGE;
|
||||
default -> Mode.OVERWRITE;
|
||||
};
|
||||
}
|
||||
|
||||
public static String deriveName(NamespacedKey key) {
|
||||
return (key.getNamespace() + "_" + key.getKey()).replace('/', '_').replace(':', '_');
|
||||
}
|
||||
|
||||
public static Result importStructure(IrisData data, NamespacedKey key, String name, Mode mode) {
|
||||
Structure structure;
|
||||
try {
|
||||
structure = Bukkit.getStructureManager().loadStructure(key);
|
||||
} catch (Throwable e) {
|
||||
return new Result(false, "Failed to load structure " + key + ": " + e.getMessage(), 0);
|
||||
}
|
||||
if (structure == null || structure.getPalettes().isEmpty()) {
|
||||
return new Result(false, "No loadable structure NBT for key " + key + " (jigsaw structures must be imported by their piece keys)", 0);
|
||||
}
|
||||
|
||||
BlockVector size = structure.getSize();
|
||||
int w = Math.max(1, size.getBlockX());
|
||||
int h = Math.max(1, size.getBlockY());
|
||||
int d = Math.max(1, size.getBlockZ());
|
||||
|
||||
File objectFile = new File(data.getDataFolder(), "objects/" + name + ".iob");
|
||||
File pieceFile = new File(data.getDataFolder(), "jigsaw-pieces/" + name + ".json");
|
||||
File poolFile = new File(data.getDataFolder(), "jigsaw-pools/" + name + ".json");
|
||||
File structureFile = new File(data.getDataFolder(), "structures/" + name + ".json");
|
||||
|
||||
boolean exists = objectFile.exists() || structureFile.exists();
|
||||
if (mode == Mode.ADD_ONLY && exists) {
|
||||
return new Result(false, "Skipped (add-only): '" + name + "' already exists", 0);
|
||||
}
|
||||
|
||||
IrisObject object = new IrisObject(w, h, d);
|
||||
int count = 0;
|
||||
int tiles = 0;
|
||||
Palette palette = structure.getPalettes().get(0);
|
||||
for (BlockState block : palette.getBlocks()) {
|
||||
Location loc = block.getLocation();
|
||||
int x = loc.getBlockX();
|
||||
int y = loc.getBlockY();
|
||||
int z = loc.getBlockZ();
|
||||
if (x < 0 || y < 0 || z < 0 || x >= w || y >= h || z >= d) {
|
||||
continue;
|
||||
}
|
||||
object.setUnsigned(x, y, z, block.getBlockData());
|
||||
count++;
|
||||
LegacyTileData tile = captureTile(block);
|
||||
if (tile != null) {
|
||||
object.setUnsignedTile(x, y, z, tile);
|
||||
tiles++;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
objectFile.getParentFile().mkdirs();
|
||||
object.write(objectFile);
|
||||
writeJson(pieceFile, pieceJson(name));
|
||||
writeJson(poolFile, poolJson(name));
|
||||
writeJson(structureFile, structureJson(name, key.toString(), Math.max(w, d)));
|
||||
} catch (Throwable e) {
|
||||
return new Result(false, "Failed writing import for '" + name + "': " + e.getMessage(), count);
|
||||
}
|
||||
|
||||
return new Result(true, "Imported " + key + " as '" + name + "' (" + count + " blocks, " + tiles + " tiles, " + w + "x" + h + "x" + d + ")", count);
|
||||
}
|
||||
|
||||
private static LegacyTileData captureTile(BlockState block) {
|
||||
try {
|
||||
return LegacyTileData.fromBukkit(block);
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> pieceJson(String name) {
|
||||
Map<String, Object> piece = new LinkedHashMap<>();
|
||||
piece.put("object", name);
|
||||
piece.put("connectors", new ArrayList<>());
|
||||
piece.put("rotatable", true);
|
||||
return piece;
|
||||
}
|
||||
|
||||
private static Map<String, Object> poolJson(String name) {
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("piece", name);
|
||||
entry.put("weight", 1);
|
||||
List<Object> pieces = new ArrayList<>();
|
||||
pieces.add(entry);
|
||||
Map<String, Object> pool = new LinkedHashMap<>();
|
||||
pool.put("pieces", pieces);
|
||||
return pool;
|
||||
}
|
||||
|
||||
private static Map<String, Object> structureJson(String name, String source, int maxSpan) {
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("startPool", name);
|
||||
root.put("maxDepth", 1);
|
||||
root.put("maxSizeChunks", Math.max(1, (maxSpan / 16) + 1));
|
||||
root.put("placeMode", "CENTER_HEIGHT");
|
||||
root.put("vanillaSource", source);
|
||||
return root;
|
||||
}
|
||||
|
||||
private static void writeJson(File file, Map<String, Object> content) throws Exception {
|
||||
file.getParentFile().mkdirs();
|
||||
String json = new GsonBuilder().setPrettyPrinting().create().toJson(content);
|
||||
Files.writeString(file.toPath(), json, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.iris.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.nms.INMS;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class StructureIndexService {
|
||||
private static final Set<String> GENERATED = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private StructureIndexService() {
|
||||
}
|
||||
|
||||
public static File writeOnce(IrisData data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File dataFolder = data.getDataFolder();
|
||||
String key = dataFolder == null ? null : dataFolder.getAbsolutePath();
|
||||
if (key == null || !GENERATED.add(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return write(data);
|
||||
} catch (Throwable e) {
|
||||
GENERATED.remove(key);
|
||||
Iris.reportError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static File write(IrisData data) {
|
||||
KList<String> structures = INMS.get().getStructureKeys();
|
||||
KList<String> sets = INMS.get().getStructureSetKeys();
|
||||
|
||||
List<String> vanilla = new ArrayList<>();
|
||||
List<String> datapack = new ArrayList<>();
|
||||
for (String k : structures) {
|
||||
if (k.startsWith("minecraft:")) {
|
||||
vanilla.add(k);
|
||||
} else {
|
||||
datapack.add(k);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> vanillaSets = new ArrayList<>();
|
||||
List<String> datapackSets = new ArrayList<>();
|
||||
for (String k : sets) {
|
||||
if (k.startsWith("minecraft:")) {
|
||||
vanillaSets.add(k);
|
||||
} else {
|
||||
datapackSets.add(k);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> iris = new ArrayList<>();
|
||||
if (data.getStructureLoader() != null) {
|
||||
Collections.addAll(iris, data.getStructureLoader().getPossibleKeys());
|
||||
}
|
||||
|
||||
Collections.sort(vanilla);
|
||||
Collections.sort(datapack);
|
||||
Collections.sort(vanillaSets);
|
||||
Collections.sort(datapackSets);
|
||||
Collections.sort(iris);
|
||||
|
||||
Map<String, Object> structuresNode = new LinkedHashMap<>();
|
||||
structuresNode.put("vanilla", vanilla);
|
||||
structuresNode.put("datapack", datapack);
|
||||
|
||||
Map<String, Object> setsNode = new LinkedHashMap<>();
|
||||
setsNode.put("vanilla", vanillaSets);
|
||||
setsNode.put("datapack", datapackSets);
|
||||
|
||||
Map<String, Object> counts = new LinkedHashMap<>();
|
||||
counts.put("vanillaStructures", vanilla.size());
|
||||
counts.put("datapackStructures", datapack.size());
|
||||
counts.put("structureSets", sets.size());
|
||||
counts.put("irisStructures", iris.size());
|
||||
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("note", "Generated index of every structure available at runtime. Toggle vanilla/datapack generation via the dimension vanillaStructures block (by structure SET key). Place structures via biome/region/dimension structures lists.");
|
||||
root.put("counts", counts);
|
||||
root.put("structures", structuresNode);
|
||||
root.put("structureSets", setsNode);
|
||||
root.put("iris", iris);
|
||||
|
||||
File file = new File(data.getDataFolder(), "structures/structure-index.json");
|
||||
try {
|
||||
file.getParentFile().mkdirs();
|
||||
String json = new GsonBuilder().setPrettyPrinting().create().toJson(root);
|
||||
Files.writeString(file.toPath(), json, StandardCharsets.UTF_8);
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
/*
|
||||
* 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.iris.core.loader.IrisData;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class VillageImporter {
|
||||
public record Result(boolean success, String message, int pools, int pieces) {
|
||||
}
|
||||
|
||||
private VillageImporter() {
|
||||
}
|
||||
|
||||
public static Result importVillage(IrisData data, NamespacedKey structureKey, String name, StructureImporter.Mode mode) {
|
||||
Object server;
|
||||
Object registryAccess;
|
||||
Object structureManager;
|
||||
try {
|
||||
Object craftServer = Bukkit.getServer();
|
||||
Object dedicated = invoke(craftServer, "getHandle");
|
||||
server = invoke(dedicated, "getServer");
|
||||
registryAccess = resolveRegistryAccess(server);
|
||||
structureManager = invoke(server, "getStructureManager");
|
||||
} catch (Throwable e) {
|
||||
return new Result(false, "Failed to access server registries via reflection: " + e, 0, 0);
|
||||
}
|
||||
if (registryAccess == null) {
|
||||
return new Result(false, "Could not resolve RegistryAccess from the server", 0, 0);
|
||||
}
|
||||
|
||||
Object startPool;
|
||||
int maxDepth;
|
||||
try {
|
||||
Object structureRegistry = lookupRegistry(registryAccess, "STRUCTURE");
|
||||
Object structure = registryGet(structureRegistry, structureKey);
|
||||
if (structure == null) {
|
||||
return new Result(false, "No structure registered for key " + structureKey, 0, 0);
|
||||
}
|
||||
if (!structure.getClass().getName().endsWith("JigsawStructure")) {
|
||||
return new Result(false, "Structure " + structureKey + " is not a jigsaw structure (" + structure.getClass().getSimpleName() + "); use 'import' for single-template structures", 0, 0);
|
||||
}
|
||||
Object startPoolHolder = invoke(structure, "getStartPool");
|
||||
startPool = unwrapHolder(startPoolHolder);
|
||||
maxDepth = readIntField(structure, "maxDepth", 7);
|
||||
} catch (Throwable e) {
|
||||
return new Result(false, "Failed to read jigsaw structure graph: " + e, 0, 0);
|
||||
}
|
||||
|
||||
Object templatePoolRegistry;
|
||||
java.util.Random random;
|
||||
try {
|
||||
templatePoolRegistry = lookupRegistry(registryAccess, "TEMPLATE_POOL");
|
||||
random = new java.util.Random(structureKey.hashCode());
|
||||
} catch (Throwable e) {
|
||||
return new Result(false, "Failed to access TEMPLATE_POOL registry: " + e, 0, 0);
|
||||
}
|
||||
|
||||
String startPoolKey;
|
||||
try {
|
||||
startPoolKey = registryKeyOf(templatePoolRegistry, startPool);
|
||||
} catch (Throwable e) {
|
||||
startPoolKey = null;
|
||||
}
|
||||
if (startPoolKey == null) {
|
||||
return new Result(false, "Could not resolve the start pool key for " + structureKey, 0, 0);
|
||||
}
|
||||
|
||||
Set<String> visitedPools = new HashSet<>();
|
||||
Set<String> importedPieces = new HashSet<>();
|
||||
Deque<String> poolQueue = new ArrayDeque<>();
|
||||
poolQueue.add(startPoolKey);
|
||||
|
||||
Map<String, Map<String, Object>> emittedPools = new LinkedHashMap<>();
|
||||
Map<String, Map<String, Object>> emittedPieces = new LinkedHashMap<>();
|
||||
List<String> errors = new ArrayList<>();
|
||||
int pieceBlocks = 0;
|
||||
|
||||
while (!poolQueue.isEmpty()) {
|
||||
String poolKey = poolQueue.poll();
|
||||
if (!visitedPools.add(poolKey)) {
|
||||
continue;
|
||||
}
|
||||
Object pool;
|
||||
try {
|
||||
pool = registryGetByKey(templatePoolRegistry, poolKey);
|
||||
} catch (Throwable e) {
|
||||
errors.add("pool " + poolKey + ": " + e.getMessage());
|
||||
continue;
|
||||
}
|
||||
if (pool == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String irisPoolName = poolName(name, poolKey);
|
||||
List<Object> pieceEntries = new ArrayList<>();
|
||||
|
||||
String fallbackKey = null;
|
||||
try {
|
||||
Object fallbackHolder = invoke(pool, "getFallback");
|
||||
Object fallbackPool = unwrapHolder(fallbackHolder);
|
||||
fallbackKey = registryKeyOf(templatePoolRegistry, fallbackPool);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (fallbackKey != null && !fallbackKey.equals(poolKey)) {
|
||||
poolQueue.add(fallbackKey);
|
||||
}
|
||||
|
||||
List<?> templates;
|
||||
try {
|
||||
templates = (List<?>) invoke(pool, "getTemplates");
|
||||
} catch (Throwable e) {
|
||||
errors.add("templates " + poolKey + ": " + e.getMessage());
|
||||
templates = List.of();
|
||||
}
|
||||
|
||||
for (Object pair : templates) {
|
||||
Object element;
|
||||
int weight;
|
||||
try {
|
||||
element = invoke(pair, "getFirst");
|
||||
Object second = invoke(pair, "getSecond");
|
||||
weight = second instanceof Number ? Math.max(1, ((Number) second).intValue()) : 1;
|
||||
} catch (Throwable e) {
|
||||
continue;
|
||||
}
|
||||
if (element == null) {
|
||||
continue;
|
||||
}
|
||||
String templateLocation = templateLocationOf(element);
|
||||
if (templateLocation == null) {
|
||||
continue;
|
||||
}
|
||||
NamespacedKey pieceNbtKey = NamespacedKey.fromString(templateLocation.toLowerCase());
|
||||
if (pieceNbtKey == null) {
|
||||
errors.add("bad piece key " + templateLocation);
|
||||
continue;
|
||||
}
|
||||
String irisPieceName = pieceName(name, templateLocation);
|
||||
|
||||
if (importedPieces.add(irisPieceName)) {
|
||||
StructureImporter.Result imported = StructureImporter.importStructure(data, pieceNbtKey, irisPieceName, mode);
|
||||
if (!imported.success()) {
|
||||
errors.add(templateLocation + ": " + imported.message());
|
||||
continue;
|
||||
}
|
||||
pieceBlocks += imported.blocks();
|
||||
removeStrayPieceArtifacts(data, irisPieceName);
|
||||
|
||||
Connectors result = readConnectors(element, structureManager, random, name);
|
||||
emittedPieces.put(irisPieceName, pieceJson(irisPieceName, result.json()));
|
||||
poolQueue.addAll(result.targetPoolKeys());
|
||||
}
|
||||
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
entry.put("piece", irisPieceName);
|
||||
entry.put("weight", weight);
|
||||
pieceEntries.add(entry);
|
||||
}
|
||||
|
||||
Map<String, Object> poolJson = new LinkedHashMap<>();
|
||||
poolJson.put("pieces", pieceEntries);
|
||||
if (fallbackKey != null && !fallbackKey.equals(poolKey)) {
|
||||
poolJson.put("fallback", poolName(name, fallbackKey));
|
||||
}
|
||||
emittedPools.put(irisPoolName, poolJson);
|
||||
}
|
||||
|
||||
if (emittedPieces.isEmpty()) {
|
||||
return new Result(false, "Imported 0 pieces for " + structureKey + (errors.isEmpty() ? "" : " (" + errors.get(0) + ")"), 0, 0);
|
||||
}
|
||||
|
||||
try {
|
||||
for (Map.Entry<String, Map<String, Object>> e : emittedPieces.entrySet()) {
|
||||
writeJson(new File(data.getDataFolder(), "jigsaw-pieces/" + e.getKey() + ".json"), e.getValue());
|
||||
}
|
||||
for (Map.Entry<String, Map<String, Object>> e : emittedPools.entrySet()) {
|
||||
writeJson(new File(data.getDataFolder(), "jigsaw-pools/" + e.getKey() + ".json"), e.getValue());
|
||||
}
|
||||
writeJson(new File(data.getDataFolder(), "structures/" + name + ".json"), structureJson(name, structureKey.toString(), poolName(name, startPoolKey), maxDepth));
|
||||
} catch (Throwable e) {
|
||||
return new Result(false, "Failed writing jigsaw resources for '" + name + "': " + e, emittedPools.size(), emittedPieces.size());
|
||||
}
|
||||
|
||||
String msg = "Imported village " + structureKey + " as '" + name + "': " + emittedPieces.size() + " pieces, " + emittedPools.size() + " pools, " + pieceBlocks + " blocks";
|
||||
if (!errors.isEmpty()) {
|
||||
msg += " (" + errors.size() + " piece(s) skipped; first: " + errors.get(0) + ")";
|
||||
}
|
||||
return new Result(true, msg, emittedPools.size(), emittedPieces.size());
|
||||
}
|
||||
|
||||
private record Connectors(List<Map<String, Object>> json, Set<String> targetPoolKeys) {
|
||||
}
|
||||
|
||||
private static Connectors readConnectors(Object element, Object structureManager, java.util.Random random, String baseName) {
|
||||
List<Map<String, Object>> connectors = new ArrayList<>();
|
||||
Set<String> targets = new HashSet<>();
|
||||
try {
|
||||
Object zero = staticField("net.minecraft.core.BlockPos", "ZERO");
|
||||
Object rotationNone = staticField("net.minecraft.world.level.block.Rotation", "NONE");
|
||||
Method m = findMethod4(element.getClass(), "getShuffledJigsawBlocks");
|
||||
if (m == null) {
|
||||
return new Connectors(connectors, targets);
|
||||
}
|
||||
m.setAccessible(true);
|
||||
Object random0 = freshRandomSource(random);
|
||||
List<?> blocks = (List<?>) m.invoke(element, structureManager, zero, rotationNone, random0);
|
||||
if (blocks == null) {
|
||||
return new Connectors(connectors, targets);
|
||||
}
|
||||
for (Object jigsaw : blocks) {
|
||||
String[] rawPoolKey = new String[1];
|
||||
Map<String, Object> connector = connectorFrom(jigsaw, baseName, rawPoolKey);
|
||||
if (connector != null) {
|
||||
connectors.add(connector);
|
||||
if (rawPoolKey[0] != null && !rawPoolKey[0].isEmpty()) {
|
||||
targets.add(rawPoolKey[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return new Connectors(connectors, targets);
|
||||
}
|
||||
|
||||
private static Map<String, Object> connectorFrom(Object jigsaw, String baseName, String[] rawPoolKeyOut) {
|
||||
try {
|
||||
Object info = invoke(jigsaw, "info");
|
||||
Object pos = invoke(info, "pos");
|
||||
Object blockState = invoke(info, "state");
|
||||
int x = readInt(pos, "getX");
|
||||
int y = readInt(pos, "getY");
|
||||
int z = readInt(pos, "getZ");
|
||||
|
||||
Object poolKey = invoke(jigsaw, "pool");
|
||||
String poolId = identifierString(invoke(poolKey, "identifier"));
|
||||
Object nameId = invoke(jigsaw, "name");
|
||||
Object targetId = invoke(jigsaw, "target");
|
||||
Object jointType = invoke(jigsaw, "jointType");
|
||||
|
||||
String front = frontFacing(blockState);
|
||||
rawPoolKeyOut[0] = poolId;
|
||||
|
||||
Map<String, Object> connector = new LinkedHashMap<>();
|
||||
Map<String, Object> position = new LinkedHashMap<>();
|
||||
position.put("x", x);
|
||||
position.put("y", y);
|
||||
position.put("z", z);
|
||||
connector.put("position", position);
|
||||
connector.put("direction", irisDirection(front));
|
||||
connector.put("pool", poolId == null ? "" : poolName(baseName, poolId));
|
||||
connector.put("name", identifierString(nameId));
|
||||
connector.put("targetName", identifierString(targetId));
|
||||
connector.put("joint", jointType != null && jointType.toString().toUpperCase().contains("ALIGN") ? "ALIGNED" : "ROLLABLE");
|
||||
return connector;
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String frontFacing(Object blockState) {
|
||||
try {
|
||||
Class<?> jigsawBlock = Class.forName("net.minecraft.world.level.block.JigsawBlock");
|
||||
Method getFront = jigsawBlock.getMethod("getFrontFacing", Class.forName("net.minecraft.world.level.block.state.BlockState"));
|
||||
Object direction = getFront.invoke(null, blockState);
|
||||
if (direction == null) {
|
||||
return "north";
|
||||
}
|
||||
Method getName = direction.getClass().getMethod("getName");
|
||||
getName.setAccessible(true);
|
||||
return String.valueOf(getName.invoke(direction)).toLowerCase();
|
||||
} catch (Throwable e) {
|
||||
return "north";
|
||||
}
|
||||
}
|
||||
|
||||
private static String irisDirection(String front) {
|
||||
return switch (front) {
|
||||
case "up" -> "UP_POSITIVE_Y";
|
||||
case "down" -> "DOWN_NEGATIVE_Y";
|
||||
case "south" -> "SOUTH_POSITIVE_Z";
|
||||
case "east" -> "EAST_POSITIVE_X";
|
||||
case "west" -> "WEST_NEGATIVE_X";
|
||||
default -> "NORTH_NEGATIVE_Z";
|
||||
};
|
||||
}
|
||||
|
||||
private static String templateLocationOf(Object element) {
|
||||
try {
|
||||
Method m = findMethod(element.getClass(), "getTemplateLocation");
|
||||
if (m == null) {
|
||||
return null;
|
||||
}
|
||||
m.setAccessible(true);
|
||||
Object id = m.invoke(element);
|
||||
return identifierString(id);
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Object resolveRegistryAccess(Object server) {
|
||||
try {
|
||||
Class<?> frozen = Class.forName("net.minecraft.core.RegistryAccess$Frozen");
|
||||
for (Method m : server.getClass().getMethods()) {
|
||||
if (m.getParameterCount() == 0 && frozen.isAssignableFrom(m.getReturnType())) {
|
||||
m.setAccessible(true);
|
||||
Object o = m.invoke(server);
|
||||
if (o != null) {
|
||||
return o;
|
||||
}
|
||||
}
|
||||
}
|
||||
Class<?> ra = Class.forName("net.minecraft.core.RegistryAccess");
|
||||
for (Method m : server.getClass().getMethods()) {
|
||||
if (m.getParameterCount() == 0 && ra.isAssignableFrom(m.getReturnType())) {
|
||||
m.setAccessible(true);
|
||||
Object o = m.invoke(server);
|
||||
if (o != null) {
|
||||
return o;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object lookupRegistry(Object registryAccess, String registryName) throws Exception {
|
||||
Class<?> registries = Class.forName("net.minecraft.core.registries.Registries");
|
||||
Object resourceKey = registries.getField(registryName).get(null);
|
||||
Class<?> registryClass = Class.forName("net.minecraft.core.Registry");
|
||||
Method registryOverload = null;
|
||||
for (Method m : registryAccess.getClass().getMethods()) {
|
||||
if (m.getName().equals("lookupOrThrow") && m.getParameterCount() == 1
|
||||
&& m.getParameterTypes()[0].getName().endsWith("ResourceKey")
|
||||
&& registryClass.isAssignableFrom(m.getReturnType())) {
|
||||
registryOverload = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (registryOverload == null) {
|
||||
for (Method m : registryAccess.getClass().getMethods()) {
|
||||
if (m.getName().equals("lookupOrThrow") && m.getParameterCount() == 1
|
||||
&& m.getParameterTypes()[0].getName().endsWith("ResourceKey")) {
|
||||
registryOverload = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (registryOverload == null) {
|
||||
throw new NoSuchMethodException("lookupOrThrow(ResourceKey) on " + registryAccess.getClass().getName());
|
||||
}
|
||||
registryOverload.setAccessible(true);
|
||||
return registryOverload.invoke(registryAccess, resourceKey);
|
||||
}
|
||||
|
||||
private static Object registryGet(Object registry, NamespacedKey key) throws Exception {
|
||||
Object id = identifierOf(key);
|
||||
for (Method m : registry.getClass().getMethods()) {
|
||||
if (m.getName().equals("getValue") && m.getParameterCount() == 1 && m.getParameterTypes()[0].getName().endsWith("Identifier")) {
|
||||
m.setAccessible(true);
|
||||
return m.invoke(registry, id);
|
||||
}
|
||||
}
|
||||
for (Method m : registry.getClass().getMethods()) {
|
||||
if (m.getName().equals("getOptional") && m.getParameterCount() == 1 && m.getParameterTypes()[0].getName().endsWith("Identifier")) {
|
||||
m.setAccessible(true);
|
||||
return unwrapOptional(m.invoke(registry, id));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object registryGetByKey(Object registry, String key) throws Exception {
|
||||
NamespacedKey nk = NamespacedKey.fromString(key.toLowerCase());
|
||||
if (nk == null) {
|
||||
return null;
|
||||
}
|
||||
return registryGet(registry, nk);
|
||||
}
|
||||
|
||||
private static String registryKeyOf(Object registry, Object value) throws Exception {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
for (Method m : registry.getClass().getMethods()) {
|
||||
if (m.getName().equals("getKey") && m.getParameterCount() == 1) {
|
||||
m.setAccessible(true);
|
||||
Object id = m.invoke(registry, value);
|
||||
String s = identifierString(id);
|
||||
if (s != null) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object identifierOf(NamespacedKey key) throws Exception {
|
||||
Class<?> identifier = Class.forName("net.minecraft.resources.Identifier");
|
||||
try {
|
||||
Method fromNamespaceAndPath = identifier.getMethod("fromNamespaceAndPath", String.class, String.class);
|
||||
return fromNamespaceAndPath.invoke(null, key.getNamespace(), key.getKey());
|
||||
} catch (NoSuchMethodException e) {
|
||||
Method withDefaultNamespace = identifier.getMethod("parse", String.class);
|
||||
return withDefaultNamespace.invoke(null, key.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static String identifierString(Object id) {
|
||||
if (id == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Method getNamespace = id.getClass().getMethod("getNamespace");
|
||||
Method getPath = id.getClass().getMethod("getPath");
|
||||
getNamespace.setAccessible(true);
|
||||
getPath.setAccessible(true);
|
||||
return getNamespace.invoke(id) + ":" + getPath.invoke(id);
|
||||
} catch (Throwable e) {
|
||||
return id.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static Object unwrapHolder(Object holder) {
|
||||
if (holder == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Method value = findMethod(holder.getClass(), "value");
|
||||
if (value != null) {
|
||||
value.setAccessible(true);
|
||||
return value.invoke(holder);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
private static Object unwrapOptional(Object opt) {
|
||||
if (opt == null) {
|
||||
return null;
|
||||
}
|
||||
if (opt instanceof java.util.Optional<?> o) {
|
||||
return o.orElse(null);
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
|
||||
private static int readIntField(Object o, String fieldName, int fallback) {
|
||||
try {
|
||||
Field f = o.getClass().getDeclaredField(fieldName);
|
||||
f.setAccessible(true);
|
||||
return f.getInt(o);
|
||||
} catch (Throwable e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private static int readInt(Object o, String method) throws Exception {
|
||||
Method m = o.getClass().getMethod(method);
|
||||
m.setAccessible(true);
|
||||
return ((Number) m.invoke(o)).intValue();
|
||||
}
|
||||
|
||||
private static Object staticField(String className, String fieldName) throws Exception {
|
||||
Class<?> c = Class.forName(className);
|
||||
Field f = c.getField(fieldName);
|
||||
return f.get(null);
|
||||
}
|
||||
|
||||
private static Object invoke(Object target, String method) throws Exception {
|
||||
Method m = findMethod(target.getClass(), method);
|
||||
if (m == null) {
|
||||
throw new NoSuchMethodException(method + " on " + target.getClass().getName());
|
||||
}
|
||||
m.setAccessible(true);
|
||||
return m.invoke(target);
|
||||
}
|
||||
|
||||
private static Method findMethod(Class<?> type, String name) {
|
||||
Class<?> c = type;
|
||||
while (c != null) {
|
||||
for (Method m : c.getDeclaredMethods()) {
|
||||
if (m.getName().equals(name) && m.getParameterCount() == 0) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
c = c.getSuperclass();
|
||||
}
|
||||
for (Method m : type.getMethods()) {
|
||||
if (m.getName().equals(name) && m.getParameterCount() == 0) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Method findMethod4(Class<?> type, String name) {
|
||||
Class<?> c = type;
|
||||
while (c != null) {
|
||||
for (Method m : c.getDeclaredMethods()) {
|
||||
if (m.getName().equals(name) && m.getParameterCount() == 4) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
c = c.getSuperclass();
|
||||
}
|
||||
for (Method m : type.getMethods()) {
|
||||
if (m.getName().equals(name) && m.getParameterCount() == 4) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object freshRandomSource(java.util.Random random) throws Exception {
|
||||
Class<?> randomSource = Class.forName("net.minecraft.util.RandomSource");
|
||||
Method create = randomSource.getMethod("create", long.class);
|
||||
return create.invoke(null, random.nextLong());
|
||||
}
|
||||
|
||||
private static String poolName(String base, String poolKey) {
|
||||
return base + "/pool/" + sanitize(poolKey);
|
||||
}
|
||||
|
||||
private static String pieceName(String base, String templateLocation) {
|
||||
return base + "/piece/" + sanitize(templateLocation);
|
||||
}
|
||||
|
||||
private static String sanitize(String key) {
|
||||
return key.replace(':', '_').replace('/', '_');
|
||||
}
|
||||
|
||||
private static Map<String, Object> pieceJson(String pieceName, List<Map<String, Object>> connectors) {
|
||||
Map<String, Object> piece = new LinkedHashMap<>();
|
||||
piece.put("object", pieceName);
|
||||
piece.put("connectors", connectors);
|
||||
piece.put("rotatable", true);
|
||||
return piece;
|
||||
}
|
||||
|
||||
private static Map<String, Object> structureJson(String name, String source, String startPool, int maxDepth) {
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("startPool", startPool);
|
||||
root.put("maxDepth", Math.max(1, Math.min(30, maxDepth)));
|
||||
root.put("maxSizeChunks", 8);
|
||||
root.put("placeMode", "STRUCTURE_PIECE");
|
||||
root.put("vanillaSource", source);
|
||||
return root;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
private static void writeJson(File file, Map<String, Object> content) throws Exception {
|
||||
file.getParentFile().mkdirs();
|
||||
String json = new GsonBuilder().setPrettyPrinting().create().toJson(content);
|
||||
Files.writeString(file.toPath(), json, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import art.arcane.iris.core.nms.container.BlockPos;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.core.project.IrisProject;
|
||||
import art.arcane.iris.core.service.PreservationSVC;
|
||||
import art.arcane.iris.core.structure.StructureIndexService;
|
||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.*;
|
||||
@@ -147,6 +148,11 @@ public class IrisEngine implements Engine {
|
||||
_t0 = M.ms();
|
||||
getData().loadPrefetch(this);
|
||||
Iris.info("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms");
|
||||
try {
|
||||
StructureIndexService.writeOnce(getData());
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
Iris.info("Initializing Engine: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " (" + target.getDimension().getDimensionHeight() + " height) Seed: " + getSeedManager().getSeed());
|
||||
failing = false;
|
||||
closed = false;
|
||||
|
||||
@@ -31,6 +31,7 @@ import art.arcane.iris.engine.mantle.components.MantleCarvingComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleFloatingObjectComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleFluidBodyComponent;
|
||||
import art.arcane.iris.engine.mantle.components.MantleObjectComponent;
|
||||
import art.arcane.iris.engine.mantle.components.IrisStructureComponent;
|
||||
import art.arcane.iris.util.project.matter.IrisMatterSupport;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -84,6 +85,7 @@ public class IrisEngineMantle implements EngineMantle {
|
||||
object = new MantleObjectComponent(this);
|
||||
registerComponent(object);
|
||||
registerComponent(new MantleFloatingObjectComponent(this));
|
||||
registerComponent(new IrisStructureComponent(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -348,8 +348,10 @@ final class DecoratorCore {
|
||||
r = EngineMantle.AIR;
|
||||
}
|
||||
if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) {
|
||||
found = true;
|
||||
data.setFace(f, true);
|
||||
if (data.getAllowedFaces().contains(f)) {
|
||||
found = true;
|
||||
data.setFace(f, true);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -361,12 +363,17 @@ final class DecoratorCore {
|
||||
|
||||
r = hunk.get(xx, yy, zz);
|
||||
if (r.isFaceSturdy(f.getOppositeFace(), BlockSupport.FULL)) {
|
||||
found = true;
|
||||
data.setFace(f, true);
|
||||
if (data.getAllowedFaces().contains(f)) {
|
||||
found = true;
|
||||
data.setFace(f, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
data.setFace(BlockFace.DOWN, true);
|
||||
BlockFace fallback = data.getAllowedFaces().contains(BlockFace.DOWN) ? BlockFace.DOWN : BlockFace.UP;
|
||||
if (data.getAllowedFaces().contains(fallback)) {
|
||||
data.setFace(fallback, true);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.engine.object.IrisJigsawPiece;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class PlacedStructurePiece {
|
||||
private final IrisJigsawPiece piece;
|
||||
private final IrisObject object;
|
||||
private final int x;
|
||||
private final int y;
|
||||
private final int z;
|
||||
private final IrisObjectRotation rotation;
|
||||
private final int minX;
|
||||
private final int minY;
|
||||
private final int minZ;
|
||||
private final int maxX;
|
||||
private final int maxY;
|
||||
private final int maxZ;
|
||||
|
||||
public boolean intersects(PlacedStructurePiece o) {
|
||||
return minX <= o.maxX && maxX >= o.minX
|
||||
&& minY <= o.maxY && maxY >= o.minY
|
||||
&& minZ <= o.maxZ && maxZ >= o.minZ;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* 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.Iris;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.object.IrisDirection;
|
||||
import art.arcane.iris.engine.object.IrisJigsawConnector;
|
||||
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||
import art.arcane.iris.engine.object.IrisJigsawPieceEntry;
|
||||
import art.arcane.iris.engine.object.IrisJigsawPool;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectRotation;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.JigsawJoint;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.util.BlockVector;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
|
||||
public final class StructureAssembler {
|
||||
private static final int HARD_PIECE_CAP = 512;
|
||||
private static final int[] Y_DEGREES = {0, 90, 180, 270};
|
||||
|
||||
private final IrisData data;
|
||||
private final IrisStructure structure;
|
||||
private final int originX;
|
||||
private final int originY;
|
||||
private final int originZ;
|
||||
private final int radius;
|
||||
|
||||
public StructureAssembler(IrisData data, IrisStructure structure, int originX, int originY, int originZ) {
|
||||
this.data = data;
|
||||
this.structure = structure;
|
||||
this.originX = originX;
|
||||
this.originY = originY;
|
||||
this.originZ = originZ;
|
||||
this.radius = Math.max(1, structure.getMaxSizeChunks()) * 16;
|
||||
}
|
||||
|
||||
public KList<PlacedStructurePiece> assemble(RNG rng) {
|
||||
IrisJigsawPool startPool = IrisData.loadAnyJigsawPool(structure.getStartPool(), data);
|
||||
if (startPool == null || startPool.getPieces().isEmpty()) {
|
||||
Iris.warn("Structure " + structure.getLoadKey() + " has no resolvable start pool '" + structure.getStartPool() + "'");
|
||||
return null;
|
||||
}
|
||||
|
||||
KList<PlacedStructurePiece> placed = new KList<>();
|
||||
Deque<OpenConnector> open = new ArrayDeque<>();
|
||||
|
||||
IrisJigsawPiece startPiece = pickPiece(startPool, rng);
|
||||
if (startPiece == null) {
|
||||
return null;
|
||||
}
|
||||
IrisObject startObject = IrisData.loadAnyObject(startPiece.getObject(), data);
|
||||
if (startObject == null) {
|
||||
Iris.warn("Jigsaw piece references missing object '" + startPiece.getObject() + "'");
|
||||
return null;
|
||||
}
|
||||
|
||||
int startRotY = startPiece.isRotatable() ? Y_DEGREES[rng.i(0, 3)] : 0;
|
||||
IrisObjectRotation startRot = IrisObjectRotation.of(0, startRotY, 0);
|
||||
PlacedStructurePiece start = build(startPiece, startObject, originX, originY, originZ, startRot);
|
||||
placed.add(start);
|
||||
enqueueConnectors(open, start, startObject, 1, null);
|
||||
|
||||
while (!open.isEmpty() && placed.size() < HARD_PIECE_CAP) {
|
||||
OpenConnector c = open.poll();
|
||||
String poolName = c.pool;
|
||||
int depth = c.depth;
|
||||
|
||||
IrisJigsawPool pool = IrisData.loadAnyJigsawPool(poolName, data);
|
||||
if (pool == null) {
|
||||
continue;
|
||||
}
|
||||
if (depth > structure.getMaxDepth()) {
|
||||
if (pool.getFallback() == null || pool.getFallback().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
pool = IrisData.loadAnyJigsawPool(pool.getFallback(), data);
|
||||
if (pool == null || pool.getPieces().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
attachOne(open, placed, pool, c, depth, rng);
|
||||
}
|
||||
|
||||
return placed;
|
||||
}
|
||||
|
||||
private void attachOne(Deque<OpenConnector> open, KList<PlacedStructurePiece> placed, IrisJigsawPool pool, OpenConnector c, int depth, RNG rng) {
|
||||
for (String pieceName : weightedOrder(pool, rng)) {
|
||||
IrisJigsawPiece piece = IrisData.loadAnyJigsawPiece(pieceName, data);
|
||||
if (piece == null) {
|
||||
continue;
|
||||
}
|
||||
IrisObject object = IrisData.loadAnyObject(piece.getObject(), data);
|
||||
if (object == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (IrisJigsawConnector cb : piece.getConnectors()) {
|
||||
if (!cb.getName().equals(c.targetName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
IrisDirection needed = c.facing.reverse();
|
||||
for (int yDeg : rotationCandidates(cb.getJoint(), rng)) {
|
||||
IrisObjectRotation rot = IrisObjectRotation.of(0, yDeg, 0);
|
||||
IrisDirection rotatedFace = rotateDirection(rot, cb.getDirection());
|
||||
if (rotatedFace != needed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
BlockVector centerB = centerOf(object);
|
||||
BlockVector crB = new BlockVector(
|
||||
cb.getPosition().getX() - centerB.getBlockX(),
|
||||
cb.getPosition().getY() - centerB.getBlockY(),
|
||||
cb.getPosition().getZ() - centerB.getBlockZ());
|
||||
BlockVector rcrB = rot.rotate(crB.clone(), 0, 0, 0);
|
||||
int wcx = c.wx + c.facing.toVector().getBlockX();
|
||||
int wcy = c.wy + c.facing.toVector().getBlockY();
|
||||
int wcz = c.wz + c.facing.toVector().getBlockZ();
|
||||
int px = wcx - rcrB.getBlockX();
|
||||
int py = wcy - rcrB.getBlockY();
|
||||
int pz = wcz - rcrB.getBlockZ();
|
||||
|
||||
PlacedStructurePiece candidate = build(piece, object, px, py, pz, rot);
|
||||
if (!withinRadius(candidate) || collides(placed, candidate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
placed.add(candidate);
|
||||
enqueueConnectors(open, candidate, object, depth + 1, cb);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void enqueueConnectors(Deque<OpenConnector> open, PlacedStructurePiece p, IrisObject object, int depth, IrisJigsawConnector skip) {
|
||||
BlockVector center = centerOf(object);
|
||||
for (IrisJigsawConnector con : p.getPiece().getConnectors()) {
|
||||
if (con == skip) {
|
||||
continue;
|
||||
}
|
||||
BlockVector cr = new BlockVector(
|
||||
con.getPosition().getX() - center.getBlockX(),
|
||||
con.getPosition().getY() - center.getBlockY(),
|
||||
con.getPosition().getZ() - center.getBlockZ());
|
||||
BlockVector rcr = p.getRotation().rotate(cr.clone(), 0, 0, 0);
|
||||
IrisDirection facing = rotateDirection(p.getRotation(), con.getDirection());
|
||||
open.add(new OpenConnector(
|
||||
p.getX() + rcr.getBlockX(),
|
||||
p.getY() + rcr.getBlockY(),
|
||||
p.getZ() + rcr.getBlockZ(),
|
||||
facing, con.getPool(), con.getName(), con.getTargetName(), con.getJoint(), depth));
|
||||
}
|
||||
}
|
||||
|
||||
private PlacedStructurePiece build(IrisJigsawPiece piece, IrisObject object, int x, int y, int z, IrisObjectRotation rot) {
|
||||
BlockVector rotated = rot.rotate(new BlockVector(object.getW(), object.getH(), object.getD()), 0, 0, 0);
|
||||
int rw = Math.abs(rotated.getBlockX());
|
||||
int rh = Math.abs(rotated.getBlockY());
|
||||
int rd = Math.abs(rotated.getBlockZ());
|
||||
int hx = rw / 2;
|
||||
int hy = rh / 2;
|
||||
int hz = rd / 2;
|
||||
return new PlacedStructurePiece(piece, object, x, y, z, rot,
|
||||
x - hx, y - hy, z - hz, x + hx, y + hy, z + hz);
|
||||
}
|
||||
|
||||
private boolean withinRadius(PlacedStructurePiece p) {
|
||||
return p.getMinX() >= originX - radius && p.getMaxX() <= originX + radius
|
||||
&& p.getMinZ() >= originZ - radius && p.getMaxZ() <= originZ + radius;
|
||||
}
|
||||
|
||||
private boolean collides(KList<PlacedStructurePiece> placed, PlacedStructurePiece candidate) {
|
||||
for (PlacedStructurePiece p : placed) {
|
||||
if (p.intersects(candidate)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int[] rotationCandidates(JigsawJoint joint, RNG rng) {
|
||||
if (joint == JigsawJoint.ALIGNED) {
|
||||
return Y_DEGREES;
|
||||
}
|
||||
int[] shuffled = {0, 90, 180, 270};
|
||||
for (int i = shuffled.length - 1; i > 0; i--) {
|
||||
int j = rng.i(0, i);
|
||||
int t = shuffled[i];
|
||||
shuffled[i] = shuffled[j];
|
||||
shuffled[j] = t;
|
||||
}
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
private IrisDirection rotateDirection(IrisObjectRotation rot, IrisDirection direction) {
|
||||
BlockVector v = direction.toVector().toBlockVector();
|
||||
BlockVector r = rot.rotate(v.clone(), 0, 0, 0);
|
||||
return IrisDirection.getDirection(r);
|
||||
}
|
||||
|
||||
private BlockVector centerOf(IrisObject object) {
|
||||
return new BlockVector(object.getW() / 2, object.getH() / 2, object.getD() / 2);
|
||||
}
|
||||
|
||||
private IrisJigsawPiece pickPiece(IrisJigsawPool pool, RNG rng) {
|
||||
String name = weightedPick(pool, rng);
|
||||
return name == null ? null : IrisData.loadAnyJigsawPiece(name, data);
|
||||
}
|
||||
|
||||
private String weightedPick(IrisJigsawPool pool, RNG rng) {
|
||||
int total = 0;
|
||||
for (IrisJigsawPieceEntry e : pool.getPieces()) {
|
||||
total += Math.max(1, e.getWeight());
|
||||
}
|
||||
if (total <= 0) {
|
||||
return null;
|
||||
}
|
||||
int t = rng.i(0, total - 1);
|
||||
for (IrisJigsawPieceEntry e : pool.getPieces()) {
|
||||
t -= Math.max(1, e.getWeight());
|
||||
if (t < 0) {
|
||||
return e.getPiece();
|
||||
}
|
||||
}
|
||||
return pool.getPieces().getFirst().getPiece();
|
||||
}
|
||||
|
||||
private KList<String> weightedOrder(IrisJigsawPool pool, RNG rng) {
|
||||
KList<IrisJigsawPieceEntry> remaining = new KList<>(pool.getPieces());
|
||||
KList<String> order = new KList<>();
|
||||
while (!remaining.isEmpty()) {
|
||||
int total = 0;
|
||||
for (IrisJigsawPieceEntry e : remaining) {
|
||||
total += Math.max(1, e.getWeight());
|
||||
}
|
||||
int t = rng.i(0, total - 1);
|
||||
int idx = 0;
|
||||
for (int i = 0; i < remaining.size(); i++) {
|
||||
t -= Math.max(1, remaining.get(i).getWeight());
|
||||
if (t < 0) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
order.add(remaining.remove(idx).getPiece());
|
||||
}
|
||||
return order;
|
||||
}
|
||||
|
||||
private record OpenConnector(int wx, int wy, int wz, IrisDirection facing, String pool, String name,
|
||||
String targetName, JigsawJoint joint, int depth) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
public final class StructurePlacementGrid {
|
||||
private StructurePlacementGrid() {
|
||||
}
|
||||
|
||||
public static boolean startsInChunk(IrisStructurePlacement placement, int cx, int cz, long seed, RNG chunkRng) {
|
||||
return switch (placement.getDistribution()) {
|
||||
case RANDOM_SPREAD -> randomSpreadStart(cx, cz, placement.getSpacing(), placement.getSeparation(), placement.getSalt(), seed);
|
||||
case DENSITY -> chunkRng.chance(placement.getDensity());
|
||||
case CONCENTRIC_RINGS -> concentricRingsStart(cx, cz, placement, seed);
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean randomSpreadStart(int cx, int cz, int spacing, int separation, int salt, long seed) {
|
||||
int sp = Math.max(1, spacing);
|
||||
int sep = Math.max(0, Math.min(separation, sp - 1));
|
||||
int cellX = Math.floorDiv(cx, sp);
|
||||
int cellZ = Math.floorDiv(cz, sp);
|
||||
RNG r = new RNG(mix(seed, cellX, cellZ, salt));
|
||||
int range = Math.max(1, sp - sep);
|
||||
int startCx = cellX * sp + r.i(0, range - 1);
|
||||
int startCz = cellZ * sp + r.i(0, range - 1);
|
||||
return startCx == cx && startCz == cz;
|
||||
}
|
||||
|
||||
private static boolean concentricRingsStart(int cx, int cz, IrisStructurePlacement placement, long seed) {
|
||||
if (cx == 0 && cz == 0) {
|
||||
return false;
|
||||
}
|
||||
int distance = Math.max(1, placement.getRingDistance());
|
||||
int count = Math.max(1, placement.getRingCount());
|
||||
int spread = Math.max(1, placement.getRingSpread());
|
||||
double dist = Math.sqrt((double) cx * cx + (double) cz * cz);
|
||||
int ring = (int) Math.round(dist / distance);
|
||||
if (ring <= 0) {
|
||||
return false;
|
||||
}
|
||||
int ringRadius = ring * distance;
|
||||
int slots = Math.min(spread * ring, count);
|
||||
if (slots <= 0) {
|
||||
return false;
|
||||
}
|
||||
double slotAngle = (2 * Math.PI) / slots;
|
||||
double offset = (mix(seed, ring, 0, 0) & 0xFFFFL) / 65536.0 * slotAngle;
|
||||
double angle = Math.atan2(cz, cx);
|
||||
int slotIndex = (int) Math.round((angle - offset) / slotAngle);
|
||||
double idealAngle = offset + slotIndex * slotAngle;
|
||||
int idealCx = (int) Math.round(Math.cos(idealAngle) * ringRadius);
|
||||
int idealCz = (int) Math.round(Math.sin(idealAngle) * ringRadius);
|
||||
return idealCx == cx && idealCz == cz;
|
||||
}
|
||||
|
||||
public static long mix(long seed, int a, int b, int salt) {
|
||||
long h = seed;
|
||||
h = h * 6364136223846793005L + (a * 341873128712L);
|
||||
h = h * 6364136223846793005L + (b * 132897987541L);
|
||||
h = h * 6364136223846793005L + salt;
|
||||
h ^= (h >>> 33);
|
||||
h *= 0xff51afd7ed558ccdL;
|
||||
h ^= (h >>> 33);
|
||||
return h;
|
||||
}
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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.mantle.components;
|
||||
|
||||
import art.arcane.iris.engine.IrisComplex;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.iris.engine.framework.PlacedStructurePiece;
|
||||
import art.arcane.iris.engine.framework.StructureAssembler;
|
||||
import art.arcane.iris.engine.framework.StructurePlacementGrid;
|
||||
import art.arcane.iris.engine.mantle.ComponentFlag;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.mantle.IrisMantleComponent;
|
||||
import art.arcane.iris.engine.mantle.MantleWriter;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisObject;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.ObjectPlaceMode;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.StructurePlacementRoute;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.iris.util.project.context.ChunkContext;
|
||||
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
|
||||
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
|
||||
@ComponentFlag(ReservedFlag.JIGSAW)
|
||||
public class IrisStructureComponent extends IrisMantleComponent {
|
||||
public IrisStructureComponent(EngineMantle engineMantle) {
|
||||
super(engineMantle, ReservedFlag.JIGSAW, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ChunkCoordinates
|
||||
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
|
||||
IrisComplex complex = context.getComplex();
|
||||
int xxx = 8 + (x << 4);
|
||||
int zzz = 8 + (z << 4);
|
||||
IrisRegion region = complex.getRegionStream().get(xxx, zzz);
|
||||
IrisBiome biome = complex.getTrueBiomeStream().get(xxx, zzz);
|
||||
RNG rng = new RNG(Cache.key(x, z) + seed());
|
||||
|
||||
KList<IrisStructurePlacement> placements = new KList<>();
|
||||
if (biome != null) {
|
||||
placements.addAll(biome.getStructures());
|
||||
}
|
||||
if (region != null) {
|
||||
placements.addAll(region.getStructures());
|
||||
}
|
||||
placements.addAll(getDimension().getStructures());
|
||||
|
||||
for (IrisStructurePlacement placement : placements) {
|
||||
placeFromPlacement(writer, placement, x, z, rng);
|
||||
}
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
private void placeFromPlacement(MantleWriter writer, IrisStructurePlacement placement, int cx, int cz, RNG rng) {
|
||||
if (placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) {
|
||||
return;
|
||||
}
|
||||
if (placement.getStructures().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed(), rng)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int sx = (cx << 4) + rng.i(0, 15);
|
||||
int sz = (cz << 4) + rng.i(0, 15);
|
||||
int surfaceY = getEngineMantle().getEngine().getHeight(sx, sz, true) + getEngineMantle().getEngine().getMinHeight();
|
||||
if (surfaceY < placement.getMinHeight() || surfaceY > placement.getMaxHeight()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String key = placement.getStructures().get(rng.i(0, placement.getStructures().size() - 1));
|
||||
IrisStructure structure = art.arcane.iris.core.loader.IrisData.loadAnyStructure(key, getData());
|
||||
if (structure == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
StructureAssembler assembler = new StructureAssembler(getData(), structure, sx, surfaceY, sz);
|
||||
KList<PlacedStructurePiece> pieces = assembler.assemble(rng);
|
||||
if (pieces == null || pieces.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ObjectPlaceMode mode = structure.getPlaceMode();
|
||||
if (mode == ObjectPlaceMode.STRUCTURE_PIECE || mode == ObjectPlaceMode.FLOATING) {
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY(), rng);
|
||||
}
|
||||
} else if (pieces.size() == 1) {
|
||||
placeObject(writer, structure, pieces.getFirst(), mode, -1, rng);
|
||||
} else {
|
||||
int lowest = Integer.MAX_VALUE;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
lowest = Math.min(lowest, p.getMinY());
|
||||
}
|
||||
int shift = surfaceY - lowest;
|
||||
for (PlacedStructurePiece p : pieces) {
|
||||
placeObject(writer, structure, p, ObjectPlaceMode.STRUCTURE_PIECE, p.getY() + shift, rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void placeObject(MantleWriter writer, IrisStructure structure, PlacedStructurePiece p, ObjectPlaceMode mode, int y, RNG rng) {
|
||||
IrisObject object = p.getObject();
|
||||
IrisObjectPlacement config = new IrisObjectPlacement();
|
||||
config.setMode(mode);
|
||||
config.setRotation(p.getRotation());
|
||||
config.getPlace().add(object.getLoadKey());
|
||||
if (!structure.getEdit().isEmpty()) {
|
||||
config.setEdit(structure.getEdit());
|
||||
}
|
||||
object.place(p.getX(), y, p.getZ(), writer, config, rng, null, null, getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int computeRadius() {
|
||||
IrisDimension dimension = getDimension();
|
||||
int maxChunks = 0;
|
||||
|
||||
for (IrisRegion region : dimension.getAllRegions(this::getData)) {
|
||||
maxChunks = Math.max(maxChunks, maxFrom(region.getStructures()));
|
||||
}
|
||||
for (IrisBiome biome : dimension.getAllBiomes(this::getData)) {
|
||||
maxChunks = Math.max(maxChunks, maxFrom(biome.getStructures()));
|
||||
}
|
||||
maxChunks = Math.max(maxChunks, maxFrom(dimension.getStructures()));
|
||||
|
||||
return maxChunks * 16;
|
||||
}
|
||||
|
||||
private int maxFrom(KList<IrisStructurePlacement> placements) {
|
||||
int max = 0;
|
||||
for (IrisStructurePlacement placement : placements) {
|
||||
if (placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) {
|
||||
continue;
|
||||
}
|
||||
for (String key : placement.getStructures()) {
|
||||
IrisStructure structure = art.arcane.iris.core.loader.IrisData.loadAnyStructure(key, getData());
|
||||
if (structure != null) {
|
||||
max = Math.max(max, structure.getMaxSizeChunks());
|
||||
}
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
}
|
||||
+105
-2
@@ -27,6 +27,8 @@ import art.arcane.iris.engine.mantle.ComponentFlag;
|
||||
import art.arcane.iris.engine.mantle.EngineMantle;
|
||||
import art.arcane.iris.engine.mantle.IrisMantleComponent;
|
||||
import art.arcane.iris.engine.mantle.MantleWriter;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.*;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
@@ -46,6 +48,7 @@ import art.arcane.iris.util.project.noise.NoiseType;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import org.bukkit.util.BlockVector;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
@@ -111,7 +114,7 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
+ " carvedAtCenter=" + carvedBlocks
|
||||
+ " biomeCarvingObjects=" + caveBiome.getCarvingObjects().size()
|
||||
+ " regionCarvingObjects=" + region.getCarvingObjects().size()
|
||||
+ " sameBiome=" + (caveBiome == surfaceBiome || caveBiome.getLoadKey().equals(surfaceBiome.getLoadKey())));
|
||||
+ " sameBiome=" + (caveBiome == surfaceBiome || java.util.Objects.equals(caveBiome.getLoadKey(), surfaceBiome.getLoadKey())));
|
||||
}
|
||||
if (traceRegen) {
|
||||
Iris.info("Regen object layer start: chunk=" + x + "," + z
|
||||
@@ -590,7 +593,9 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
AtomicBoolean wrotePlacementData = new AtomicBoolean(false);
|
||||
|
||||
try {
|
||||
int result = object.place(x, y, z, writer, effectivePlacement, rng, (b, data) -> {
|
||||
int caveCeiling = findCaveCeiling(writer, x, y, z);
|
||||
IObjectPlacer clampedPlacer = new CeilingClampedPlacer(writer, caveCeiling);
|
||||
int result = object.place(x, y, z, clampedPlacer, effectivePlacement, rng, (b, data) -> {
|
||||
wrotePlacementData.set(true);
|
||||
String marker = placementMarker(object, id, "cave");
|
||||
if (marker != null) {
|
||||
@@ -901,6 +906,104 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
return anchors.get(anchors.size() - 1);
|
||||
}
|
||||
|
||||
private int findCaveCeiling(MantleWriter writer, int x, int anchorY, int z) {
|
||||
Engine engine = getEngineMantle().getEngine();
|
||||
int surfaceY = engine.getHeight(x, z);
|
||||
int maxScan = Math.min(engine.getHeight() - 1, Math.max(0, surfaceY));
|
||||
for (int sy = anchorY + 1; sy <= maxScan; sy++) {
|
||||
if (!writer.isCarved(x, sy, z)) {
|
||||
return sy;
|
||||
}
|
||||
}
|
||||
return maxScan;
|
||||
}
|
||||
|
||||
private static final class CeilingClampedPlacer implements IObjectPlacer {
|
||||
private final IObjectPlacer delegate;
|
||||
private final int maxY;
|
||||
|
||||
private CeilingClampedPlacer(IObjectPlacer delegate, int maxY) {
|
||||
this.delegate = delegate;
|
||||
this.maxY = maxY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data) {
|
||||
return delegate.getHighest(x, z, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getHighest(int x, int z, IrisData data, boolean ignoreFluid) {
|
||||
return delegate.getHighest(x, z, data, ignoreFluid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(int x, int y, int z, BlockData d) {
|
||||
if (y >= maxY) {
|
||||
return;
|
||||
}
|
||||
delegate.set(x, y, z, d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockData get(int x, int y, int z) {
|
||||
return delegate.get(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPreventingDecay() {
|
||||
return delegate.isPreventingDecay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCarved(int x, int y, int z) {
|
||||
return delegate.isCarved(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSolid(int x, int y, int z) {
|
||||
return delegate.isSolid(x, y, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnderwater(int x, int z) {
|
||||
return delegate.isUnderwater(x, z);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFluidHeight() {
|
||||
return delegate.getFluidHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDebugSmartBore() {
|
||||
return delegate.isDebugSmartBore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void setData(int xx, int yy, int zz, T data) {
|
||||
delegate.setData(xx, yy, zz, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getData(int xx, int yy, int zz, Class<T> t) {
|
||||
return delegate.getData(xx, yy, zz, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTile(int xx, int yy, int zz, TileData tile) {
|
||||
if (yy >= maxY) {
|
||||
return;
|
||||
}
|
||||
delegate.setTile(xx, yy, zz, tile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Engine getEngine() {
|
||||
return delegate.getEngine();
|
||||
}
|
||||
}
|
||||
|
||||
private int findCaveAnchorY(MantleWriter writer, RNG rng, int x, int z, IrisCaveAnchorMode anchorMode, int anchorScanStep, int objectMinDepthBelowSurface, KMap<Long, KList<Integer>> anchorCache) {
|
||||
long key = Cache.key(x, z);
|
||||
KList<Integer> anchors = anchorCache.computeIfAbsent(key, (k) -> scanCaveAnchorColumn(writer, anchorMode, anchorScanStep, objectMinDepthBelowSurface, x, z));
|
||||
|
||||
@@ -166,6 +166,9 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
@ArrayType(min = 1, type = IrisObjectPlacement.class)
|
||||
@Desc("Objects define what schematics (iob files) iris will place in this biome")
|
||||
private KList<IrisObjectPlacement> objects = new KList<>();
|
||||
@ArrayType(min = 1, type = IrisStructurePlacement.class)
|
||||
@Desc("Structures define jigsaw or vanilla/datapack structures iris will place in this biome")
|
||||
private KList<IrisStructurePlacement> structures = new KList<>();
|
||||
@ArrayType(min = 1, type = IrisFloatingChildBiomes.class)
|
||||
@Desc("Floating child biomes that procedurally generate above this biome's terrain. Each entry references a target biome whose layers, decorators, and objects drive the floating island's visual design, while the config here drives size, shape, altitude, rarity, and water level. Multiple entries are supported and selected by rarity per column unless mergeFloatingChildBiomes is enabled.")
|
||||
private KList<IrisFloatingChildBiomes> floatingChildBiomes = new KList<>();
|
||||
|
||||
@@ -255,6 +255,11 @@ public class IrisDimension extends IrisRegistrant {
|
||||
@Desc("Collection of ores to be generated")
|
||||
@ArrayType(type = IrisOreGenerator.class, min = 1)
|
||||
private KList<IrisOreGenerator> ores = new KList<>();
|
||||
@Desc("Dimension-wide structure placements (jigsaw or vanilla/datapack), independent of biome/region placements")
|
||||
@ArrayType(type = IrisStructurePlacement.class, min = 1)
|
||||
private KList<IrisStructurePlacement> structures = new KList<>();
|
||||
@Desc("Controls native vanilla & datapack structure generation for this dimension. Defaults to ALL_ON.")
|
||||
private IrisVanillaStructureControl vanillaStructures = new IrisVanillaStructureControl();
|
||||
@MinNumber(0)
|
||||
@MaxNumber(318)
|
||||
@Desc("The Subterrain Fluid Layer Height")
|
||||
@@ -585,7 +590,9 @@ public class IrisDimension extends IrisRegistrant {
|
||||
{
|
||||
"pack": {
|
||||
"description": "Iris Data Pack. This pack contains all installed Iris Packs' resources.",
|
||||
"pack_format": {}
|
||||
"pack_format": {},
|
||||
"min_format": {},
|
||||
"max_format": {}
|
||||
}
|
||||
}
|
||||
""".replace("{}", INMS.get().getDataVersion().getPackFormat() + "");
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("A jigsaw connection point on a piece. When two connectors face each other and their names match, the assembler may join the pieces.")
|
||||
@Data
|
||||
public class IrisJigsawConnector {
|
||||
@Required
|
||||
@Desc("The position of this connector relative to the piece object's origin (0,0,0 is the lowest-corner of the object).")
|
||||
private IrisPosition position = new IrisPosition();
|
||||
|
||||
@Required
|
||||
@Desc("The direction this connector faces. The connecting piece is placed on this side.")
|
||||
private IrisDirection direction = IrisDirection.NORTH_NEGATIVE_Z;
|
||||
|
||||
@Required
|
||||
@RegistryListResource(IrisJigsawPool.class)
|
||||
@Desc("The jigsaw pool to draw the connecting piece from.")
|
||||
private String pool = "";
|
||||
|
||||
@Desc("The name of this connector. A connector only joins to a connector on another piece whose targetName equals this name (and whose name equals this connector's targetName).")
|
||||
private String name = "";
|
||||
|
||||
@Desc("The name this connector wants to connect to on the other piece.")
|
||||
private String targetName = "";
|
||||
|
||||
@Desc("How the connecting piece may be rotated relative to this connector.")
|
||||
private JigsawJoint joint = JigsawJoint.ROLLABLE;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("A jigsaw piece. A piece is a single Iris object plus the connection points (connectors) that let the assembler attach other pieces to it.")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class IrisJigsawPiece extends IrisRegistrant {
|
||||
@Required
|
||||
@RegistryListResource(IrisObject.class)
|
||||
@Desc("The object (schematic) that makes up this piece.")
|
||||
private String object = "";
|
||||
|
||||
@ArrayType(type = IrisJigsawConnector.class, min = 1)
|
||||
@Desc("The jigsaw connection points on this piece.")
|
||||
private KList<IrisJigsawConnector> connectors = new KList<>();
|
||||
|
||||
@Desc("If true, this piece may be rotated around the Y axis when placed so the assembler can match its connectors. Disable for pieces that must keep a fixed orientation.")
|
||||
private boolean rotatable = true;
|
||||
|
||||
@Override
|
||||
public String getFolderName() {
|
||||
return "jigsaw-pieces";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Jigsaw Piece";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scanForErrors(JSONObject p, VolmitSender sender) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("A weighted reference to a jigsaw piece inside a jigsaw pool.")
|
||||
@Data
|
||||
public class IrisJigsawPieceEntry {
|
||||
@Required
|
||||
@RegistryListResource(IrisJigsawPiece.class)
|
||||
@Desc("The jigsaw piece this entry refers to.")
|
||||
private String piece = "";
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("The relative weight of this piece within its pool. Higher weights are chosen more often.")
|
||||
private int weight = 1;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("A jigsaw pool. A pool is a weighted set of pieces the assembler chooses from when a connector targets this pool.")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class IrisJigsawPool extends IrisRegistrant {
|
||||
@ArrayType(type = IrisJigsawPieceEntry.class, min = 1)
|
||||
@Desc("The weighted pieces in this pool.")
|
||||
private KList<IrisJigsawPieceEntry> pieces = new KList<>();
|
||||
|
||||
@RegistryListResource(IrisJigsawPool.class)
|
||||
@Desc("The pool to fall back to when the structure's max depth is reached. Use this for terminal pieces (caps, dead ends). Leave empty to simply stop expanding.")
|
||||
private String fallback = "";
|
||||
|
||||
@Override
|
||||
public String getFolderName() {
|
||||
return "jigsaw-pools";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Jigsaw Pool";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scanForErrors(JSONObject p, VolmitSender sender) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -595,6 +595,16 @@ public class IrisObject extends IrisRegistrant {
|
||||
}
|
||||
}
|
||||
|
||||
public void setUnsignedTile(int x, int y, int z, TileData tile) {
|
||||
Vector3i v = getSigned(x, y, z);
|
||||
|
||||
if (tile == null) {
|
||||
states.remove(v);
|
||||
} else {
|
||||
states.put(v, tile);
|
||||
}
|
||||
}
|
||||
|
||||
public void setUnsigned(int x, int y, int z, Block block, boolean legacy) {
|
||||
Vector3i v = getSigned(x, y, z);
|
||||
|
||||
|
||||
@@ -92,6 +92,9 @@ public class IrisRegion extends IrisRegistrant implements IRare {
|
||||
@ArrayType(min = 1, type = IrisObjectPlacement.class)
|
||||
@Desc("Objects define what schematics (iob files) iris will place in this region")
|
||||
private KList<IrisObjectPlacement> objects = new KList<>();
|
||||
@ArrayType(min = 1, type = IrisStructurePlacement.class)
|
||||
@Desc("Structures define jigsaw or vanilla/datapack structures iris will place in this region")
|
||||
private KList<IrisStructurePlacement> structures = new KList<>();
|
||||
@MinNumber(0)
|
||||
@Desc("The min shore height")
|
||||
private double shoreHeightMin = 1.2;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.iris.core.loader.IrisRegistrant;
|
||||
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.Required;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.iris.util.common.plugin.VolmitSender;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("A jigsaw structure. Starting from a pool, the assembler places a start piece and recursively attaches more pieces through matching connectors until it reaches the max depth or size. Pieces are Iris objects, so they carry the full object placement and block-control feature set.")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
public class IrisStructure extends IrisRegistrant {
|
||||
@Required
|
||||
@RegistryListResource(IrisJigsawPool.class)
|
||||
@Desc("The pool the assembler draws the start piece from.")
|
||||
private String startPool = "";
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(30)
|
||||
@Desc("The maximum jigsaw recursion depth. Larger values allow larger structures (e.g. villages) at the cost of generation time.")
|
||||
private int maxDepth = 7;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(32)
|
||||
@Desc("The maximum radius of the structure in chunks from its start piece. Acts as a hard bound on assembly size and collision checks.")
|
||||
private int maxSizeChunks = 8;
|
||||
|
||||
@Desc("The place mode used when stamping each piece object into the world.")
|
||||
private ObjectPlaceMode placeMode = ObjectPlaceMode.STRUCTURE_PIECE;
|
||||
|
||||
@ArrayType(min = 1, type = IrisObjectReplace.class)
|
||||
@Desc("Find-and-replace block edits applied to every piece of this structure, using the exact same syntax as an object's 'edit'. For example, replace stone bricks with obsidian.")
|
||||
private KList<IrisObjectReplace> edit = new KList<>();
|
||||
|
||||
@ArrayType(type = String.class, min = 1)
|
||||
@RegistryListResource(IrisLootTable.class)
|
||||
@Desc("Loot tables applied to containers placed by this structure's pieces.")
|
||||
private KList<String> loot = new KList<>();
|
||||
|
||||
@Desc("If this structure was generated by importing a vanilla or datapack structure, this is that structure's key (provenance). Empty for hand-authored structures.")
|
||||
private String vanillaSource = "";
|
||||
|
||||
@Override
|
||||
public String getFolderName() {
|
||||
return "structures";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeName() {
|
||||
return "Structure";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scanForErrors(JSONObject p, VolmitSender sender) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.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.RegistryListVanillaStructure;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("Attaches structures to a biome, region or dimension and controls where and how often they generate. This is independent of a structure's own native generation, so you can add custom placements in tandem with native generation or instead of it.")
|
||||
@Data
|
||||
public class IrisStructurePlacement {
|
||||
@ArrayType(type = String.class, min = 1)
|
||||
@RegistryListResource(IrisStructure.class)
|
||||
@Desc("Iris structures to place here (route IRIS_PLACED). Use this for imported or hand-authored structures with full block control.")
|
||||
private KList<String> structures = new KList<>();
|
||||
|
||||
@ArrayType(type = String.class, min = 1)
|
||||
@RegistryListVanillaStructure
|
||||
@Desc("Vanilla or datapack structure keys to place here faithfully (route NATIVE_AT_POINT), e.g. minecraft:igloo. Real loot/mobs/variety, no block editing.")
|
||||
private KList<String> vanilla = new KList<>();
|
||||
|
||||
@Desc("How the placement is realized. Defaults sensibly by which list is populated, but you may set it explicitly.")
|
||||
private StructurePlacementRoute route = StructurePlacementRoute.IRIS_PLACED;
|
||||
|
||||
@Desc("How start positions are scattered.")
|
||||
private StructureDistribution distribution = StructureDistribution.RANDOM_SPREAD;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(4096)
|
||||
@Desc("RANDOM_SPREAD only: the grid cell size in chunks. One placement attempt per cell. Larger = rarer.")
|
||||
private int spacing = 32;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("RANDOM_SPREAD only: the minimum chunk separation between placements within the grid. Must be smaller than spacing.")
|
||||
private int separation = 8;
|
||||
|
||||
@Desc("RANDOM_SPREAD only: a salt mixed into the placement RNG so different structures using the same spacing do not stack.")
|
||||
private int salt = 165745296;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("DENSITY only: the per-chunk probability (0-1) of a placement starting in a chunk.")
|
||||
private double density = 0.02;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("CONCENTRIC_RINGS only: the number of placements distributed across the rings.")
|
||||
private int ringCount = 128;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("CONCENTRIC_RINGS only: the ring spacing in chunks.")
|
||||
private int ringDistance = 32;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("CONCENTRIC_RINGS only: how many placements share each ring before moving outward.")
|
||||
private int ringSpread = 3;
|
||||
|
||||
@Desc("IRIS_PLACED only: rotation applied to the placed structure.")
|
||||
private IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
|
||||
@Desc("IRIS_PLACED only: translation applied to the placed structure.")
|
||||
private IrisObjectTranslate translate = new IrisObjectTranslate();
|
||||
|
||||
@Desc("IRIS_PLACED only: scale applied to the placed structure.")
|
||||
private IrisObjectScale scale = new IrisObjectScale();
|
||||
|
||||
@Desc("The minimum world Y this structure may start at.")
|
||||
private int minHeight = -2032;
|
||||
|
||||
@Desc("The maximum world Y this structure may start at.")
|
||||
private int maxHeight = 2032;
|
||||
|
||||
@Desc("If false, this placement is skipped underwater.")
|
||||
private boolean underwater = false;
|
||||
}
|
||||
@@ -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.object;
|
||||
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Accessors(chain = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Desc("Controls native vanilla & datapack structure generation for this dimension. The default mode is ALL_ON (everything generates).")
|
||||
@Data
|
||||
public class IrisVanillaStructureControl {
|
||||
@Desc("Master toggle. ALL_ON generates every vanilla & datapack structure except those in 'disabled'. ALL_OFF (or CUSTOM) generates nothing except those in 'enabled'.")
|
||||
private VanillaStructureMode mode = VanillaStructureMode.ALL_ON;
|
||||
|
||||
@ArrayType(type = String.class, min = 1)
|
||||
@RegistryListVanillaStructure
|
||||
@Desc("Structure keys to turn OFF while mode is ALL_ON, e.g. 'minecraft:stronghold'. A namespace:path prefix also matches, so 'minecraft:village' disables every village.")
|
||||
private KList<String> disabled = new KList<>();
|
||||
|
||||
@ArrayType(type = String.class, min = 1)
|
||||
@RegistryListVanillaStructure
|
||||
@Desc("Structure keys to turn ON while mode is ALL_OFF (or CUSTOM), e.g. 'minecraft:village_plains'. A namespace:path prefix also matches.")
|
||||
private KList<String> enabled = new KList<>();
|
||||
|
||||
public boolean active() {
|
||||
return mode == VanillaStructureMode.ALL_ON || !enabled.isEmpty();
|
||||
}
|
||||
|
||||
public boolean shouldGenerate(String key) {
|
||||
if (mode == VanillaStructureMode.ALL_ON) {
|
||||
return !matches(disabled, key);
|
||||
}
|
||||
return matches(enabled, key);
|
||||
}
|
||||
|
||||
private boolean matches(KList<String> list, String key) {
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
for (String entry : list) {
|
||||
if (entry == null || entry.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (key.equals(entry) || key.startsWith(entry)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("How a jigsaw connection may be rotated when matching two pieces together.")
|
||||
public enum JigsawJoint {
|
||||
@Desc("The connecting piece may rotate freely around the connection axis. Use this for vertical connectors (floors, ceilings) and any joint where roll does not matter.")
|
||||
ROLLABLE,
|
||||
|
||||
@Desc("The connecting piece's rotation is locked to face the connector. Use this for horizontal connectors where the attached piece must align to a specific facing (doorways, street ends).")
|
||||
ALIGNED
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import org.bukkit.block.sign.Side;
|
||||
import org.bukkit.block.sign.SignSide;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.loot.LootTable;
|
||||
import org.bukkit.loot.Lootable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -37,7 +39,8 @@ public class LegacyTileData extends TileData {
|
||||
private static final Map<Integer, Pair<Builder, IOFunction<DataInputStream, Handler>>> legacy = Map.of(
|
||||
0, new Pair<>(SignHandler::fromBukkit, SignHandler::new),
|
||||
1, new Pair<>(SpawnerHandler::fromBukkit, SpawnerHandler::new),
|
||||
2, new Pair<>(BannerHandler::fromBukkit, BannerHandler::new));
|
||||
2, new Pair<>(BannerHandler::fromBukkit, BannerHandler::new),
|
||||
3, new Pair<>(LootableHandler::fromBukkit, LootableHandler::new));
|
||||
private static final AtomicCache<Tag<Material>> SIGNS = new AtomicCache<>();
|
||||
private final int id;
|
||||
private final Handler handler;
|
||||
@@ -338,6 +341,75 @@ public class LegacyTileData extends TileData {
|
||||
}
|
||||
}
|
||||
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@AllArgsConstructor
|
||||
private static class LootableHandler implements Handler {
|
||||
private final Material material;
|
||||
private final NamespacedKey lootTable;
|
||||
private final long seed;
|
||||
|
||||
private LootableHandler(DataInputStream in) throws IOException {
|
||||
String materialKey = in.readUTF();
|
||||
NamespacedKey resolvedMaterial = NamespacedKey.fromString(materialKey);
|
||||
Material resolved = resolvedMaterial == null ? null : Registry.MATERIAL.get(resolvedMaterial);
|
||||
if (resolved == null) {
|
||||
resolved = Material.matchMaterial(materialKey);
|
||||
}
|
||||
material = resolved == null ? Material.CHEST : resolved;
|
||||
String tableKey = in.readUTF();
|
||||
lootTable = tableKey.isEmpty() ? null : NamespacedKey.fromString(tableKey);
|
||||
seed = in.readLong();
|
||||
}
|
||||
|
||||
private static LootableHandler fromBukkit(BlockState blockState, Material type) {
|
||||
if (!(blockState instanceof Lootable lootable)) {
|
||||
return null;
|
||||
}
|
||||
LootTable table = lootable.getLootTable();
|
||||
if (table == null) {
|
||||
return null;
|
||||
}
|
||||
NamespacedKey key = table.getKey();
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
return new LootableHandler(type, key, lootable.getSeed());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Material getMaterial() {
|
||||
return material;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isApplicable(BlockData data) {
|
||||
return data.getMaterial() == material;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toBinary(DataOutputStream out) throws IOException {
|
||||
NamespacedKey key = KeyedType.getKey(material);
|
||||
out.writeUTF(key == null ? material.name() : key.toString());
|
||||
out.writeUTF(lootTable == null ? "" : lootTable.toString());
|
||||
out.writeLong(seed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toBukkit(Block block) {
|
||||
if (lootTable == null || !(block.getState() instanceof Lootable lootable)) {
|
||||
return;
|
||||
}
|
||||
LootTable table = Bukkit.getLootTable(lootTable);
|
||||
if (table == null) {
|
||||
return;
|
||||
}
|
||||
lootable.setLootTable(table);
|
||||
lootable.setSeed(seed);
|
||||
((BlockState) lootable).update(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static Tag<Material> signsTag() {
|
||||
return SIGNS.aquire(() -> {
|
||||
var signs = Bukkit.getTag("blocks", NamespacedKey.minecraft("all_signs"), Material.class);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Controls how structure start positions are scattered across the world.")
|
||||
public enum StructureDistribution {
|
||||
@Desc("Vanilla-style spacing/separation grid. One placement attempt per grid cell, randomly offset within it by the separation. Use spacing, separation and salt.")
|
||||
RANDOM_SPREAD,
|
||||
|
||||
@Desc("A per-chunk probability roll, like object density. Use density (0-1).")
|
||||
DENSITY,
|
||||
|
||||
@Desc("Concentric rings around the world origin, like vanilla strongholds. Use rings.")
|
||||
CONCENTRIC_RINGS
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("How a custom structure placement is realized in the world.")
|
||||
public enum StructurePlacementRoute {
|
||||
@Desc("Place an imported or hand-authored Iris structure through the object pipeline. Full block & content control. References structures (IrisStructure keys).")
|
||||
IRIS_PLACED,
|
||||
|
||||
@Desc("Invoke the real vanilla/datapack structure generation at the chosen coordinates (faithful loot, mobs, jigsaw variety). No Iris block editing. References vanilla (structure keys).")
|
||||
NATIVE_AT_POINT
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Master toggle for vanilla & datapack structure generation in a dimension.")
|
||||
public enum VanillaStructureMode {
|
||||
@Desc("All vanilla & datapack structures generate normally. This is the default. Per-set overrides may still disable individual sets.")
|
||||
ALL_ON,
|
||||
|
||||
@Desc("All vanilla & datapack structures are suppressed. Per-set overrides may still enable individual sets.")
|
||||
ALL_OFF,
|
||||
|
||||
@Desc("Nothing generates unless explicitly enabled via a per-set override with enabled=true.")
|
||||
CUSTOM
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.annotations;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.*;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Retention(RUNTIME)
|
||||
@Target({PARAMETER, TYPE, FIELD})
|
||||
public @interface RegistryListVanillaStructure {
|
||||
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.annotations;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import static java.lang.annotation.ElementType.*;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Retention(RUNTIME)
|
||||
@Target({PARAMETER, TYPE, FIELD})
|
||||
public @interface RegistryListVanillaStructureSet {
|
||||
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.framework.EngineTarget;
|
||||
import art.arcane.iris.engine.framework.GenerationSessionException;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
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;
|
||||
@@ -816,7 +817,15 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
|
||||
|
||||
@Override
|
||||
public boolean shouldGenerateStructures() {
|
||||
return false;
|
||||
try {
|
||||
Engine e = this.engine;
|
||||
if (e == null) {
|
||||
return true;
|
||||
}
|
||||
return e.getDimension().getVanillaStructures().getMode() != VanillaStructureMode.ALL_OFF;
|
||||
} catch (Throwable t) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -30,6 +30,17 @@ public class WorldLifecycleStagingTest {
|
||||
assertSame(generator, WorldLifecycleStaging.consumeStemGenerator("world"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stagedStemGeneratorIsConsumedWhenLevelIdDiffersFromWorldName() {
|
||||
ChunkGenerator generator = mock(ChunkGenerator.class);
|
||||
|
||||
WorldLifecycleStaging.stageStemGenerator("iris_world", generator);
|
||||
|
||||
assertSame(generator, WorldLifecycleStaging.consumeStemGenerator("world"));
|
||||
assertNull(WorldLifecycleStaging.consumeStemGenerator("iris_world"));
|
||||
assertNull(WorldLifecycleStaging.consumeStemGenerator("world"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clearAllRemovesGeneratorBiomeAndStemState() {
|
||||
ChunkGenerator generator = mock(ChunkGenerator.class);
|
||||
|
||||
@@ -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.engine.framework;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class StructurePlacementGridTest {
|
||||
@Test
|
||||
public void randomSpreadStartIsDeterministic() {
|
||||
boolean first = StructurePlacementGrid.randomSpreadStart(5, 7, 32, 8, 165745296, 12345L);
|
||||
boolean second = StructurePlacementGrid.randomSpreadStart(5, 7, 32, 8, 165745296, 12345L);
|
||||
assertEquals(first, second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void randomSpreadStartYieldsExactlyOneStartPerGridCell() {
|
||||
int spacing = 32;
|
||||
int separation = 8;
|
||||
int salt = 165745296;
|
||||
long seed = 99L;
|
||||
int count = 0;
|
||||
for (int cx = 0; cx < spacing; cx++) {
|
||||
for (int cz = 0; cz < spacing; cz++) {
|
||||
if (StructurePlacementGrid.randomSpreadStart(cx, cz, spacing, separation, salt, seed)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals(1, count);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void randomSpreadStartVariesAcrossCells() {
|
||||
int spacing = 32;
|
||||
int separation = 8;
|
||||
int salt = 165745296;
|
||||
long seed = 99L;
|
||||
int starts = 0;
|
||||
for (int cellX = 0; cellX < 4; cellX++) {
|
||||
for (int cellZ = 0; cellZ < 4; cellZ++) {
|
||||
for (int x = 0; x < spacing; x++) {
|
||||
for (int z = 0; z < spacing; z++) {
|
||||
int cx = cellX * spacing + x;
|
||||
int cz = cellZ * spacing + z;
|
||||
if (StructurePlacementGrid.randomSpreadStart(cx, cz, spacing, separation, salt, seed)) {
|
||||
starts++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals(16, starts);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mixIsDeterministicAndSensitiveToInputs() {
|
||||
assertEquals(StructurePlacementGrid.mix(1L, 2, 3, 4), StructurePlacementGrid.mix(1L, 2, 3, 4));
|
||||
assertNotEquals(StructurePlacementGrid.mix(1L, 2, 3, 4), StructurePlacementGrid.mix(1L, 2, 3, 5));
|
||||
assertNotEquals(StructurePlacementGrid.mix(1L, 2, 3, 4), StructurePlacementGrid.mix(2L, 2, 3, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void differentSaltsShiftStartPositions() {
|
||||
int spacing = 32;
|
||||
int separation = 8;
|
||||
long seed = 7L;
|
||||
int matches = 0;
|
||||
int total = 0;
|
||||
for (int cx = 0; cx < spacing; cx++) {
|
||||
for (int cz = 0; cz < spacing; cz++) {
|
||||
boolean a = StructurePlacementGrid.randomSpreadStart(cx, cz, spacing, separation, 10387312, seed);
|
||||
boolean b = StructurePlacementGrid.randomSpreadStart(cx, cz, spacing, separation, 165745296, seed);
|
||||
if (a) {
|
||||
total++;
|
||||
}
|
||||
if (a && b) {
|
||||
matches++;
|
||||
}
|
||||
}
|
||||
}
|
||||
assertEquals(1, total);
|
||||
assertTrue(matches <= total);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ grgit = "5.3.2" # https://github.com/ajoberstar/grgit
|
||||
|
||||
# Core Libraries
|
||||
lombok = "1.18.44"
|
||||
spigot = "1.21.11-R0.1-SNAPSHOT" # https://hub.spigotmc.org/nexus/repository/snapshots/org/spigotmc/spigot-api/maven-metadata.xml
|
||||
spigot = "26.1.2-R0.1-SNAPSHOT" # https://hub.spigotmc.org/nexus/repository/snapshots/org/spigotmc/spigot-api/maven-metadata.xml
|
||||
log4j = "2.19.0" # https://central.sonatype.com/artifact/org.apache.logging.log4j/log4j-api
|
||||
adventure-api = "4.24.0" # https://github.com/KyoriPowered/adventure
|
||||
adventure-platform = "4.4.1" # https://github.com/KyoriPowered/adventure-platform
|
||||
|
||||
+139
-1
@@ -2,10 +2,13 @@ package art.arcane.iris.core.nms.v1_21_R7;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisVanillaStructureControl;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
@@ -23,7 +26,19 @@ import net.minecraft.world.level.levelgen.*;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import java.util.stream.Collectors;
|
||||
import art.arcane.iris.engine.framework.StructurePlacementGrid;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.StructurePlacementRoute;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||
@@ -49,7 +64,10 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
|
||||
return null;
|
||||
if (!vanillaControl().active()) {
|
||||
return null;
|
||||
}
|
||||
return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,6 +94,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
|
||||
@Override
|
||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||
if (!vanillaControl().active()) {
|
||||
return;
|
||||
}
|
||||
delegate.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
||||
}
|
||||
|
||||
private IrisVanillaStructureControl vanillaControl() {
|
||||
return engine.getDimension().getVanillaStructures();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -126,9 +152,121 @@ public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
|
||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
if (vanillaControl().active()) {
|
||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
}
|
||||
placeIrisNativeStructures(generatoraccessseed, ichunkaccess);
|
||||
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||
}
|
||||
|
||||
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
|
||||
if (!structureManager.shouldGenerateStructures()) {
|
||||
return;
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
|
||||
BlockPos origin = sectionPos.origin();
|
||||
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
Map<Integer, List<Structure>> byStep = registry.stream().collect(Collectors.groupingBy(s -> s.step().ordinal()));
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
|
||||
BoundingBox area = writableArea(chunk);
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
IrisVanillaStructureControl control = vanillaControl();
|
||||
for (int step = 0; step < steps; step++) {
|
||||
int index = 0;
|
||||
for (Structure structure : byStep.getOrDefault(step, List.of())) {
|
||||
Object id = registry.getKey(structure);
|
||||
if (control.shouldGenerate(id == null ? null : id.toString())) {
|
||||
random.setFeatureSeed(decoSeed, index, step);
|
||||
try {
|
||||
structureManager.startsForStructure(sectionPos, structure)
|
||||
.forEach(start -> start.placeInChunk(world, structureManager, this, random, area, chunkPos));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BoundingBox writableArea(ChunkAccess chunk) {
|
||||
ChunkPos cp = chunk.getPos();
|
||||
int i = cp.getMinBlockX();
|
||||
int j = cp.getMinBlockZ();
|
||||
int minY = getMinY() + 1;
|
||||
int maxY = getMinY() + engine.getHeight() - 1;
|
||||
return new BoundingBox(i, minY, j, i + 15, maxY, j + 15);
|
||||
}
|
||||
|
||||
private void placeIrisNativeStructures(WorldGenLevel world, ChunkAccess chunk) {
|
||||
if (!(world instanceof WorldGenRegion region)) {
|
||||
return;
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
int cx = chunkPos.getMinBlockX() >> 4;
|
||||
int cz = chunkPos.getMinBlockZ() >> 4;
|
||||
int bx = 8 + (cx << 4);
|
||||
int bz = 8 + (cz << 4);
|
||||
IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(bx, bz);
|
||||
IrisRegion ireg = engine.getComplex().getRegionStream().get(bx, bz);
|
||||
KList<IrisStructurePlacement> placements = new KList<>();
|
||||
if (biome != null) {
|
||||
placements.addAll(biome.getStructures());
|
||||
}
|
||||
if (ireg != null) {
|
||||
placements.addAll(ireg.getStructures());
|
||||
}
|
||||
placements.addAll(engine.getDimension().getStructures());
|
||||
if (placements.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long seed = world.getSeed();
|
||||
RNG rng = new RNG(Cache.key(cx, cz) + seed);
|
||||
ServerLevel level = region.getLevel();
|
||||
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
RandomState randomState = level.getChunkSource().randomState();
|
||||
StructureTemplateManager templateManager = level.getStructureManager();
|
||||
StructureManager structureManager = level.structureManager();
|
||||
BiomeSource biomeSource = getBiomeSource();
|
||||
ResourceKey<Level> levelKey = level.dimension();
|
||||
int minY = getMinY() + 1;
|
||||
int maxY = getMinY() + engine.getHeight() - 1;
|
||||
|
||||
for (IrisStructurePlacement placement : placements) {
|
||||
if (placement.getRoute() != StructurePlacementRoute.NATIVE_AT_POINT || placement.getVanilla().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, rng)) {
|
||||
continue;
|
||||
}
|
||||
String key = placement.getVanilla().get(rng.i(0, placement.getVanilla().size() - 1)).toLowerCase();
|
||||
try {
|
||||
String[] parts = key.split(":", 2);
|
||||
Identifier id = parts.length > 1 ? Identifier.fromNamespaceAndPath(parts[0], parts[1]) : Identifier.fromNamespaceAndPath("minecraft", parts[0]);
|
||||
Holder<Structure> holder = registry.getOrThrow(ResourceKey.create(Registries.STRUCTURE, id));
|
||||
Structure structure = holder.value();
|
||||
StructureStart start = structure.generate(holder, levelKey, world.registryAccess(), this, biomeSource, randomState, templateManager, seed, chunkPos, 0, level, b -> true);
|
||||
if (start == null || !start.isValid()) {
|
||||
continue;
|
||||
}
|
||||
BoundingBox box = start.getBoundingBox();
|
||||
WorldgenRandom placeRandom = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
for (int scx = box.minX() >> 4; scx <= box.maxX() >> 4; scx++) {
|
||||
for (int scz = box.minZ() >> 4; scz <= box.maxZ() >> 4; scz++) {
|
||||
ChunkPos target = new ChunkPos(scx, scz);
|
||||
BoundingBox chunkBox = new BoundingBox(target.getMinBlockX(), minY, target.getMinBlockZ(), target.getMaxBlockX(), maxY, target.getMaxBlockZ());
|
||||
start.placeInChunk(world, structureManager, this, placeRandom, chunkBox, target);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
|
||||
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
|
||||
|
||||
@@ -61,7 +61,6 @@ import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import net.minecraft.world.level.storage.ServerLevelData;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
@@ -391,6 +390,28 @@ public class NMSBinding implements INMSBinding {
|
||||
return biomes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getStructureKeys() {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
registry().lookupOrThrow(Registries.STRUCTURE).keySet().forEach(k -> keys.add(k.toString()));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getStructureSetKeys() {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
registry().lookupOrThrow(Registries.STRUCTURE_SET).keySet().forEach(k -> keys.add(k.toString()));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBukkit() {
|
||||
return true;
|
||||
@@ -731,9 +752,9 @@ public class NMSBinding implements INMSBinding {
|
||||
Iris.info("Injecting Bukkit");
|
||||
var buddy = new ByteBuddy();
|
||||
buddy.redefine(ServerLevel.class)
|
||||
.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor().and(ElementMatchers.takesArguments(
|
||||
MinecraftServer.class, Executor.class, LevelStorageSource.LevelStorageAccess.class, ServerLevelData.class,
|
||||
ResourceKey.class, LevelStem.class, boolean.class, long.class, List.class, boolean.class))))
|
||||
.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||
.and(ElementMatchers.takesArgument(5, LevelStem.class))))
|
||||
.make()
|
||||
.load(ServerLevel.class.getClassLoader(), Agent.installed());
|
||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||
@@ -807,8 +828,7 @@ public class NMSBinding implements INMSBinding {
|
||||
static void enter(
|
||||
@Advice.Argument(0) MinecraftServer server,
|
||||
@Advice.Argument(2) LevelStorageSource.LevelStorageAccess levelStorageAccess,
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem,
|
||||
@Advice.Argument(3) ServerLevelData levelData
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem
|
||||
) {
|
||||
if (levelStorageAccess == null)
|
||||
return;
|
||||
@@ -833,11 +853,21 @@ public class NMSBinding implements INMSBinding {
|
||||
.getClassLoader())
|
||||
.getDeclaredMethod("get")
|
||||
.invoke(null);
|
||||
if (!(bindings instanceof INMSBinding binding)) {
|
||||
if (bindings == null) {
|
||||
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
|
||||
}
|
||||
|
||||
Object resolvedStem = binding.createRuntimeLevelStem(server.registryAccess(), gen);
|
||||
java.lang.reflect.Method stemMethod = null;
|
||||
for (java.lang.reflect.Method candidate : bindings.getClass().getMethods()) {
|
||||
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
|
||||
stemMethod = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stemMethod == null) {
|
||||
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
|
||||
}
|
||||
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
|
||||
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
|
||||
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
package art.arcane.iris.core.nms.v26_1_R1;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBiomeCustom;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
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.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftServer;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class CustomBiomeSource extends BiomeSource {
|
||||
private static final int NOISE_BIOME_CACHE_MAX = 262144;
|
||||
|
||||
private final long seed;
|
||||
private final Engine engine;
|
||||
private final Registry<Biome> biomeCustomRegistry;
|
||||
private final Registry<Biome> biomeRegistry;
|
||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||
private final KMap<String, Holder<Biome>> customBiomes;
|
||||
private final Holder<Biome> fallbackBiome;
|
||||
private final ConcurrentHashMap<Long, Holder<Biome>> noiseBiomeCache = new ConcurrentHashMap<>();
|
||||
|
||||
public CustomBiomeSource(long seed, Engine engine, World world) {
|
||||
this.engine = engine;
|
||||
this.seed = seed;
|
||||
this.biomeCustomRegistry = registry().lookup(Registries.BIOME).orElse(null);
|
||||
this.biomeRegistry = ((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer())).lookup(Registries.BIOME).orElse(null);
|
||||
this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry);
|
||||
this.customBiomes = fillCustomBiomes(this.biomeCustomRegistry, engine, this.fallbackBiome);
|
||||
}
|
||||
|
||||
private static List<Holder<Biome>> getAllBiomes(Registry<Biome> customRegistry, Registry<Biome> registry, Engine engine, Holder<Biome> fallback) {
|
||||
LinkedHashSet<Holder<Biome>> biomes = new LinkedHashSet<>();
|
||||
if (fallback != null) {
|
||||
biomes.add(fallback);
|
||||
}
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
Holder<Biome> customHolder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
if (customHolder != null) {
|
||||
biomes.add(customHolder);
|
||||
} else if (fallback != null) {
|
||||
biomes.add(fallback);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Holder<Biome> vanillaHolder = NMSBinding.biomeToBiomeBase(registry, i.getVanillaDerivative());
|
||||
if (vanillaHolder != null) {
|
||||
biomes.add(vanillaHolder);
|
||||
} else if (fallback != null) {
|
||||
biomes.add(fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ArrayList<>(biomes);
|
||||
}
|
||||
|
||||
private static Object getFor(Class<?> type, Object source) {
|
||||
Object o = fieldFor(type, source);
|
||||
|
||||
if (o != null) {
|
||||
return o;
|
||||
}
|
||||
|
||||
return invokeFor(type, source);
|
||||
}
|
||||
|
||||
private static Object fieldFor(Class<?> returns, Object in) {
|
||||
return fieldForClass(returns, in.getClass(), in);
|
||||
}
|
||||
|
||||
private static Object invokeFor(Class<?> returns, Object in) {
|
||||
for (Method i : in.getClass().getMethods()) {
|
||||
if (i.getReturnType().equals(returns)) {
|
||||
i.setAccessible(true);
|
||||
try {
|
||||
Iris.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
|
||||
return i.invoke(in);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T fieldForClass(Class<T> returnType, Class<?> sourceType, Object in) {
|
||||
for (Field i : sourceType.getDeclaredFields()) {
|
||||
if (i.getType().equals(returnType)) {
|
||||
i.setAccessible(true);
|
||||
try {
|
||||
Iris.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
|
||||
return (T) i.get(in);
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return getAllBiomes(
|
||||
((RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()))
|
||||
.lookup(Registries.BIOME).orElse(null),
|
||||
((CraftWorld) engine.getWorld().realWorld()).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null),
|
||||
engine,
|
||||
fallbackBiome).stream();
|
||||
}
|
||||
|
||||
private KMap<String, Holder<Biome>> fillCustomBiomes(Registry<Biome> customRegistry, Engine engine, Holder<Biome> fallback) {
|
||||
KMap<String, Holder<Biome>> m = new KMap<>();
|
||||
if (customRegistry == null) {
|
||||
return m;
|
||||
}
|
||||
|
||||
for (IrisBiome i : engine.getAllBiomes()) {
|
||||
if (i.isCustom()) {
|
||||
for (IrisBiomeCustom j : i.getCustomDerivitives()) {
|
||||
Holder<Biome> holder = resolveCustomBiomeHolder(customRegistry, engine, j.getId());
|
||||
if (holder == null) {
|
||||
if (fallback != null) {
|
||||
m.put(j.getId(), fallback);
|
||||
}
|
||||
Iris.error("Cannot find biome for IrisBiomeCustom " + j.getId() + " from engine " + engine.getName());
|
||||
continue;
|
||||
}
|
||||
m.put(j.getId(), holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
private RegistryAccess registry() {
|
||||
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("Not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(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> existingHolder = noiseBiomeCache.putIfAbsent(cacheKey, resolvedHolder);
|
||||
if (existingHolder != null) {
|
||||
return existingHolder;
|
||||
}
|
||||
|
||||
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
|
||||
noiseBiomeCache.clear();
|
||||
}
|
||||
|
||||
return resolvedHolder;
|
||||
}
|
||||
|
||||
private Holder<Biome> resolveNoiseBiomeHolder(int x, int y, int z) {
|
||||
if (engine == null || engine.isClosed()) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
if (engine.getComplex() == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
int blockX = x << 2;
|
||||
int blockZ = z << 2;
|
||||
int blockY = y << 2;
|
||||
int worldMinHeight = engine.getWorld().minHeight();
|
||||
int internalY = blockY - worldMinHeight;
|
||||
int surfaceInternalY = engine.getComplex().getHeightStream().get(blockX, blockZ).intValue();
|
||||
int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40);
|
||||
boolean deepUnderground = internalY <= caveSwitchInternalY;
|
||||
boolean belowSurface = internalY <= surfaceInternalY - 8;
|
||||
boolean underground = deepUnderground && belowSurface;
|
||||
IrisBiome irisBiome = underground
|
||||
? engine.getCaveBiome(blockX, internalY, blockZ)
|
||||
: engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
if (irisBiome == null && underground) {
|
||||
irisBiome = engine.getComplex().getTrueBiomeStream().get(blockX, blockZ);
|
||||
}
|
||||
if (irisBiome == null) {
|
||||
return getFallbackBiome();
|
||||
}
|
||||
|
||||
RNG noiseRng = new RNG(seed
|
||||
^ (((long) blockX) * 341873128712L)
|
||||
^ (((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();
|
||||
}
|
||||
|
||||
private Holder<Biome> getFallbackBiome() {
|
||||
if (fallbackBiome != null) {
|
||||
return fallbackBiome;
|
||||
}
|
||||
|
||||
Holder<Biome> holder = resolveFallbackBiome(biomeRegistry, biomeCustomRegistry);
|
||||
if (holder != null) {
|
||||
return holder;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Unable to resolve any biome holder fallback for Iris biome source");
|
||||
}
|
||||
|
||||
private static long packNoiseKey(int x, int y, int z) {
|
||||
return (((long) x & 67108863L) << 38)
|
||||
| (((long) z & 67108863L) << 12)
|
||||
| ((long) y & 4095L);
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveCustomBiomeHolder(Registry<Biome> customRegistry, Engine engine, String customBiomeId) {
|
||||
if (customRegistry == null || engine == null || customBiomeId == null || customBiomeId.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Identifier resourceLocation = Identifier.fromNamespaceAndPath(
|
||||
engine.getDimension().getLoadKey().toLowerCase(java.util.Locale.ROOT),
|
||||
customBiomeId.toLowerCase(java.util.Locale.ROOT)
|
||||
);
|
||||
Biome biome = customRegistry.getValue(resourceLocation);
|
||||
if (biome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<ResourceKey<Biome>> optionalBiomeKey = customRegistry.getResourceKey(biome);
|
||||
if (optionalBiomeKey.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Optional<Holder.Reference<Biome>> optionalReferenceHolder = customRegistry.get(optionalBiomeKey.get());
|
||||
if (optionalReferenceHolder.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return optionalReferenceHolder.get();
|
||||
}
|
||||
|
||||
private static Holder<Biome> resolveFallbackBiome(Registry<Biome> registry, Registry<Biome> customRegistry) {
|
||||
Holder<Biome> plains = NMSBinding.biomeToBiomeBase(registry, org.bukkit.block.Biome.PLAINS);
|
||||
if (plains != null) {
|
||||
return plains;
|
||||
}
|
||||
|
||||
Holder<Biome> vanilla = firstHolder(registry);
|
||||
if (vanilla != null) {
|
||||
return vanilla;
|
||||
}
|
||||
|
||||
return firstHolder(customRegistry);
|
||||
}
|
||||
|
||||
private static Holder<Biome> firstHolder(Registry<Biome> registry) {
|
||||
if (registry == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Biome biome : registry) {
|
||||
Optional<ResourceKey<Biome>> optionalBiomeKey = registry.getResourceKey(biome);
|
||||
if (optionalBiomeKey.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Optional<Holder.Reference<Biome>> optionalHolder = registry.get(optionalBiomeKey.get());
|
||||
if (optionalHolder.isPresent()) {
|
||||
return optionalHolder.get();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
package art.arcane.iris.core.nms.v26_1_R1;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.object.IrisVanillaStructureControl;
|
||||
import art.arcane.iris.util.common.reflect.WrappedField;
|
||||
import art.arcane.iris.util.common.reflect.WrappedReturningMethod;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.util.random.WeightedList;
|
||||
import net.minecraft.world.entity.MobCategory;
|
||||
import net.minecraft.world.level.*;
|
||||
import net.minecraft.world.level.biome.*;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.levelgen.*;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
||||
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import java.util.stream.Collectors;
|
||||
import art.arcane.iris.engine.framework.StructurePlacementGrid;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.StructurePlacementRoute;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.data.cache.Cache;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.math.RNG;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.craftbukkit.generator.CustomChunkGenerator;
|
||||
import org.spigotmc.SpigotWorldConfig;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class IrisChunkGenerator extends CustomChunkGenerator {
|
||||
private static final WrappedField<ChunkGenerator, BiomeSource> BIOME_SOURCE;
|
||||
private static final WrappedReturningMethod<Heightmap, Object> SET_HEIGHT;
|
||||
private final ChunkGenerator delegate;
|
||||
private final Engine engine;
|
||||
|
||||
public IrisChunkGenerator(ChunkGenerator delegate, long seed, Engine engine, World world) {
|
||||
super(((CraftWorld) world).getHandle(), edit(delegate, new CustomBiomeSource(seed, engine, world)), null);
|
||||
this.delegate = delegate;
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level, HolderSet<Structure> holders, BlockPos pos, int radius, boolean findUnexplored) {
|
||||
if (!vanillaControl().active()) {
|
||||
return null;
|
||||
}
|
||||
return delegate.findNearestMapStructure(level, holders, pos, radius, findUnexplored);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return MapCodec.unit(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkGenerator getDelegate() {
|
||||
if (delegate instanceof CustomChunkGenerator chunkGenerator)
|
||||
return chunkGenerator.getDelegate();
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinY() {
|
||||
return delegate.getMinY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSeaLevel() {
|
||||
return delegate.getSeaLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey<Level> levelKey) {
|
||||
if (!vanillaControl().active()) {
|
||||
return;
|
||||
}
|
||||
delegate.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey);
|
||||
}
|
||||
|
||||
private IrisVanillaStructureControl vanillaControl() {
|
||||
return engine.getDimension().getVanillaStructures();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> holderlookup, RandomState randomstate, long i, SpigotWorldConfig conf) {
|
||||
return delegate.createState(holderlookup, randomstate, i, conf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createReferences(WorldGenLevel generatoraccessseed, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
delegate.createReferences(generatoraccessseed, structuremanager, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> createBiomes(RandomState randomstate, Blender blender, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
return delegate.createBiomes(randomstate, blender, structuremanager, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buildSurface(WorldGenRegion regionlimitedworldaccess, StructureManager structuremanager, RandomState randomstate, ChunkAccess ichunkaccess) {
|
||||
delegate.buildSurface(regionlimitedworldaccess, structuremanager, randomstate, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyCarvers(WorldGenRegion regionlimitedworldaccess, long seed, RandomState randomstate, BiomeManager biomemanager, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
delegate.applyCarvers(regionlimitedworldaccess, seed, randomstate, biomemanager, structuremanager, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState randomstate, StructureManager structuremanager, ChunkAccess ichunkaccess) {
|
||||
return delegate.fillFromNoise(blender, randomstate, structuremanager, ichunkaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeightedList<MobSpawnSettings.SpawnerData> getMobsAt(Holder<Biome> holder, StructureManager structuremanager, MobCategory enumcreaturetype, BlockPos blockposition) {
|
||||
return delegate.getMobsAt(holder, structuremanager, enumcreaturetype, blockposition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager) {
|
||||
applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDebugScreenInfo(List<String> list, RandomState randomstate, BlockPos blockposition) {
|
||||
delegate.addDebugScreenInfo(list, randomstate, blockposition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) {
|
||||
addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
if (vanillaControl().active()) {
|
||||
placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager);
|
||||
}
|
||||
placeIrisNativeStructures(generatoraccessseed, ichunkaccess);
|
||||
delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false);
|
||||
}
|
||||
|
||||
private void placeVanillaStructures(WorldGenLevel world, ChunkAccess chunk, StructureManager structureManager) {
|
||||
if (!structureManager.shouldGenerateStructures()) {
|
||||
return;
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
SectionPos sectionPos = SectionPos.of(chunkPos, world.getMinSectionY());
|
||||
BlockPos origin = sectionPos.origin();
|
||||
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
Map<Integer, List<Structure>> byStep = registry.stream().collect(Collectors.groupingBy(s -> s.step().ordinal()));
|
||||
WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ());
|
||||
BoundingBox area = writableArea(chunk);
|
||||
int steps = GenerationStep.Decoration.values().length;
|
||||
IrisVanillaStructureControl control = vanillaControl();
|
||||
for (int step = 0; step < steps; step++) {
|
||||
int index = 0;
|
||||
for (Structure structure : byStep.getOrDefault(step, List.of())) {
|
||||
Object id = registry.getKey(structure);
|
||||
if (control.shouldGenerate(id == null ? null : id.toString())) {
|
||||
random.setFeatureSeed(decoSeed, index, step);
|
||||
try {
|
||||
structureManager.startsForStructure(sectionPos, structure)
|
||||
.forEach(start -> start.placeInChunk(world, structureManager, this, random, area, chunkPos));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BoundingBox writableArea(ChunkAccess chunk) {
|
||||
ChunkPos cp = chunk.getPos();
|
||||
int i = cp.getMinBlockX();
|
||||
int j = cp.getMinBlockZ();
|
||||
int minY = getMinY() + 1;
|
||||
int maxY = getMinY() + engine.getHeight() - 1;
|
||||
return new BoundingBox(i, minY, j, i + 15, maxY, j + 15);
|
||||
}
|
||||
|
||||
private void placeIrisNativeStructures(WorldGenLevel world, ChunkAccess chunk) {
|
||||
if (!(world instanceof WorldGenRegion region)) {
|
||||
return;
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
int cx = chunkPos.getMinBlockX() >> 4;
|
||||
int cz = chunkPos.getMinBlockZ() >> 4;
|
||||
int bx = 8 + (cx << 4);
|
||||
int bz = 8 + (cz << 4);
|
||||
IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(bx, bz);
|
||||
IrisRegion ireg = engine.getComplex().getRegionStream().get(bx, bz);
|
||||
KList<IrisStructurePlacement> placements = new KList<>();
|
||||
if (biome != null) {
|
||||
placements.addAll(biome.getStructures());
|
||||
}
|
||||
if (ireg != null) {
|
||||
placements.addAll(ireg.getStructures());
|
||||
}
|
||||
placements.addAll(engine.getDimension().getStructures());
|
||||
if (placements.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long seed = world.getSeed();
|
||||
RNG rng = new RNG(Cache.key(cx, cz) + seed);
|
||||
ServerLevel level = region.getLevel();
|
||||
Registry<Structure> registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE);
|
||||
RandomState randomState = level.getChunkSource().randomState();
|
||||
StructureTemplateManager templateManager = level.getStructureManager();
|
||||
StructureManager structureManager = level.structureManager();
|
||||
BiomeSource biomeSource = getBiomeSource();
|
||||
ResourceKey<Level> levelKey = level.dimension();
|
||||
int minY = getMinY() + 1;
|
||||
int maxY = getMinY() + engine.getHeight() - 1;
|
||||
|
||||
for (IrisStructurePlacement placement : placements) {
|
||||
if (placement.getRoute() != StructurePlacementRoute.NATIVE_AT_POINT || placement.getVanilla().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, rng)) {
|
||||
continue;
|
||||
}
|
||||
String key = placement.getVanilla().get(rng.i(0, placement.getVanilla().size() - 1)).toLowerCase();
|
||||
try {
|
||||
String[] parts = key.split(":", 2);
|
||||
Identifier id = parts.length > 1 ? Identifier.fromNamespaceAndPath(parts[0], parts[1]) : Identifier.fromNamespaceAndPath("minecraft", parts[0]);
|
||||
Holder<Structure> holder = registry.getOrThrow(ResourceKey.create(Registries.STRUCTURE, id));
|
||||
Structure structure = holder.value();
|
||||
StructureStart start = structure.generate(holder, levelKey, world.registryAccess(), this, biomeSource, randomState, templateManager, seed, chunkPos, 0, level, b -> true);
|
||||
if (start == null || !start.isValid()) {
|
||||
continue;
|
||||
}
|
||||
BoundingBox box = start.getBoundingBox();
|
||||
WorldgenRandom placeRandom = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed()));
|
||||
for (int scx = box.minX() >> 4; scx <= box.maxX() >> 4; scx++) {
|
||||
for (int scz = box.minZ() >> 4; scz <= box.maxZ() >> 4; scz++) {
|
||||
ChunkPos target = new ChunkPos(scx, scz);
|
||||
BoundingBox chunkBox = new BoundingBox(target.getMinBlockX(), minY, target.getMinBlockZ(), target.getMaxBlockX(), maxY, target.getMaxBlockZ());
|
||||
start.placeInChunk(world, structureManager, this, placeRandom, chunkBox, target);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) {
|
||||
SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY());
|
||||
BlockPos blockPos = sectionPos.origin();
|
||||
|
||||
Heightmap surface = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap ocean = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
Heightmap motion = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING);
|
||||
Heightmap motionNoLeaves = chunkAccess.getOrCreateHeightmapUnprimed(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES);
|
||||
|
||||
for (int x = 0; x < 16; x++) {
|
||||
for (int z = 0; z < 16; z++) {
|
||||
int wX = x + blockPos.getX();
|
||||
int wZ = z + blockPos.getZ();
|
||||
|
||||
int terrainTop = engine.getHeight(wX, wZ, false) + engine.getMinHeight() + 1;
|
||||
int terrainNoFluid = engine.getHeight(wX, wZ, true) + engine.getMinHeight() + 1;
|
||||
SET_HEIGHT.invoke(ocean, x, z, terrainNoFluid);
|
||||
SET_HEIGHT.invoke(surface, x, z, terrainTop);
|
||||
SET_HEIGHT.invoke(motion, x, z, terrainTop);
|
||||
SET_HEIGHT.invoke(motionNoLeaves, x, z, terrainTop);
|
||||
}
|
||||
}
|
||||
|
||||
Heightmap.primeHeightmaps(chunkAccess, ChunkStatus.FINAL_HEIGHTMAPS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spawnOriginalMobs(WorldGenRegion regionlimitedworldaccess) {
|
||||
delegate.spawnOriginalMobs(regionlimitedworldaccess);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpawnHeight(LevelHeightAccessor levelheightaccessor) {
|
||||
return delegate.getSpawnHeight(levelheightaccessor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGenDepth() {
|
||||
return delegate.getGenDepth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int i, int j, Heightmap.Types heightmap_type, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
return levelheightaccessor.getMinY() + engine.getHeight(i, j, !heightmap_type.isOpaque().test(Blocks.WATER.defaultBlockState())) + 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int i, int j, LevelHeightAccessor levelheightaccessor, RandomState randomstate) {
|
||||
int block = engine.getHeight(i, j, true);
|
||||
int water = engine.getHeight(i, j, false);
|
||||
BlockState[] column = new BlockState[levelheightaccessor.getHeight()];
|
||||
for (int k = 0; k < column.length; k++) {
|
||||
if (k <= block) column[k] = Blocks.STONE.defaultBlockState();
|
||||
else if (k <= water) column[k] = Blocks.WATER.defaultBlockState();
|
||||
else column[k] = Blocks.AIR.defaultBlockState();
|
||||
}
|
||||
return new NoiseColumn(levelheightaccessor.getMinY(), column);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Identifier> getTypeNameForDataFixer() {
|
||||
return delegate.getTypeNameForDataFixer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() {
|
||||
delegate.validate();
|
||||
}
|
||||
|
||||
static {
|
||||
Field biomeSource = null;
|
||||
for (Field field : ChunkGenerator.class.getDeclaredFields()) {
|
||||
if (!field.getType().equals(BiomeSource.class))
|
||||
continue;
|
||||
biomeSource = field;
|
||||
break;
|
||||
}
|
||||
if (biomeSource == null)
|
||||
throw new RuntimeException("Could not find biomeSource field in ChunkGenerator!");
|
||||
|
||||
Method setHeight = null;
|
||||
for (Method method : Heightmap.class.getDeclaredMethods()) {
|
||||
var types = method.getParameterTypes();
|
||||
if (types.length != 3 || !Arrays.equals(types, new Class<?>[]{int.class, int.class, int.class})
|
||||
|| !method.getReturnType().equals(void.class))
|
||||
continue;
|
||||
setHeight = method;
|
||||
break;
|
||||
}
|
||||
if (setHeight == null)
|
||||
throw new RuntimeException("Could not find setHeight method in Heightmap!");
|
||||
|
||||
BIOME_SOURCE = new WrappedField<>(ChunkGenerator.class, biomeSource.getName());
|
||||
SET_HEIGHT = new WrappedReturningMethod<>(Heightmap.class, setHeight.getName(), setHeight.getParameterTypes());
|
||||
}
|
||||
|
||||
private static ChunkGenerator edit(ChunkGenerator generator, BiomeSource source) {
|
||||
try {
|
||||
BIOME_SOURCE.set(generator, source);
|
||||
if (generator instanceof CustomChunkGenerator custom)
|
||||
BIOME_SOURCE.set(custom.getDelegate(), source);
|
||||
|
||||
return generator;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,880 @@
|
||||
package art.arcane.iris.core.nms.v26_1_R1;
|
||||
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import art.arcane.iris.Iris;
|
||||
import art.arcane.iris.core.nms.INMSBinding;
|
||||
import art.arcane.iris.core.nms.container.BiomeColor;
|
||||
import art.arcane.iris.core.nms.container.Pair;
|
||||
import art.arcane.iris.core.nms.container.BlockProperty;
|
||||
import art.arcane.iris.core.nms.datapack.DataVersion;
|
||||
import art.arcane.iris.engine.data.cache.AtomicCache;
|
||||
import art.arcane.iris.engine.framework.Engine;
|
||||
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
|
||||
import art.arcane.iris.util.project.agent.Agent;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.iris.util.common.format.C;
|
||||
import art.arcane.iris.util.project.hunk.Hunk;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||
import art.arcane.volmlib.util.matter.Matter;
|
||||
import art.arcane.volmlib.util.math.Vector3d;
|
||||
import art.arcane.volmlib.util.matter.MatterBiomeInject;
|
||||
import art.arcane.iris.util.nbt.common.mca.NBTWorld;
|
||||
import art.arcane.volmlib.util.nbt.mca.palette.*;
|
||||
import art.arcane.volmlib.util.nbt.tag.CompoundTag;
|
||||
import art.arcane.iris.util.common.scheduling.J;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.shorts.ShortList;
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.matcher.ElementMatchers;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.Registry;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.nbt.*;
|
||||
import net.minecraft.nbt.Tag;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.commands.data.BlockDataAccessor;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
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.Biomes;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.properties.Property;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||
import net.minecraft.world.level.storage.LevelStorageSource;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.Biome;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.craftbukkit.CraftChunk;
|
||||
import org.bukkit.craftbukkit.CraftServer;
|
||||
import org.bukkit.craftbukkit.CraftWorld;
|
||||
import org.bukkit.craftbukkit.block.CraftBlockState;
|
||||
import org.bukkit.craftbukkit.block.CraftBlockStates;
|
||||
import org.bukkit.craftbukkit.block.data.CraftBlockData;
|
||||
import org.bukkit.craftbukkit.inventory.CraftItemStack;
|
||||
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
|
||||
import org.bukkit.craftbukkit.util.CraftNamespacedKey;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.entity.CreatureSpawnEvent;
|
||||
import org.bukkit.generator.BiomeProvider;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class NMSBinding implements INMSBinding {
|
||||
private final KMap<Biome, Object> baseBiomeCache = new KMap<>();
|
||||
private final BlockData AIR = Material.AIR.createBlockData();
|
||||
private final AtomicCache<MCAIdMap<net.minecraft.world.level.biome.Biome>> biomeMapCache = new AtomicCache<>();
|
||||
private final AtomicBoolean injected = new AtomicBoolean();
|
||||
private final AtomicCache<MCAIdMapper<BlockState>> registryCache = new AtomicCache<>();
|
||||
private final AtomicCache<MCAPalette<BlockState>> globalCache = new AtomicCache<>();
|
||||
private final AtomicCache<RegistryAccess> registryAccess = new AtomicCache<>();
|
||||
private final AtomicCache<Method> byIdRef = new AtomicCache<>();
|
||||
|
||||
private static Object getFor(Class<?> type, Object source) {
|
||||
Object o = fieldFor(type, source);
|
||||
|
||||
if (o != null) {
|
||||
return o;
|
||||
}
|
||||
|
||||
return invokeFor(type, source);
|
||||
}
|
||||
|
||||
private static Object invokeFor(Class<?> returns, Object in) {
|
||||
for (Method i : in.getClass().getMethods()) {
|
||||
if (i.getReturnType().equals(returns)) {
|
||||
i.setAccessible(true);
|
||||
try {
|
||||
Iris.debug("[NMS] Found " + returns.getSimpleName() + " in " + in.getClass().getSimpleName() + "." + i.getName() + "()");
|
||||
return i.invoke(in);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object fieldFor(Class<?> returns, Object in) {
|
||||
return fieldForClass(returns, in.getClass(), in);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T fieldForClass(Class<T> returnType, Class<?> sourceType, Object in) {
|
||||
for (Field i : sourceType.getDeclaredFields()) {
|
||||
if (i.getType().equals(returnType)) {
|
||||
i.setAccessible(true);
|
||||
try {
|
||||
Iris.debug("[NMS] Found " + returnType.getSimpleName() + " in " + sourceType.getSimpleName() + "." + i.getName());
|
||||
return (T) i.get(in);
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Class<?> getClassType(Class<?> type, int ordinal) {
|
||||
return type.getDeclaredClasses()[ordinal];
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTile(Material material) {
|
||||
return !CraftBlockState.class.equals(CraftBlockStates.getBlockStateType(material));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasTile(Location l) {
|
||||
return ((CraftWorld) l.getWorld()).getHandle().getBlockEntity(new BlockPos(l.getBlockX(), l.getBlockY(), l.getBlockZ())) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public KMap<String, Object> serializeTile(Location location) {
|
||||
BlockEntity e = ((CraftWorld) location.getWorld()).getHandle().getBlockEntity(new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()));
|
||||
|
||||
if (e == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
net.minecraft.nbt.CompoundTag tag = e.saveWithoutMetadata(registry());
|
||||
return (KMap<String, Object>) convertFromTag(tag, 0, 64);
|
||||
}
|
||||
|
||||
@Contract(value = "null, _, _ -> null", pure = true)
|
||||
private Object convertFromTag(Tag tag, int depth, int maxDepth) {
|
||||
if (tag == null || depth > maxDepth) return null;
|
||||
return switch (tag) {
|
||||
case CollectionTag collection -> {
|
||||
KList<Object> list = new KList<>();
|
||||
|
||||
for (Object i : collection) {
|
||||
if (i instanceof Tag t)
|
||||
list.add(convertFromTag(t, depth + 1, maxDepth));
|
||||
else list.add(i);
|
||||
}
|
||||
yield list;
|
||||
}
|
||||
case net.minecraft.nbt.CompoundTag compound -> {
|
||||
KMap<String, Object> map = new KMap<>();
|
||||
|
||||
for (String key : compound.keySet()) {
|
||||
var child = compound.get(key);
|
||||
if (child == null) continue;
|
||||
var value = convertFromTag(child, depth + 1, maxDepth);
|
||||
if (value == null) continue;
|
||||
map.put(key, value);
|
||||
}
|
||||
yield map;
|
||||
}
|
||||
case NumericTag numeric -> numeric.box();
|
||||
default -> tag.asString().orElse(null);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deserializeTile(KMap<String, Object> map, Location pos) {
|
||||
if (map == null || pos == null || pos.getWorld() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Tag converted = convertToTag(map, 0, 64);
|
||||
if (!(converted instanceof net.minecraft.nbt.CompoundTag tag)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var level = ((CraftWorld) pos.getWorld()).getHandle();
|
||||
var blockPos = new BlockPos(pos.getBlockX(), pos.getBlockY(), pos.getBlockZ());
|
||||
if (!J.runAt(pos, () -> merge(level, blockPos, tag))) {
|
||||
Iris.warn("[NMS] Failed to schedule tile deserialize at " + blockPos + " in world " + pos.getWorld().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void merge(ServerLevel level, BlockPos blockPos, net.minecraft.nbt.CompoundTag tag) {
|
||||
if (level == null || blockPos == null || tag == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var blockEntity = level.getBlockEntity(blockPos);
|
||||
if (blockEntity == null) {
|
||||
Iris.warn("[NMS] BlockEntity not found at " + blockPos);
|
||||
var state = level.getBlockState(blockPos);
|
||||
if (!state.hasBlockEntity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
blockEntity = ((EntityBlock) state.getBlock())
|
||||
.newBlockEntity(blockPos, state);
|
||||
}
|
||||
|
||||
var accessor = new BlockDataAccessor(blockEntity, blockPos);
|
||||
accessor.setData(accessor.getData().merge(tag));
|
||||
} catch (Throwable e) {
|
||||
Iris.warn("[NMS] Failed to merge tile data at " + blockPos + ": " + e.getMessage());
|
||||
Iris.reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Tag convertToTag(Object object, int depth, int maxDepth) {
|
||||
if (object == null || depth > maxDepth) return EndTag.INSTANCE;
|
||||
return switch (object) {
|
||||
case Map<?, ?> map -> {
|
||||
var tag = new net.minecraft.nbt.CompoundTag();
|
||||
for (var i : map.entrySet()) {
|
||||
tag.put(i.getKey().toString(), convertToTag(i.getValue(), depth + 1, maxDepth));
|
||||
}
|
||||
yield tag;
|
||||
}
|
||||
case List<?> list -> {
|
||||
var tag = new ListTag();
|
||||
for (var i : list) {
|
||||
tag.add(convertToTag(i, depth + 1, maxDepth));
|
||||
}
|
||||
yield tag;
|
||||
}
|
||||
case Byte number -> ByteTag.valueOf(number);
|
||||
case Short number -> ShortTag.valueOf(number);
|
||||
case Integer number -> IntTag.valueOf(number);
|
||||
case Long number -> LongTag.valueOf(number);
|
||||
case Float number -> FloatTag.valueOf(number);
|
||||
case Double number -> DoubleTag.valueOf(number);
|
||||
case String string -> StringTag.valueOf(string);
|
||||
default -> EndTag.INSTANCE;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag serializeEntity(Entity location) {
|
||||
return null;// TODO:
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entity deserializeEntity(CompoundTag s, Location newPosition) {
|
||||
return null;// TODO:
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCustomHeight() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private RegistryAccess registry() {
|
||||
return registryAccess.aquire(() -> (RegistryAccess) getFor(RegistryAccess.Frozen.class, ((CraftServer) Bukkit.getServer()).getHandle().getServer()));
|
||||
}
|
||||
|
||||
private Registry<net.minecraft.world.level.biome.Biome> getCustomBiomeRegistry() {
|
||||
return registry().lookup(Registries.BIOME).orElse(null);
|
||||
}
|
||||
|
||||
private Registry<Block> getBlockRegistry() {
|
||||
return registry().lookup(Registries.BLOCK).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBiomeBaseFromId(int id) {
|
||||
return getCustomBiomeRegistry().get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinHeight(World world) {
|
||||
return world.getMinHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsCustomBiomes() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTrueBiomeBaseId(Object biomeBase) {
|
||||
return getCustomBiomeRegistry().getId(((Holder<net.minecraft.world.level.biome.Biome>) biomeBase).value());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getTrueBiomeBase(Location location) {
|
||||
return ((CraftWorld) location.getWorld()).getHandle().getBiome(new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTrueBiomeBaseKey(Location location) {
|
||||
return getKeyForBiomeBase(getTrueBiomeBase(location));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCustomBiomeBaseFor(String mckey) {
|
||||
return getCustomBiomeRegistry().getValue(net.minecraft.resources.Identifier.parse(mckey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCustomBiomeBaseHolderFor(String mckey) {
|
||||
return getCustomBiomeRegistry().get(getTrueBiomeBaseId(getCustomBiomeRegistry().get(net.minecraft.resources.Identifier.parse(mckey)))).orElse(null);
|
||||
}
|
||||
|
||||
public int getBiomeBaseIdForKey(String key) {
|
||||
return getCustomBiomeRegistry().getId(getCustomBiomeRegistry().get(net.minecraft.resources.Identifier.parse(key)).map(Holder::value).orElse(null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeyForBiomeBase(Object biomeBase) {
|
||||
return getCustomBiomeRegistry().getKey((net.minecraft.world.level.biome.Biome) biomeBase).getPath(); // something, not something:something
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBiomeBase(World world, Biome biome) {
|
||||
return biomeToBiomeBase(((CraftWorld) world).getHandle()
|
||||
.registryAccess().lookup(Registries.BIOME).orElse(null), biome);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBiomeBase(Object registry, Biome biome) {
|
||||
Object v = baseBiomeCache.get(biome);
|
||||
|
||||
if (v != null) {
|
||||
return v;
|
||||
}
|
||||
//noinspection unchecked
|
||||
v = biomeToBiomeBase((Registry<net.minecraft.world.level.biome.Biome>) registry, biome);
|
||||
if (v == null) {
|
||||
// Ok so there is this new biome name called "CUSTOM" in Paper's new releases.
|
||||
// But, this does NOT exist within CraftBukkit which makes it return an error.
|
||||
// So, we will just return the ID that the plains biome returns instead.
|
||||
//noinspection unchecked
|
||||
return biomeToBiomeBase((Registry<net.minecraft.world.level.biome.Biome>) registry, Biome.PLAINS);
|
||||
}
|
||||
baseBiomeCache.put(biome, v);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<Biome> getBiomes() {
|
||||
KList<Biome> biomes = new KList<>();
|
||||
for (Biome biome : org.bukkit.Registry.BIOME) {
|
||||
biomes.add(biome);
|
||||
}
|
||||
return biomes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getStructureKeys() {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
registry().lookupOrThrow(Registries.STRUCTURE).keySet().forEach(k -> keys.add(k.toString()));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KList<String> getStructureSetKeys() {
|
||||
KList<String> keys = new KList<>();
|
||||
try {
|
||||
registry().lookupOrThrow(Registries.STRUCTURE_SET).keySet().forEach(k -> keys.add(k.toString()));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBukkit() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBiomeId(Biome biome) {
|
||||
for (World i : Bukkit.getWorlds()) {
|
||||
if (i.getEnvironment().equals(World.Environment.NORMAL)) {
|
||||
Registry<net.minecraft.world.level.biome.Biome> registry = ((CraftWorld) i).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null);
|
||||
return registry.getId((net.minecraft.world.level.biome.Biome) getBiomeBase(registry, biome));
|
||||
}
|
||||
}
|
||||
|
||||
List<Biome> biomes = new ArrayList<>();
|
||||
for (Biome entry : org.bukkit.Registry.BIOME) {
|
||||
biomes.add(entry);
|
||||
}
|
||||
int index = biomes.indexOf(biome);
|
||||
return Math.max(index, 0);
|
||||
}
|
||||
|
||||
private MCAIdMap<net.minecraft.world.level.biome.Biome> getBiomeMapping() {
|
||||
return biomeMapCache.aquire(() -> new MCAIdMap<>() {
|
||||
@NotNull
|
||||
@Override
|
||||
public Iterator<net.minecraft.world.level.biome.Biome> iterator() {
|
||||
return getCustomBiomeRegistry().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId(net.minecraft.world.level.biome.Biome paramT) {
|
||||
return getCustomBiomeRegistry().getId(paramT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public net.minecraft.world.level.biome.Biome byId(int paramInt) {
|
||||
return (net.minecraft.world.level.biome.Biome) getBiomeBaseFromId(paramInt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private MCABiomeContainer getBiomeContainerInterface(MCAIdMap<net.minecraft.world.level.biome.Biome> biomeMapping, MCAChunkBiomeContainer<net.minecraft.world.level.biome.Biome> base) {
|
||||
return new MCABiomeContainer() {
|
||||
@Override
|
||||
public int[] getData() {
|
||||
return base.writeBiomes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBiome(int x, int y, int z, int id) {
|
||||
base.setBiome(x, y, z, biomeMapping.byId(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBiome(int x, int y, int z) {
|
||||
return biomeMapping.getId(base.getBiome(x, y, z));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public MCABiomeContainer newBiomeContainer(int min, int max) {
|
||||
MCAChunkBiomeContainer<net.minecraft.world.level.biome.Biome> base = new MCAChunkBiomeContainer<>(getBiomeMapping(), min, max);
|
||||
return getBiomeContainerInterface(getBiomeMapping(), base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MCABiomeContainer newBiomeContainer(int min, int max, int[] data) {
|
||||
MCAChunkBiomeContainer<net.minecraft.world.level.biome.Biome> base = new MCAChunkBiomeContainer<>(getBiomeMapping(), min, max, data);
|
||||
return getBiomeContainerInterface(getBiomeMapping(), base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countCustomBiomes() {
|
||||
AtomicInteger a = new AtomicInteger(0);
|
||||
|
||||
getCustomBiomeRegistry().keySet().forEach((i) -> {
|
||||
if (i.getNamespace().equals("minecraft")) {
|
||||
return;
|
||||
}
|
||||
|
||||
a.incrementAndGet();
|
||||
Iris.debug("Custom Biome: " + i);
|
||||
});
|
||||
|
||||
return a.get();
|
||||
}
|
||||
|
||||
public boolean supportsDataPacks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setBiomes(int cx, int cz, World world, Hunk<Object> biomes) {
|
||||
LevelChunk c = ((CraftWorld) world).getHandle().getChunk(cx, cz);
|
||||
biomes.iterateSync((x, y, z, b) -> c.setNoiseBiome(x, y, z, (Holder<net.minecraft.world.level.biome.Biome>) b));
|
||||
c.markUnsaved();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MCAPaletteAccess createPalette() {
|
||||
MCAIdMapper<BlockState> registry = registryCache.aquireNasty(() -> {
|
||||
Field cf = IdMapper.class.getDeclaredField("tToId");
|
||||
Field df = IdMapper.class.getDeclaredField("idToT");
|
||||
Field bf = IdMapper.class.getDeclaredField("nextId");
|
||||
cf.setAccessible(true);
|
||||
df.setAccessible(true);
|
||||
bf.setAccessible(true);
|
||||
IdMapper<BlockState> blockData = Block.BLOCK_STATE_REGISTRY;
|
||||
int b = bf.getInt(blockData);
|
||||
Object2IntMap<BlockState> c = (Object2IntMap<BlockState>) cf.get(blockData);
|
||||
List<BlockState> d = (List<BlockState>) df.get(blockData);
|
||||
return new MCAIdMapper<BlockState>(c, d, b);
|
||||
});
|
||||
MCAPalette<BlockState> global = globalCache.aquireNasty(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState()));
|
||||
MCAPalettedContainer<BlockState> container = new MCAPalettedContainer<>(global, registry,
|
||||
i -> ((CraftBlockData) NBTWorld.getBlockData(i)).getState(),
|
||||
i -> NBTWorld.getCompound(CraftBlockData.createData(i)),
|
||||
((CraftBlockData) AIR).getState());
|
||||
return new MCAWrappedPalettedContainer<>(container,
|
||||
i -> NBTWorld.getCompound(CraftBlockData.createData(i)),
|
||||
i -> ((CraftBlockData) NBTWorld.getBlockData(i)).getState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle) {
|
||||
ChunkAccess chunk = ((CraftChunk) e).getHandle(ChunkStatus.FULL);
|
||||
AtomicInteger c = new AtomicInteger();
|
||||
AtomicInteger r = new AtomicInteger();
|
||||
mantle.iterateChunk(e.getX(), e.getZ(), MatterBiomeInject.class, (x, y, z, b) -> {
|
||||
if (b != null) {
|
||||
if (b.isCustom()) {
|
||||
chunk.setNoiseBiome(x, y, z, getCustomBiomeRegistry().get(b.getBiomeId()).get());
|
||||
c.getAndIncrement();
|
||||
} else {
|
||||
chunk.setNoiseBiome(x, y, z, (Holder<net.minecraft.world.level.biome.Biome>) getBiomeBase(e.getWorld(), b.getBiome()));
|
||||
r.getAndIncrement();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
try {
|
||||
net.minecraft.nbt.CompoundTag tag = TagParser.parseCompoundFully((new JSONObject(customNbt)).toString());
|
||||
tag.merge(s.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag());
|
||||
s.set(DataComponents.CUSTOM_DATA, CustomData.of(tag));
|
||||
} catch (CommandSyntaxException var5) {
|
||||
throw new IllegalArgumentException(var5);
|
||||
}
|
||||
|
||||
return CraftItemStack.asBukkitCopy(s);
|
||||
} else {
|
||||
return itemStack;
|
||||
}
|
||||
}
|
||||
|
||||
public void inject(long seed, Engine engine, World world) throws NoSuchFieldException, IllegalAccessException {
|
||||
var chunkMap = ((CraftWorld)world).getHandle().getChunkSource().chunkMap;
|
||||
var worldGenContextField = getField(chunkMap.getClass(), WorldGenContext.class);
|
||||
worldGenContextField.setAccessible(true);
|
||||
var worldGenContext = (WorldGenContext) worldGenContextField.get(chunkMap);
|
||||
var dimensionType = chunkMap.level.dimensionTypeRegistration().unwrapKey().orElse(null);
|
||||
String expectedDimensionType = "iris:" + engine.getDimension().getDimensionTypeKey();
|
||||
if (dimensionType != null) {
|
||||
String actualDimensionType = dimensionType.identifier().toString();
|
||||
if (!dimensionType.identifier().getNamespace().equals("iris")) {
|
||||
Iris.error("Loaded world %s with invalid dimension type! expected=%s actual=%s", world.getName(), expectedDimensionType, actualDimensionType);
|
||||
} else {
|
||||
Iris.info("Loaded world %s with Iris dimension type %s", world.getName(), actualDimensionType);
|
||||
}
|
||||
} else {
|
||||
Iris.error("Loaded world %s with unknown dimension type! expected=%s", world.getName(), expectedDimensionType);
|
||||
}
|
||||
|
||||
var newContext = new WorldGenContext(
|
||||
worldGenContext.level(), new IrisChunkGenerator(worldGenContext.generator(), seed, engine, world),
|
||||
worldGenContext.structureManager(), worldGenContext.lightEngine(), worldGenContext.mainThreadExecutor(), worldGenContext.unsavedListener());
|
||||
|
||||
worldGenContextField.set(chunkMap, newContext);
|
||||
}
|
||||
|
||||
public Vector3d getBoundingbox(org.bukkit.entity.EntityType entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
String descriptionId = "entity.minecraft." + entity.name().toLowerCase(Locale.ROOT);
|
||||
Field[] fields = EntityType.class.getDeclaredFields();
|
||||
for (Field field : fields) {
|
||||
if (!Modifier.isStatic(field.getModifiers()) || !field.getType().equals(EntityType.class)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
EntityType entityType = (EntityType) field.get(null);
|
||||
if (entityType == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (descriptionId.equals(entityType.getDescriptionId())) {
|
||||
return new Vector3d(entityType.getWidth(), entityType.getHeight(), entityType.getWidth());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (Throwable e) {
|
||||
Iris.error("Unable to get entity dimensions for " + entity + "!");
|
||||
Iris.reportError(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Entity spawnEntity(Location location, org.bukkit.entity.EntityType type, CreatureSpawnEvent.SpawnReason reason) {
|
||||
if (location == null || location.getWorld() == null || type == null || type.getEntityClass() == null) {
|
||||
return null;
|
||||
}
|
||||
return ((CraftWorld) location.getWorld()).spawn(location, type.getEntityClass(), null, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Color getBiomeColor(Location location, BiomeColor type) {
|
||||
ServerLevel reader = ((CraftWorld) location.getWorld()).getHandle();
|
||||
var pos = new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ());
|
||||
var holder = reader.getBiome(pos);
|
||||
var biome = holder.value();
|
||||
if (biome == null) throw new IllegalArgumentException("Invalid biome: " + holder.unwrapKey().orElse(null));
|
||||
|
||||
var attributes = reader.environmentAttributes();
|
||||
int rgba = switch (type) {
|
||||
case FOG -> attributes.getValue(EnvironmentAttributes.FOG_COLOR, pos);
|
||||
case WATER -> biome.getWaterColor();
|
||||
case WATER_FOG -> attributes.getValue(EnvironmentAttributes.WATER_FOG_COLOR, pos);
|
||||
case SKY -> attributes.getValue(EnvironmentAttributes.SKY_COLOR, pos);
|
||||
case FOLIAGE -> biome.getFoliageColor();
|
||||
case GRASS -> biome.getGrassColor(location.getBlockX(), location.getBlockZ());
|
||||
};
|
||||
if (rgba == 0) {
|
||||
if (BiomeColor.FOLIAGE == type && biome.getSpecialEffects().foliageColorOverride().isEmpty())
|
||||
return null;
|
||||
if (BiomeColor.GRASS == type && biome.getSpecialEffects().grassColorOverride().isEmpty())
|
||||
return null;
|
||||
}
|
||||
return new Color(rgba, true);
|
||||
}
|
||||
|
||||
private static Field getField(Class<?> clazz, Class<?> fieldType) throws NoSuchFieldException {
|
||||
try {
|
||||
for (Field f : clazz.getDeclaredFields()) {
|
||||
if (f.getType().equals(fieldType))
|
||||
return f;
|
||||
}
|
||||
throw new NoSuchFieldException(fieldType.getName());
|
||||
} catch (NoSuchFieldException var4) {
|
||||
Class<?> superClass = clazz.getSuperclass();
|
||||
if (superClass == null) {
|
||||
throw var4;
|
||||
} else {
|
||||
return getField(superClass, fieldType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Holder<net.minecraft.world.level.biome.Biome> biomeToBiomeBase(Registry<net.minecraft.world.level.biome.Biome> registry, Biome biome) {
|
||||
if (registry == null || biome == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
NamespacedKey biomeKey = resolveBiomeKey(biome);
|
||||
if (biomeKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ResourceKey<net.minecraft.world.level.biome.Biome> key = ResourceKey.create(Registries.BIOME, CraftNamespacedKey.toMinecraft(biomeKey));
|
||||
return registry.get(key).orElse(null);
|
||||
}
|
||||
|
||||
private static NamespacedKey resolveBiomeKey(Biome biome) {
|
||||
Object keyOrNullValue = invokeNoThrow(biome, "getKeyOrNull", new Class<?>[0]);
|
||||
if (keyOrNullValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
Object keyOrThrowValue = invokeNoThrow(biome, "getKeyOrThrow", new Class<?>[0]);
|
||||
if (keyOrThrowValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
Object keyValue = invokeNoThrow(biome, "getKey", new Class<?>[0]);
|
||||
if (keyValue instanceof NamespacedKey namespacedKey) {
|
||||
return namespacedKey;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Object invokeNoThrow(Object target, String methodName, Class<?>[] parameterTypes, Object... args) {
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Method method = target.getClass().getMethod(methodName, parameterTypes);
|
||||
return method.invoke(target, args);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataVersion getDataVersion() {
|
||||
return DataVersion.V26_1_2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpawnChunkCount(World world) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean missingDimensionTypes(String... keys) {
|
||||
var type = registry().lookupOrThrow(Registries.DIMENSION_TYPE);
|
||||
return !Arrays.stream(keys)
|
||||
.map(key -> Identifier.fromNamespaceAndPath("iris", key))
|
||||
.allMatch(type::containsKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean injectBukkit() {
|
||||
if (injected.getAndSet(true))
|
||||
return true;
|
||||
try {
|
||||
Iris.info("Injecting Bukkit");
|
||||
var buddy = new ByteBuddy();
|
||||
buddy.redefine(ServerLevel.class)
|
||||
.visit(Advice.to(ServerLevelAdvice.class).on(ElementMatchers.isConstructor()
|
||||
.and(ElementMatchers.takesArgument(0, MinecraftServer.class))
|
||||
.and(ElementMatchers.takesArgument(5, LevelStem.class))))
|
||||
.make()
|
||||
.load(ServerLevel.class.getClassLoader(), Agent.installed());
|
||||
for (Class<?> clazz : List.of(ChunkAccess.class, ProtoChunk.class)) {
|
||||
buddy.redefine(clazz)
|
||||
.visit(Advice.to(ChunkAccessAdvice.class).on(ElementMatchers.isMethod().and(ElementMatchers.takesArguments(ShortList.class, int.class))))
|
||||
.make()
|
||||
.load(clazz.getClassLoader(), Agent.installed());
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
Iris.error(C.RED + "Failed to inject Bukkit");
|
||||
e.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KMap<Material, List<BlockProperty>> getBlockProperties() {
|
||||
KMap<Material, List<BlockProperty>> states = new KMap<>();
|
||||
|
||||
for (var block : registry().lookupOrThrow(Registries.BLOCK)) {
|
||||
var state = block.defaultBlockState();
|
||||
if (state == null) state = block.getStateDefinition().any();
|
||||
final var finalState = state;
|
||||
|
||||
states.put(CraftMagicNumbers.getMaterial(block), block.getStateDefinition()
|
||||
.getProperties()
|
||||
.stream()
|
||||
.map(p -> createProperty(p, finalState))
|
||||
.toList());
|
||||
}
|
||||
return states;
|
||||
}
|
||||
|
||||
private <T extends Comparable<T>> BlockProperty createProperty(Property<T> property, BlockState state) {
|
||||
return new BlockProperty(property.getName(), property.getValueClass(), state.getValue(property), property.getPossibleValues(), property::getName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object createRuntimeLevelStem(Object registryAccess, ChunkGenerator raw) {
|
||||
if (!(registryAccess instanceof RegistryAccess access)) {
|
||||
throw new IllegalStateException("Runtime LevelStem creation requires a RegistryAccess instance.");
|
||||
}
|
||||
if (!(raw instanceof PlatformChunkGenerator generator)) {
|
||||
throw new IllegalStateException("Generator is not platform chunk generator!");
|
||||
}
|
||||
|
||||
Identifier dimensionKey = Identifier.fromNamespaceAndPath("iris", generator.getTarget().getDimension().getDimensionTypeKey());
|
||||
Holder.Reference<net.minecraft.world.level.dimension.DimensionType> dimensionType = access.lookupOrThrow(Registries.DIMENSION_TYPE)
|
||||
.getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, dimensionKey));
|
||||
return new LevelStem(dimensionType, chunkGenerator(access));
|
||||
}
|
||||
|
||||
private net.minecraft.world.level.chunk.ChunkGenerator chunkGenerator(RegistryAccess access) {
|
||||
var settings = new FlatLevelGeneratorSettings(Optional.empty(), access.lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID), List.of());
|
||||
settings.getLayersInfo().add(new FlatLayerInfo(1, Blocks.AIR));
|
||||
settings.updateLayers();
|
||||
return new FlatLevelSource(settings);
|
||||
}
|
||||
|
||||
private static class ChunkAccessAdvice {
|
||||
@Advice.OnMethodEnter(skipOn = Advice.OnNonDefaultValue.class)
|
||||
static boolean enter(@Advice.This ChunkAccess access, @Advice.Argument(1) int index) {
|
||||
return index >= access.getPostProcessing().length;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServerLevelAdvice {
|
||||
@Advice.OnMethodEnter
|
||||
static void enter(
|
||||
@Advice.Argument(0) MinecraftServer server,
|
||||
@Advice.Argument(2) LevelStorageSource.LevelStorageAccess levelStorageAccess,
|
||||
@Advice.Argument(value = 5, readOnly = false) LevelStem levelStem
|
||||
) {
|
||||
if (levelStorageAccess == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
String levelId = levelStorageAccess.getLevelId();
|
||||
if (levelId == null || levelId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object generator = Class.forName("art.arcane.iris.core.lifecycle.WorldLifecycleStaging", true, Bukkit.getPluginManager().getPlugin("Iris")
|
||||
.getClass()
|
||||
.getClassLoader())
|
||||
.getDeclaredMethod("consumeStemGenerator", String.class)
|
||||
.invoke(null, levelId);
|
||||
if (!(generator instanceof ChunkGenerator gen) || !gen.getClass().getPackageName().startsWith("art.arcane.iris")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object bindings = Class.forName("art.arcane.iris.core.nms.INMS", true, Bukkit.getPluginManager().getPlugin("Iris")
|
||||
.getClass()
|
||||
.getClassLoader())
|
||||
.getDeclaredMethod("get")
|
||||
.invoke(null);
|
||||
if (bindings == null) {
|
||||
throw new IllegalStateException("Iris failed to resolve an INMSBinding instance.");
|
||||
}
|
||||
|
||||
java.lang.reflect.Method stemMethod = null;
|
||||
for (java.lang.reflect.Method candidate : bindings.getClass().getMethods()) {
|
||||
if (candidate.getName().equals("createRuntimeLevelStem") && candidate.getParameterCount() == 2) {
|
||||
stemMethod = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (stemMethod == null) {
|
||||
throw new IllegalStateException("Iris binding is missing createRuntimeLevelStem.");
|
||||
}
|
||||
Object resolvedStem = stemMethod.invoke(bindings, server.registryAccess(), gen);
|
||||
if (!(resolvedStem instanceof LevelStem runtimeStem)) {
|
||||
throw new IllegalStateException("Iris runtime LevelStem binding returned " + (resolvedStem == null ? "null" : resolvedStem.getClass().getName()) + ".");
|
||||
}
|
||||
levelStem = runtimeStem;
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Iris failed to replace the levelStem", e instanceof InvocationTargetException ex ? ex.getCause() : e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,3 +70,4 @@ if (useLocalVolmLib && localVolmLibDirectory != null) {
|
||||
|
||||
include(':core', ':core:agent')
|
||||
include(':nms:v1_21_R7')
|
||||
include(':nms:v26_1_R1')
|
||||
|
||||
Reference in New Issue
Block a user