Tracked generated content ownership

- Generated furniture keeps ownership tokens for targeted removal
- Native writes invalidate pending custom content and tile records
- Regeneration and chunk updates remove invalidated content on owning threads
- Provider callbacks return ownership tokens across server platforms
- Regression fixtures cover terrain replacement and restart cleanup
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-18 17:58:30 -04:00
1 parent ed5483d75a
commit 34417b2f5b
53 files changed
+2082 -110

No files matched your search

@@ -0,0 +1,43 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.integration.Identifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public record CustomBlockOwnership(List<Entry> entries) {
public CustomBlockOwnership {
entries = List.copyOf(entries);
}
public CustomBlockOwnership add(Entry entry) {
if (entries.contains(entry)) {
return this;
}
List<Entry> updated = new ArrayList<>(entries);
updated.add(entry);
return new CustomBlockOwnership(updated);
}
public CustomBlockOwnership invalidate() {
List<Entry> updated = new ArrayList<>(entries.size());
for (Entry entry : entries) {
updated.add(new Entry(entry.identifier(), entry.token(), true));
}
return new CustomBlockOwnership(updated);
}
public CustomBlockOwnership remove(Entry entry) {
List<Entry> updated = new ArrayList<>(entries);
updated.remove(entry);
return updated.isEmpty() ? null : new CustomBlockOwnership(updated);
}
public record Entry(Identifier identifier, String token, boolean invalidated) {
public Entry {
Objects.requireNonNull(identifier);
Objects.requireNonNull(token);
}
}
}
@@ -1,18 +1,91 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EnginePlatformHooks;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
import art.arcane.volmlib.nativelib.terrain.NativeWorld;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.flag.ReservedFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.MantleClosedException;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public final class DeferredPlacementRecords {
private static final Set<MantleFlag> RETAINED_MARGIN_FLAGS = Set.of(
MantleFlag.TILE, MantleFlag.CUSTOM, MantleFlag.UPDATE, MantleFlag.ETCHED,
MantleFlag.INITIAL_SPAWNED, MantleFlag.INITIAL_SPAWNED_MARKER, MantleFlag.DISCOVERED);
private DeferredPlacementRecords() {
}
public static void resetMantleChunk(Mantle<Matter> mantle, int chunkX, int chunkZ, boolean target) {
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
try {
synchronized (chunk) {
for (int index = 0; index < chunk.sectionCount(); index++) {
Matter section = chunk.get(index);
if (section == null) {
continue;
}
section.getSliceTypes().removeIf(type -> type != CustomBlockOwnership.class);
if (section.isEmpty()) {
chunk.delete(index);
}
}
for (ReservedFlag flag : ReservedFlag.values()) {
if (target || !RETAINED_MARGIN_FLAGS.contains(flag)) {
chunk.flag(flag, false);
}
}
for (int ordinal = MantleFlag.MIN_ORDINAL; ordinal <= MantleFlag.MAX_ORDINAL; ordinal++) {
chunk.flag(MantleFlag.of(ordinal), false);
}
}
} finally {
chunk.release();
}
}
public static void invalidateCustomBlockOwnership(MantleChunk<Matter> chunk) {
synchronized (chunk) {
List<Pending<CustomBlockOwnership>> records =
snapshot(chunk, CustomBlockOwnership.class);
for (Pending<CustomBlockOwnership> record : records) {
NativeBlockPoint position = record.position();
chunk.get(position.y() >> 4).slice(CustomBlockOwnership.class)
.set(position.x() & 15, position.y() & 15, position.z() & 15, record.value().invalidate());
}
}
}
public static <T> List<Pending<T>> snapshot(MantleChunk<Matter> chunk, Class<T> type) {
List<Pending<T>> records = new ArrayList<>();
synchronized (chunk) {
chunk.iterate(type, (x, y, z, value) -> records.add(new Pending<>(new NativeBlockPoint(x, y, z), value)));
}
return records;
}
public static <T> boolean isPending(MantleChunk<Matter> chunk, Class<T> type, Pending<T> record) {
NativeBlockPoint position = record.position();
synchronized (chunk) {
return Objects.equals(record.value(), chunk.get(position.x(), position.y(), position.z(), type));
}
}
public static NativeBlockChangeSet track(Engine engine, NativeBlockChangeSet.Bounds bounds) {
Mantle<Matter> mantle = engine.getMantle().getMantle();
for (int chunkX = bounds.minChunkX(); chunkX <= bounds.maxChunkX(); chunkX++) {
@@ -31,7 +104,163 @@ public final class DeferredPlacementRecords {
}
Mantle<Matter> mantle = engine.getMantle().getMantle();
int minHeight = engine.getMinHeight();
changes.forEachChunk((chunkX, chunkZ) -> invalidateChunk(mantle, changes, chunkX, chunkZ, minHeight));
changes.forEachChunk((chunkX, chunkZ) -> {
MantleChunk<Matter> chunk = invalidateChunk(mantle, changes, chunkX, chunkZ, minHeight);
if (chunk != null) {
scheduleRemoval(engine, chunk, minHeight);
}
});
}
public static NativeBlockChangeSet positions(Engine engine, NativeBlockChangeSet.Bounds bounds) {
Mantle<Matter> mantle = engine.getMantle().getMantle();
NativeBlockChangeSet positions = null;
int minHeight = engine.getMinHeight();
for (int chunkX = bounds.minChunkX(); chunkX <= bounds.maxChunkX(); chunkX++) {
for (int chunkZ = bounds.minChunkZ(); chunkZ <= bounds.maxChunkZ(); chunkZ++) {
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
try {
synchronized (chunk) {
if (!hasRecords(chunk)) {
continue;
}
if (positions == null) {
positions = NativeBlockChangeSet.create(bounds);
}
collectPositions(chunk, positions, Identifier.class, minHeight);
collectPositions(chunk, positions, TileWrapper.class, minHeight);
collectPositions(chunk, positions, CustomBlockOwnership.class, minHeight);
}
} finally {
chunk.release();
}
}
}
return positions == null || positions.isEmpty() ? NativeBlockChangeSet.disabled() : positions;
}
public static void remember(Engine engine, MantleChunk<Matter> chunk, NativeBlockPoint position,
CustomBlockOwnership.Entry entry) {
boolean invalidated;
synchronized (chunk) {
Matter section = chunk.getOrCreate(position.y() >> 4);
MatterSlice<Identifier> deferred = section.getSlice(Identifier.class);
invalidated = entry.invalidated() || deferred == null
|| deferred.get(position.x() & 15, position.y() & 15, position.z() & 15) == null;
CustomBlockOwnership.Entry completed = invalidated
? new CustomBlockOwnership.Entry(entry.identifier(), entry.token(), true) : entry;
MatterSlice<CustomBlockOwnership> slice = section.slice(CustomBlockOwnership.class);
CustomBlockOwnership existing = slice.get(position.x() & 15, position.y() & 15, position.z() & 15);
CustomBlockOwnership updated = existing == null
? new CustomBlockOwnership(List.of(completed)) : existing.add(completed);
slice.set(position.x() & 15, position.y() & 15, position.z() & 15, updated);
}
if (invalidated) {
scheduleRemoval(engine, chunk, engine.getMinHeight());
}
}
public static void removeInvalidated(Engine engine, int chunkX, int chunkZ) {
removeInvalidated(engine, engine.getMantle().getMantle(), chunkX, chunkZ);
}
public static void removeInvalidated(Engine engine, Mantle<Matter> mantle, int chunkX, int chunkZ) {
NativeWorld world = engine.getWorld().platformWorld();
if (world == null || engine.isClosed() || !world.isChunkLoaded(chunkX, chunkZ)) {
return;
}
if (mantle.isClosed()) {
return;
}
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
try {
removeInvalidated(engine, chunk, engine.getMinHeight());
} finally {
chunk.release();
}
}
private static void scheduleRemoval(Engine engine, MantleChunk<Matter> chunk, int minHeight) {
NativeWorld world = engine.getWorld().platformWorld();
if (world != null && IrisPlatforms.isBound()) {
IrisPlatforms.get().scheduler().region(world, chunk.getX(), chunk.getZ(),
() -> removeScheduled(new ScheduledRemoval(engine, world, chunk, minHeight)));
}
}
private static void removeScheduled(ScheduledRemoval removal) {
MantleChunk<Matter> chunk = removal.chunk();
if (removal.engine().isClosed() || chunk.isClosed()
|| !removal.world().isChunkLoaded(chunk.getX(), chunk.getZ())) {
return;
}
try {
chunk.use();
} catch (MantleClosedException closed) {
IrisLogging.reportError("Custom content cleanup deferred after mantle chunk "
+ chunk.getX() + "," + chunk.getZ() + " closed.", closed);
return;
}
try {
removeInvalidated(removal.engine(), chunk, removal.minHeight());
} finally {
chunk.release();
}
}
private static void removeInvalidated(Engine engine, MantleChunk<Matter> chunk, int minHeight) {
EnginePlatformHooks hooks = IrisServices.getOrNull(EnginePlatformHooks.class);
if (hooks == null) {
return;
}
int baseX = chunk.getX() << 4;
int baseZ = chunk.getZ() << 4;
List<Removal> removals = new ArrayList<>();
synchronized (chunk) {
chunk.iterate(CustomBlockOwnership.class, (x, y, z, ownership) -> {
for (CustomBlockOwnership.Entry entry : ownership.entries()) {
if (entry.invalidated()) {
removals.add(new Removal(new NativeBlockPoint(baseX + (x & 15),
y + minHeight, baseZ + (z & 15)), entry));
}
}
});
}
for (Removal removal : removals) {
try {
if (hooks.removeCustomBlock(engine, removal.position(), removal.entry())) {
forgetRemoval(chunk, removal, minHeight);
}
} catch (RuntimeException | LinkageError error) {
IrisLogging.reportError("Failed to remove generated custom content at " + removal.position() + ".", error);
}
}
}
private static <T> void collectPositions(MantleChunk<Matter> chunk, NativeBlockChangeSet positions,
Class<T> type, int minHeight) {
int baseX = chunk.getX() << 4;
int baseZ = chunk.getZ() << 4;
chunk.iterate(type, (x, y, z, value) -> positions.add(baseX + (x & 15), y + minHeight, baseZ + (z & 15)));
}
private static void forgetRemoval(MantleChunk<Matter> chunk, Removal removal, int minHeight) {
NativeBlockPoint position = removal.position();
int y = position.y() - minHeight;
synchronized (chunk) {
Matter section = chunk.get(y >> 4);
if (section == null) {
return;
}
MatterSlice<CustomBlockOwnership> slice = section.getSlice(CustomBlockOwnership.class);
if (slice == null) {
return;
}
CustomBlockOwnership ownership = slice.get(position.x() & 15, y & 15, position.z() & 15);
if (ownership != null) {
slice.set(position.x() & 15, y & 15, position.z() & 15, ownership.remove(removal.entry()));
}
}
}
private static boolean hasRecords(Mantle<Matter> mantle, int chunkX, int chunkZ) {
@@ -48,38 +277,51 @@ public final class DeferredPlacementRecords {
private static boolean hasRecords(MantleChunk<Matter> chunk) {
for (int section = 0; section < chunk.sectionCount(); section++) {
Matter matter = chunk.get(section);
if (matter != null && (matter.hasSlice(Identifier.class) || matter.hasSlice(TileWrapper.class))) {
if (matter != null && (matter.hasSlice(Identifier.class) || matter.hasSlice(TileWrapper.class)
|| matter.hasSlice(CustomBlockOwnership.class))) {
return true;
}
}
return false;
}
private static void invalidateChunk(Mantle<Matter> mantle, NativeBlockChangeSet changes,
private static MantleChunk<Matter> invalidateChunk(Mantle<Matter> mantle, NativeBlockChangeSet changes,
int chunkX, int chunkZ, int minHeight) {
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
try {
synchronized (chunk) {
if (!hasRecords(chunk)) {
return;
return null;
}
changes.forEachPosition(chunkX, chunkZ, (x, y, z) -> clearRecords(chunk, x & 15, y - minHeight, z & 15));
boolean[] cleanup = new boolean[1];
changes.forEachPosition(chunkX, chunkZ, (x, y, z) ->
cleanup[0] |= clearRecords(chunk, x & 15, y - minHeight, z & 15));
return cleanup[0] ? chunk : null;
}
} finally {
chunk.release();
}
}
private static void clearRecords(MantleChunk<Matter> chunk, int x, int y, int z) {
private static boolean clearRecords(MantleChunk<Matter> chunk, int x, int y, int z) {
if (y < 0 || (y >> 4) >= chunk.sectionCount()) {
return;
return false;
}
Matter section = chunk.get(y >> 4);
if (section == null) {
return;
return false;
}
clearRecord(section.getSlice(Identifier.class), x, y & 15, z);
clearRecord(section.getSlice(TileWrapper.class), x, y & 15, z);
MatterSlice<CustomBlockOwnership> ownership = section.getSlice(CustomBlockOwnership.class);
if (ownership != null) {
CustomBlockOwnership current = ownership.get(x, y & 15, z);
if (current != null) {
ownership.set(x, y & 15, z, current.invalidate());
return true;
}
}
return false;
}
private static void clearRecord(MatterSlice<?> slice, int x, int y, int z) {
@@ -87,4 +329,13 @@ public final class DeferredPlacementRecords {
slice.set(x, y, z, null);
}
}
private record Removal(NativeBlockPoint position, CustomBlockOwnership.Entry entry) {
}
public record Pending<T>(NativeBlockPoint position, T value) {
}
private record ScheduledRemoval(Engine engine, NativeWorld world, MantleChunk<Matter> chunk, int minHeight) {
}
}
@@ -21,6 +21,8 @@ package art.arcane.iris.generation.runtime;
import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
import art.arcane.iris.world.history.SavedTerrainChunk;
import java.util.concurrent.CompletableFuture;
@@ -28,6 +30,10 @@ import art.arcane.iris.generation.mantle.MantleComponent;
import art.arcane.volmlib.util.collection.KList;
public interface EnginePlatformHooks {
default boolean removeCustomBlock(Engine engine, NativeBlockPoint position, CustomBlockOwnership.Entry entry) {
return false;
}
default CompletableFuture<Void> flushSavedTerrainCapture(Engine engine) {
return CompletableFuture.failedFuture(new UnsupportedOperationException(
"This platform does not support saved terrain checkpoints."));
@@ -44,6 +44,7 @@ import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockDropItemEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.world.EntitiesLoadEvent;
import java.util.concurrent.Future;
import java.util.concurrent.CompletableFuture;
@@ -249,6 +250,16 @@ public class IrisWorldManager extends EngineAssignedWorldManager {
}
}
@EventHandler
public void onEntitiesLoad(EntitiesLoadEvent event) {
runManagerTask("bukkit_world_manager_entities_load", () -> {
if (!event.getEntities().isEmpty()
&& event.getWorld().equals(BukkitWorldBinding.world(getEngine().getWorld()))) {
chunkMaintenance.onEntitiesLoaded(event.getChunk());
}
});
}
@Override
public void onChunkUnload(Chunk e) {
final var future = cleanup.remove(Cache.key(e.getX(), e.getZ()));
@@ -104,6 +104,16 @@ final class WorldChunkMaintenance {
}
}
void onEntitiesLoaded(Chunk chunk) {
int chunkX = chunk.getX();
int chunkZ = chunk.getZ();
World world = chunk.getWorld();
invalidateMaterialization(chunkX, chunkZ);
J.runRegion(world, chunkX, chunkZ,
manager.managedTask("bukkit_world_manager_entity_content_cleanup",
() -> scheduleChunkUpdate(world, chunkX, chunkZ)), 1);
}
void discoverChunks() {
World world = BukkitWorldBinding.world(manager.getEngine().getWorld());
if (world == null) {
@@ -120,7 +120,13 @@ public abstract class ExternalDataProvider {
* @param block The block where the block should be placed
* @param blockId The blockId to place
*/
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {}
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
return null;
}
public boolean removeGenerated(@NotNull Block block, @NotNull String token) {
return false;
}
/**
* Spawns a mob in the specified location using the given engine and entity identifier.
@@ -19,6 +19,7 @@
package art.arcane.iris.integration;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
import art.arcane.iris.platform.bukkit.BukkitPlatform;
import art.arcane.iris.integration.data.DataType;
import art.arcane.volmlib.nativelib.terrain.BlockProperty;
@@ -247,13 +248,24 @@ public class ExternalDataSVC implements IrisService {
}
}
public void processUpdate(Engine engine, Block block, Identifier blockId) {
public CustomBlockOwnership.Entry processUpdate(Engine engine, Block block, Identifier blockId) {
Pair<Identifier, KMap<String, String>> state = parseState(blockId);
BlockProvider match = findBlockProvider(state.getA());
if (match == null) {
throw new MissingResourceException("No matching provider found for external block placement.", blockId.namespace(), blockId.key());
}
match.provider().processUpdate(engine, block, buildState(match.identifier(), state.getB()));
String token = match.provider().processUpdate(engine, block, buildState(match.identifier(), state.getB()));
return token == null ? null : new CustomBlockOwnership.Entry(
qualifyBlockId(match.provider(), buildState(match.identifier(), state.getB())), token, false);
}
public boolean removeGenerated(Block block, CustomBlockOwnership.Entry entry) {
for (ExternalDataProvider provider : activeProviders) {
if (provider.getPluginId().equalsIgnoreCase(entry.identifier().namespace()) && provider.isReady()) {
return provider.removeGenerated(block, entry.token());
}
}
return false;
}
public boolean placeBlock(Block block, Identifier blockId) {
@@ -18,6 +18,7 @@ import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
import net.momirealms.craftengine.bukkit.api.CraftEngineItems;
import net.momirealms.craftengine.bukkit.api.event.CraftEngineReloadEvent;
import net.momirealms.craftengine.bukkit.item.BukkitItemDefinition;
import net.momirealms.craftengine.bukkit.entity.furniture.BukkitFurniture;
import net.momirealms.craftengine.bukkit.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.core.block.BlockDefinition;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
@@ -27,9 +28,11 @@ import net.momirealms.craftengine.core.block.property.Property;
import net.momirealms.craftengine.core.entity.furniture.FurnitureDefinition;
import net.momirealms.craftengine.core.util.Key;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.entity.Entity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
@@ -43,6 +46,7 @@ import java.util.Map;
import java.util.MissingResourceException;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -140,21 +144,38 @@ public class CraftEngineDataProvider extends ExternalDataProvider implements Lis
}
@Override
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
Identifier baseBlockId = statePair.getA();
KMap<String, String> state = statePair.getB();
ImmutableBlockState blockState = findBlockState(baseBlockId);
if (blockState != null) {
placeBlockState(block, applyProperties(baseBlockId, blockState, state), blockId);
return;
return null;
}
FurnitureDefinition furniture = requireFurniture(baseBlockId);
KMap<String, String> properties = validateFurnitureProperties(baseBlockId, furniture, state);
Location location = parseYawAndPitch(engine, block, properties);
if (CraftEngineFurniture.place(location, furniture, properties.get("variant"), false) == null) {
BukkitFurniture placed = CraftEngineFurniture.place(location, furniture, properties.get("variant"), false);
if (placed == null) {
throw missing(blockId, "Failed to place CraftEngine furniture.");
}
return placed.baseEntity().getUniqueId().toString();
}
@Override
public boolean removeGenerated(@NotNull Block block, @NotNull String token) {
Chunk chunk = block.getChunk();
if (!chunk.isEntitiesLoaded()) {
return false;
}
UUID entityId = UUID.fromString(token);
for (Entity entity : chunk.getEntities()) {
if (entityId.equals(entity.getUniqueId())) {
return CraftEngineFurniture.remove(entity, false, false);
}
}
return true;
}
@Override
@@ -84,10 +84,11 @@ public class ItemAdderDataProvider extends ExternalDataProvider implements Liste
}
@Override
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
if (CustomBlock.place(blockId.toString(), block.getLocation()) == null) {
throw new MissingResourceException("Failed to place ItemsAdder block.", blockId.namespace(), blockId.key());
}
return null;
}
@Override
@@ -47,17 +47,18 @@ public class KGeneratorsDataProvider extends ExternalDataProvider {
}
@Override
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
if (block.getType() != Material.STRUCTURE_VOID) return;
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
if (block.getType() != Material.STRUCTURE_VOID) return null;
IGeneratorLocation existing = KGeneratorsAPI.getLoadedGeneratorLocation(block.getLocation());
if (existing != null) return;
if (existing != null) return null;
block.setBlockData(BukkitBlockResolution.getAir(), false);
Generator generator = Main.getGenerators().get(blockId.key());
if (generator == null) return;
if (generator == null) return null;
GeneratorLocation location = new GeneratorLocation(-1, generator, block.getLocation(), Main.getPlacedGenerators().getChunkInfo(block.getChunk()), null, null);
Main.getDatabases().getDb().saveGenerator(location);
Main.getPlacedGenerators().addLoaded(location);
Main.getSchedules().schedule(location, true);
return null;
}
@Override
@@ -112,15 +112,15 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider {
}
@Override
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
Pair<Identifier, KMap<String, String>> parsedState = ExternalDataSVC.parseState(blockId);
KMap<String, String> state = parsedState.getB();
blockId = parsedState.getA();
Optional<CrucibleItem> item = itemManager.getItem(blockId.key());
if (item.isEmpty()) return;
if (item.isEmpty()) return null;
FurnitureItemContext furniture = item.get().getFurnitureData();
if (furniture == null) return;
if (furniture == null) return null;
Pair<Float, BlockFace> pair = parseYawAndFace(engine, block, state);
BiomeColor type = null;
@@ -130,10 +130,11 @@ public class MythicCrucibleDataProvider extends ExternalDataProvider {
} catch (NullPointerException | IllegalArgumentException ignored) {}
if (type != null) {
Color biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
if (biomeColor == null) return;
if (biomeColor == null) return null;
color = Chroma.of(biomeColor.getRGB());
}
furniture.place(block, pair.getB(), pair.getA(), color);
return null;
}
@Override
@@ -89,24 +89,24 @@ public class NexoDataProvider extends ExternalDataProvider {
}
@Override
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
KMap<String, String> state = statePair.getB();
blockId = statePair.getA();
if (NexoBlocks.isCustomBlock(blockId.key())) {
NexoBlocks.place(blockId.key(), block.getLocation());
return;
return null;
}
if (!NexoFurniture.isFurniture(blockId.key()))
return;
return null;
Pair<Float, BlockFace> pair = parseYawAndFace(engine, block, state);
ItemDisplay display = NexoFurniture.place(blockId.key(), block.getLocation(), pair.getA(), pair.getB());
if (display == null) return;
if (display == null) return null;
ItemStack itemStack = display.getItemStack();
if (itemStack == null) return;
if (itemStack == null) return null;
BiomeColor type = null;
try {
@@ -115,7 +115,7 @@ public class NexoDataProvider extends ExternalDataProvider {
if (type != null) {
Color biomeColor = INMS.get().getBiomeColor(block.getLocation(), type);
if (biomeColor == null) return;
if (biomeColor == null) return null;
ItemMeta meta = itemStack.getItemMeta();
switch (meta) {
case LeatherArmorMeta armor -> armor.setColor(fromARGB(biomeColor.getAlpha(), biomeColor.getRed(), biomeColor.getGreen(), biomeColor.getBlue()));
@@ -126,6 +126,7 @@ public class NexoDataProvider extends ExternalDataProvider {
itemStack.setItemMeta(meta);
}
display.setItemStack(itemStack);
return null;
}
@Override
@@ -61,8 +61,9 @@ public final class OraxenDataProvider extends ExternalDataProvider implements Li
}
@Override
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
public String processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
OraxenBlocks.place(blockId.key(), block.getLocation());
return null;
}
@Override
@@ -23,6 +23,9 @@ import art.arcane.iris.world.history.GenerationFindCatalog;
import art.arcane.iris.world.event.IrisLootEvent;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.integration.ExternalDataSVC;
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
import art.arcane.volmlib.nativelib.terrain.NativeBlockPoint;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.generation.locator.Locator;
@@ -137,13 +140,17 @@ public final class EngineBukkitOps {
Runnable tileTask = () -> materializeTiles(engine, c, chunk);
Runnable customTask = () -> materializeCustomBlocks(engine, c, chunk);
Runnable updateTask = () -> materializeUpdates(engine, c, chunk);
Runnable materialize = () -> {
DeferredPlacementRecords.removeInvalidated(engine, mantle, c.getX(), c.getZ());
runMaterializationPasses(chunk, tileTask, customTask, updateTask);
};
if (shouldRunChunkUpdateInline(c)) {
runMaterializationPasses(chunk, tileTask, customTask, updateTask);
materialize.run();
return;
}
runScheduledMaterializationPasses(c, () -> runMaterializationPasses(chunk, tileTask, customTask, updateTask));
runScheduledMaterializationPasses(c, materialize);
} finally {
chunk.release();
}
@@ -151,14 +158,19 @@ public final class EngineBukkitOps {
static void materializeTiles(Engine engine, Chunk chunk, MantleChunk<Matter> mantleChunk) {
StaleRecords stale = new StaleRecords();
mantleChunk.iterate(TileWrapper.class, (x, y, z, value) -> {
int worldY = y + engine.getWorld().minHeight();
if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) {
return;
for (DeferredPlacementRecords.Pending<TileWrapper> record : DeferredPlacementRecords.snapshot(mantleChunk, TileWrapper.class)) {
if (!DeferredPlacementRecords.isPending(mantleChunk, TileWrapper.class, record)) {
continue;
}
Block block = chunk.getBlock(x & 15, worldY, z & 15);
NativeBlockPoint position = record.position();
TileWrapper value = record.value();
int worldY = position.y() + engine.getWorld().minHeight();
if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) {
continue;
}
Block block = chunk.getBlock(position.x() & 15, worldY, position.z() & 15);
if (stale.tileReplaced(block, value.getData())) {
return;
continue;
}
if (!TileData.setTileState(block, value.getData())) {
NamespacedKey blockTypeKey = KeyedType.getKey(block.getType());
@@ -166,25 +178,33 @@ public final class EngineBukkitOps {
String tileType = value.getData().getMaterialKey();
IrisLogging.warn("Failed to set tile entity data at [%d %d %d | %s] for tile %s!", block.getX(), block.getY(), block.getZ(), blockType, tileType);
}
});
}
mantleChunk.deleteSlices(TileWrapper.class);
stale.report("tile", chunk);
}
static void materializeCustomBlocks(Engine engine, Chunk chunk, MantleChunk<Matter> mantleChunk) {
StaleRecords stale = new StaleRecords();
mantleChunk.iterate(Identifier.class, (x, y, z, value) -> {
int worldY = y + engine.getWorld().minHeight();
for (DeferredPlacementRecords.Pending<Identifier> record : DeferredPlacementRecords.snapshot(mantleChunk, Identifier.class)) {
if (!DeferredPlacementRecords.isPending(mantleChunk, Identifier.class, record)) {
continue;
}
NativeBlockPoint position = record.position();
Identifier value = record.value();
int worldY = position.y() + engine.getWorld().minHeight();
if (worldY < chunk.getWorld().getMinHeight() || worldY >= chunk.getWorld().getMaxHeight()) {
return;
continue;
}
ExternalDataSVC service = IrisServices.get(ExternalDataSVC.class);
Block block = chunk.getBlock(x & 15, worldY, z & 15);
Block block = chunk.getBlock(position.x() & 15, worldY, position.z() & 15);
if (stale.carrierReplaced(service, block, value)) {
return;
continue;
}
service.processUpdate(engine, block, value);
});
CustomBlockOwnership.Entry ownership = service.processUpdate(engine, block, value);
if (ownership != null) {
DeferredPlacementRecords.remember(engine, mantleChunk, position, ownership);
}
}
mantleChunk.deleteSlices(Identifier.class);
stale.report("custom block", chunk);
}
@@ -877,7 +897,10 @@ public final class EngineBukkitOps {
carrier = carrierType(service, record);
carriers.put(record, carrier);
}
if (carrier == Material.AIR || block.getType() == carrier) {
Material live = block.getType();
if (carrier == Material.AIR || live == carrier
|| ((carrier == Material.FIRE || carrier == Material.SOUL_FIRE)
&& (live == Material.AIR || live == Material.CAVE_AIR || live == Material.VOID_AIR))) {
return false;
}
skipped++;
@@ -1,7 +1,9 @@
package art.arcane.iris.structure.nativegen;
import art.arcane.volmlib.nativelib.terrain.NativeBlockPositionPredicate;
import art.arcane.volmlib.nativelib.terrain.NativeBlockChangeSet;
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.biome.IrisBiome;
@@ -48,6 +50,16 @@ public class ImportedFeaturePolicy implements NativeImportedFeaturePolicy<IrisDi
return NativeFeatureGenerationPolicy.control(engine);
}
@Override
public NativeBlockChangeSet trackedBlockPositions(NativeBlockChangeSet.Bounds bounds) {
return DeferredPlacementRecords.positions(engine, bounds);
}
@Override
public void invalidateBlockChanges(NativeBlockChangeSet changes) {
DeferredPlacementRecords.invalidate(engine, changes);
}
@Override
public Set<String> visibleBiomeKeys() {
Set<String> keys = new LinkedHashSet<>();
@@ -28,11 +28,14 @@ import art.arcane.iris.generation.chunk.TerrainChunkBiomeHunkView;
import art.arcane.iris.platform.generation.EngineBukkitOps;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.mantle.EngineMantle;
import art.arcane.iris.generation.mantle.DeferredPlacementRecords;
import art.arcane.volmlib.nativelib.terrain.NativeBiome;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.platform.bukkit.plugin.VolmitSender;
import art.arcane.iris.world.task.J;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import org.bukkit.Chunk;
import org.bukkit.ChunkSnapshot;
import org.bukkit.Material;
@@ -96,11 +99,12 @@ public final class InPlaceChunkRegenerator {
private void resetMantleMargin() {
EngineMantle engineMantle = engine.getMantle();
Mantle mantle = engineMantle.getMantle();
Mantle<Matter> mantle = engineMantle.getMantle();
int margin = radius + Math.max(engineMantle.getRadius(), engineMantle.getRealRadius()) + 1;
for (int dx = -margin; dx <= margin; dx++) {
for (int dz = -margin; dz <= margin; dz++) {
mantle.deleteChunk(centerChunkX + dx, centerChunkZ + dz);
DeferredPlacementRecords.resetMantleChunk(mantle, centerChunkX + dx, centerChunkZ + dz,
Math.abs(dx) <= radius && Math.abs(dz) <= radius);
}
}
}
@@ -188,7 +192,14 @@ public final class InPlaceChunkRegenerator {
}
}
EngineBukkitOps.updateChunk(engine, chunk);
Mantle<Matter> mantle = engine.getMantle().getMantle();
MantleChunk<Matter> records = mantle.useChunk(chunkX, chunkZ);
try {
DeferredPlacementRecords.invalidateCustomBlockOwnership(records);
} finally {
records.release();
}
EngineBukkitOps.updateChunk(engine, chunk, mantle);
world.refreshChunk(chunkX, chunkZ);
}
@@ -0,0 +1,53 @@
package art.arcane.iris.world.storage.matter;
import art.arcane.iris.generation.mantle.CustomBlockOwnership;
import art.arcane.iris.integration.Identifier;
import art.arcane.volmlib.util.data.palette.Palette;
import art.arcane.volmlib.util.matter.Sliced;
import art.arcane.volmlib.util.matter.slices.RawMatter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Sliced
public final class CustomBlockOwnershipMatter extends RawMatter<CustomBlockOwnership> {
public CustomBlockOwnershipMatter() {
this(1, 1, 1);
}
public CustomBlockOwnershipMatter(int width, int height, int depth) {
super(width, height, depth, CustomBlockOwnership.class);
}
@Override
public Palette<CustomBlockOwnership> getGlobalPalette() {
return null;
}
@Override
public void writeNode(CustomBlockOwnership ownership, DataOutputStream output) throws IOException {
output.writeInt(ownership.entries().size());
for (CustomBlockOwnership.Entry entry : ownership.entries()) {
output.writeUTF(entry.identifier().toString());
output.writeUTF(entry.token());
output.writeBoolean(entry.invalidated());
}
}
@Override
public CustomBlockOwnership readNode(DataInputStream input) throws IOException {
int size = input.readInt();
if (size < 0 || size > 65_536) {
throw new IOException("Invalid custom block ownership count: " + size);
}
List<CustomBlockOwnership.Entry> entries = new ArrayList<>(size);
for (int index = 0; index < size; index++) {
entries.add(new CustomBlockOwnership.Entry(Identifier.fromString(input.readUTF()),
input.readUTF(), input.readBoolean()));
}
return new CustomBlockOwnership(entries);
}
}
@@ -40,6 +40,7 @@ public final class IrisMatterSupport {
}
IrisMatter.registerSliceType(new EntityMatter());
IrisMatter.registerSliceType(new CustomBlockOwnershipMatter());
IrisMatter.registerSliceType(new ObjectContinuationMatter());
IrisMatter.registerSliceType("art.arcane.iris.core.link.Identifier", new IdentifierMatter());
IrisMatter.registerSliceType("art.arcane.iris.engine.framework.NativeStructureOwnershipBundle", new NativeStructureOwnershipMatter());