CrantEngine Tweak and a bit more.

- Fixed CE support
- Added some checks for things like paint and so on in the 3d Generator.
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-08 19:22:55 -04:00
1 parent 0782bbf0b4
commit 4de1aea8b9
44 files changed
+3161 -244

No files matched your search

@@ -27,6 +27,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.MissingResourceException;
import java.util.Optional;
@Getter
@RequiredArgsConstructor
@@ -46,6 +47,14 @@ public abstract class ExternalDataProvider {
public abstract void init();
public Optional<Identifier> identifyBlock(@NotNull BlockData blockData) {
return Optional.empty();
}
public boolean placeBlock(@NotNull Block block, @NotNull Identifier blockId) {
return false;
}
/**
* @see ExternalDataProvider#getBlockData(Identifier, KMap)
*/
@@ -1,20 +1,24 @@
package art.arcane.iris.core.link.data;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.core.link.ExternalDataProvider;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.nms.container.BlockProperty;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.engine.data.cache.Cache;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.platform.bukkit.BukkitBlockResolution;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.volmlib.util.collection.KMap;
import art.arcane.volmlib.util.math.RNG;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
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.plugin.BukkitCraftEngine;
import net.momirealms.craftengine.core.block.BlockDefinition;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.property.BooleanProperty;
@@ -22,27 +26,36 @@ import net.momirealms.craftengine.core.block.property.IntegerProperty;
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.Location;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.Collectors;
public class CraftEngineDataProvider extends ExternalDataProvider {
private static final BlockProperty[] FURNITURE_PROPERTIES = new BlockProperty[]{
public class CraftEngineDataProvider extends ExternalDataProvider implements Listener {
private static final List<BlockProperty> FURNITURE_PROPERTIES = List.of(
BlockProperty.ofBoolean("randomYaw", false),
BlockProperty.ofDouble("yaw", 0, 0, 360f, false, true),
BlockProperty.ofBoolean("randomPitch", false),
BlockProperty.ofDouble("pitch", 0, 0, 360f, false, true),
};
BlockProperty.ofDouble("pitch", 0, 0, 360f, false, true)
);
private static final ContentRegistry EMPTY_CONTENT = new ContentRegistry(Set.of(), Set.of(), false);
private volatile ContentRegistry content = EMPTY_CONTENT;
public CraftEngineDataProvider() {
super("CraftEngine");
@@ -50,130 +63,244 @@ public class CraftEngineDataProvider extends ExternalDataProvider {
@Override
public void init() {
content = EMPTY_CONTENT;
BukkitCraftEngine plugin = BukkitCraftEngine.instance();
if (plugin != null && plugin.isFullyLoaded()) {
updateContent();
}
}
@Override
public boolean isReady() {
return content.ready() && super.isReady();
}
@EventHandler
public void onReload(CraftEngineReloadEvent event) {
if (!updateContent()) {
return;
}
ExternalDataSVC service = IrisServices.getOrNull(ExternalDataSVC.class);
if (service != null) {
service.notifyContentChanged();
}
}
@Override
public @NotNull List<BlockProperty> getBlockProperties(@NotNull Identifier blockId) throws MissingResourceException {
Key key = Key.of(blockId.namespace(), blockId.key());
BlockDefinition block = CraftEngineBlocks.byId(key);
if (block != null) {
return block.properties().stream().map(CraftEngineDataProvider::convert).toList();
ImmutableBlockState blockState = findBlockState(blockId);
if (blockState != null) {
return blockState.owner().value().properties().stream().map(CraftEngineDataProvider::convert).toList();
}
FurnitureDefinition furniture = CraftEngineFurniture.byId(key);
if (furniture != null) {
BlockProperty[] properties = Arrays.copyOf(FURNITURE_PROPERTIES, 5);
properties[4] = new BlockProperty(
"variant",
String.class,
furniture.anyVariantName(),
furniture.variants().keySet(),
Function.identity()
);
return List.of(properties);
}
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
FurnitureDefinition furniture = requireFurniture(blockId);
List<BlockProperty> properties = new ArrayList<>(FURNITURE_PROPERTIES);
properties.add(new BlockProperty("variant", String.class, defaultVariant(furniture),
furniture.variants().keySet(), Function.identity()));
return List.copyOf(properties);
}
@Override
public @NotNull ItemStack getItemStack(@NotNull Identifier itemId, @NotNull KMap<String, Object> customNbt) throws MissingResourceException {
BukkitItemDefinition item = CraftEngineItems.byId(Key.of(itemId.namespace(), itemId.key()));
if (item == null) {
throw new MissingResourceException("Failed to find ItemData!", itemId.namespace(), itemId.key());
throw missing(itemId, "Failed to find CraftEngine item.");
}
return item.buildBukkitItem();
}
@Override
public @NotNull BlockData getBlockData(@NotNull Identifier blockId, @NotNull KMap<String, String> state) throws MissingResourceException {
Key key = Key.of(blockId.namespace(), blockId.key());
if (CraftEngineBlocks.byId(key) == null && CraftEngineFurniture.byId(key) == null) {
throw new MissingResourceException("Failed to find BlockData!", blockId.namespace(), blockId.key());
ImmutableBlockState blockState = findBlockState(blockId);
if (blockState != null) {
ImmutableBlockState resolved = applyProperties(blockId, blockState, state);
return IrisCustomData.of(CraftEngineBlocks.getBukkitBlockData(resolved), Identifier.fromString(resolved.toString()));
}
FurnitureDefinition furniture = requireFurniture(blockId);
KMap<String, String> properties = validateFurnitureProperties(blockId, furniture, state);
return IrisCustomData.of(BukkitBlockResolution.getAir(), ExternalDataSVC.buildState(blockId, properties));
}
return IrisCustomData.of(BukkitBlockResolution.getAir(), ExternalDataSVC.buildState(blockId, state));
@Override
public Optional<Identifier> identifyBlock(@NotNull BlockData blockData) {
BlockData nativeData = blockData instanceof IrisCustomData custom ? custom.getBase() : blockData;
ImmutableBlockState state = CraftEngineBlocks.getCustomBlockState(nativeData);
return state == null || state.isEmpty() ? Optional.empty() : Optional.of(Identifier.fromString(state.toString()));
}
@Override
public boolean placeBlock(@NotNull Block block, @NotNull Identifier blockId) {
Pair<Identifier, KMap<String, String>> parsed = ExternalDataSVC.parseState(blockId);
ImmutableBlockState state = findBlockState(parsed.getA());
if (state == null) {
validateFurnitureProperties(parsed.getA(), requireFurniture(parsed.getA()), parsed.getB());
return false;
}
placeBlockState(block, applyProperties(parsed.getA(), state, parsed.getB()), blockId);
return true;
}
@Override
public void processUpdate(@NotNull Engine engine, @NotNull Block block, @NotNull Identifier blockId) {
art.arcane.iris.core.nms.container.Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
Pair<Identifier, KMap<String, String>> statePair = ExternalDataSVC.parseState(blockId);
Identifier baseBlockId = statePair.getA();
KMap<String, String> state = statePair.getB();
Key key = Key.of(baseBlockId.namespace(), baseBlockId.key());
ImmutableBlockState blockState = findBlockState(baseBlockId);
if (blockState != null) {
placeBlockState(block, applyProperties(baseBlockId, blockState, state), blockId);
return;
}
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) {
throw missing(blockId, "Failed to place CraftEngine furniture.");
}
}
BlockDefinition customBlock = CraftEngineBlocks.byId(key);
if (customBlock != null) {
ImmutableBlockState blockState = customBlock.defaultState();
@Override
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
return switch (dataType) {
case ENTITY -> List.of();
case ITEM -> content.items();
case BLOCK -> content.blocks();
};
}
for (Map.Entry<String, String> entry : state.entrySet()) {
Property<?> property = customBlock.getProperty(entry.getKey());
if (property == null) {
continue;
}
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
ContentRegistry registry = content;
if (!registry.ready()) {
return false;
}
return switch (dataType) {
case ENTITY -> false;
case ITEM -> registry.items().contains(id);
case BLOCK -> registry.blocks().contains(id) || findRuntimeBlockState(id) != null;
};
}
Comparable<?> tag = property.optional(entry.getValue()).orElse(null);
if (tag == null) {
continue;
}
blockState = ImmutableBlockState.with(blockState, property, tag);
private boolean updateContent() {
try {
Set<Identifier> blocks = new LinkedHashSet<>();
for (Key key : CraftEngineBlocks.loadedBlocks().keySet()) {
blocks.add(new Identifier(key.namespace(), key.value()));
}
CraftEngineBlocks.place(block.getLocation(), blockState, false);
return;
for (Key key : CraftEngineFurniture.loadedFurniture().keySet()) {
blocks.add(new Identifier(key.namespace(), key.value()));
}
Set<Identifier> items = CraftEngineItems.loadedItems().keySet().stream()
.map(key -> new Identifier(key.namespace(), key.value())).collect(Collectors.toUnmodifiableSet());
content = new ContentRegistry(items, Set.copyOf(blocks), true);
return true;
} catch (RuntimeException | LinkageError error) {
content = EMPTY_CONTENT;
IrisLogging.reportError("Failed to update CraftEngine content registry.", error);
return false;
}
}
FurnitureDefinition furniture = CraftEngineFurniture.byId(key);
private static ImmutableBlockState findBlockState(Identifier blockId) {
BlockDefinition block = CraftEngineBlocks.byId(Key.of(blockId.namespace(), blockId.key()));
return block != null ? block.defaultState() : findRuntimeBlockState(blockId);
}
private static ImmutableBlockState findRuntimeBlockState(Identifier blockId) {
if (!Key.CRAFTENGINE_NAMESPACE.equals(blockId.namespace())) {
return null;
}
BlockData data;
try {
data = Bukkit.createBlockData(blockId.toString());
} catch (IllegalArgumentException error) {
return null;
}
ImmutableBlockState state = CraftEngineBlocks.getCustomBlockState(data);
return state == null || state.isEmpty() ? null : state;
}
private static ImmutableBlockState applyProperties(Identifier id, ImmutableBlockState state, Map<String, String> properties) {
BlockDefinition block = state.owner().value();
for (Map.Entry<String, String> entry : properties.entrySet()) {
Property<?> property = block.getProperty(entry.getKey());
if (property == null) {
throw missing(id, "Unknown CraftEngine block property '" + entry.getKey() + "'.");
}
Comparable<?> value = property.optional(entry.getValue()).orElse(null);
if (value == null) {
throw missing(id, "Invalid CraftEngine block property '" + entry.getKey() + "=" + entry.getValue() + "'.");
}
state = ImmutableBlockState.with(state, property, value);
}
return state;
}
private static void placeBlockState(Block block, ImmutableBlockState state, Identifier id) {
if (!CraftEngineBlocks.place(block.getLocation(), state, false)
&& !state.equals(CraftEngineBlocks.getCustomBlockState(block))) {
throw missing(id, "Failed to place CraftEngine block.");
}
}
private static FurnitureDefinition requireFurniture(Identifier id) {
FurnitureDefinition furniture = CraftEngineFurniture.byId(Key.of(id.namespace(), id.key()));
if (furniture == null) {
return;
throw missing(id, "Failed to find CraftEngine block or furniture.");
}
return furniture;
}
Location location = parseYawAndPitch(engine, block, state);
String variant = state.getOrDefault("variant", furniture.anyVariantName());
CraftEngineFurniture.place(location, furniture, variant, false);
private static KMap<String, String> validateFurnitureProperties(Identifier id, FurnitureDefinition furniture, Map<String, String> state) {
KMap<String, String> properties = new KMap<>();
for (Map.Entry<String, String> entry : state.entrySet()) {
String value = entry.getValue();
switch (entry.getKey()) {
case "randomYaw", "randomPitch" -> {
if (!"true".equals(value) && !"false".equals(value)) {
throw missing(id, "Invalid CraftEngine furniture boolean '" + entry.getKey() + "=" + value + "'.");
}
}
case "yaw", "pitch" -> validateAngle(id, entry.getKey(), value);
case "variant" -> {
if (!furniture.variants().containsKey(value)) {
throw missing(id, "Unknown CraftEngine furniture variant '" + value + "'.");
}
}
default -> throw missing(id, "Unknown CraftEngine furniture property '" + entry.getKey() + "'.");
}
properties.put(entry.getKey(), value);
}
properties.putIfAbsent("variant", defaultVariant(furniture));
return properties;
}
private static void validateAngle(Identifier id, String property, String value) {
float angle;
try {
angle = Float.parseFloat(value);
} catch (NumberFormatException error) {
throw missing(id, "Invalid CraftEngine furniture angle '" + property + "=" + value + "'.");
}
if (!Float.isFinite(angle) || angle < 0 || angle >= 360) {
throw missing(id, "CraftEngine furniture angle '" + property + "' must be at least 0 and below 360.");
}
}
private static String defaultVariant(FurnitureDefinition furniture) {
return furniture.variants().keySet().stream().sorted().findFirst()
.orElseThrow(() -> missing(Identifier.fromString(furniture.id().toString()), "CraftEngine furniture has no variants."));
}
private static Location parseYawAndPitch(@NotNull Engine engine, @NotNull Block block, @NotNull Map<String, String> state) {
Location location = block.getLocation();
long seed = engine.getSeedManager().getSeed() + Cache.key(block.getX(), block.getZ()) + block.getY();
RNG rng = new RNG(seed);
if ("true".equals(state.get("randomYaw"))) {
location.setYaw(rng.f(0, 360));
} else if (state.containsKey("yaw")) {
location.setYaw(Float.parseFloat(state.get("yaw")));
}
if ("true".equals(state.get("randomPitch"))) {
location.setPitch(rng.f(0, 360));
} else if (state.containsKey("pitch")) {
location.setPitch(Float.parseFloat(state.get("pitch")));
}
location.setYaw("true".equals(state.get("randomYaw")) ? rng.f(0, 360) : Float.parseFloat(state.getOrDefault("yaw", "0")));
location.setPitch("true".equals(state.get("randomPitch")) ? rng.f(0, 360) : Float.parseFloat(state.getOrDefault("pitch", "0")));
return location;
}
@Override
public @NotNull Collection<@NotNull Identifier> getTypes(@NotNull DataType dataType) {
Stream<Key> keys = switch (dataType) {
case ENTITY -> Stream.<Key>empty();
case ITEM -> CraftEngineItems.loadedItems().keySet().stream();
case BLOCK -> Stream.concat(CraftEngineBlocks.loadedBlocks().keySet().stream(),
CraftEngineFurniture.loadedFurniture().keySet().stream());
};
return keys.map(key -> new Identifier(key.namespace(), key.value())).toList();
}
@Override
public boolean isValidProvider(@NotNull Identifier id, DataType dataType) {
Key key = Key.of(id.namespace(), id.key());
return switch (dataType) {
case ENTITY -> false;
case ITEM -> CraftEngineItems.byId(key) != null;
case BLOCK -> CraftEngineBlocks.byId(key) != null || CraftEngineFurniture.byId(key) != null;
};
private static MissingResourceException missing(Identifier id, String message) {
return new MissingResourceException(message, id.namespace(), id.key());
}
private static <T extends Comparable<T>> BlockProperty convert(Property<T> raw) {
@@ -183,4 +310,7 @@ public class CraftEngineDataProvider extends ExternalDataProvider {
default -> new BlockProperty(raw.name(), raw.valueClass(), raw.defaultValue(), raw.possibleValues(), raw::valueName);
};
}
private record ContentRegistry(Set<Identifier> items, Set<Identifier> blocks, boolean ready) {
}
}
@@ -27,6 +27,7 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.tools.IrisToolbelt;
import art.arcane.iris.engine.framework.BiomeEnvironment;
import art.arcane.iris.engine.framework.Engine;
import art.arcane.iris.engine.framework.EngineLifecycleTasks;
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
import art.arcane.iris.engine.platform.PlatformChunkGenerator;
import art.arcane.volmlib.util.board.Board;
@@ -498,8 +499,13 @@ public class BoardSVC implements IrisService, BoardProvider {
return;
}
EngineLifecycleTasks.run(engine, "bukkit_studio_scoreboard",
() -> updateOrdinary(engine, loc, world.getMinHeight()));
}
private void updateOrdinary(Engine engine, Location loc, int minimumY) {
int x = loc.getBlockX();
int y = loc.getBlockY() - world.getMinHeight();
int y = loc.getBlockY() - minimumY;
int z = loc.getBlockZ();
List<String> lines = new ArrayList<>(this.lines.size());
@@ -180,6 +180,20 @@ public class ExternalDataSVC implements IrisService {
}
}
public BlockData captureBlockData(BlockData blockData) {
Objects.requireNonNull(blockData);
if (blockData instanceof IrisCustomData) {
return blockData;
}
for (ExternalDataProvider provider : activeProviders) {
Optional<Identifier> identifier = provider.identifyBlock(blockData);
if (identifier.isPresent()) {
return IrisCustomData.of(blockData, qualifyBlockId(provider, identifier.get()));
}
}
return blockData;
}
public Optional<BlockData> getBlockData(final Identifier key) {
Pair<Identifier, KMap<String, String>> pair;
try {
@@ -244,6 +258,15 @@ public class ExternalDataSVC implements IrisService {
match.provider().processUpdate(engine, block, buildState(match.identifier(), state.getB()));
}
public boolean placeBlock(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());
}
return match.provider().placeBlock(block, buildState(match.identifier(), state.getB()));
}
public Entity spawnMob(Location location, Identifier mobId) {
Optional<ExternalDataProvider> provider = activeProviders.stream().filter(p -> p.isValidProvider(mobId, DataType.ENTITY)).findFirst();
if (provider.isEmpty()) {
@@ -18,6 +18,9 @@
package art.arcane.iris.core.service;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.plugin.IrisService;
import art.arcane.iris.util.common.scheduling.J;
import lombok.Getter;
@@ -81,7 +84,11 @@ public class ObjectSVC implements IrisService {
int amount = 0;
while (it.hasNext()) {
Map.Entry<Block, BlockData> entry = it.next();
entry.getKey().setBlockData(entry.getValue(), false);
Block block = entry.getKey();
BlockData data = entry.getValue();
if (!J.runAt(block.getLocation(), () -> restoreBlock(block, data))) {
IrisLogging.warn("Could not schedule object undo at %d, %d, %d.", block.getX(), block.getY(), block.getZ());
}
it.remove();
if (++amount >= 200) {
@@ -94,4 +101,16 @@ public class ObjectSVC implements IrisService {
}
});
}
private static void restoreBlock(Block block, BlockData data) {
if (data instanceof IrisCustomData custom) {
ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class);
if (external != null && external.placeBlock(block, custom.getCustom())) {
return;
}
block.setBlockData(custom.getBase(), false);
return;
}
block.setBlockData(data, false);
}
}
@@ -277,6 +277,13 @@ final class EngineRuntimeBuilder {
throw propagate(failure);
}
EngineRuntime next = new EngineRuntime(previous.generation(), effects, worldManager);
try {
worldManager.start();
} catch (Throwable failure) {
failure = EngineShutdownSequence.runCleanup(failure, worldManager::close);
failure = EngineShutdownSequence.runCleanup(failure, effects::close);
throw new IllegalStateException("Failed to start the replacement Studio world manager.", failure);
}
Throwable retirementFailure = EngineShutdownSequence.runCleanup(null, previous.worldManager()::close);
retirementFailure = EngineShutdownSequence.runCleanup(retirementFailure, previous.effects()::close);
if (retirementFailure != null) {
@@ -285,7 +292,6 @@ final class EngineRuntimeBuilder {
throw new IllegalStateException("Failed to retire Studio runtime services.", retirementFailure);
}
engine.runtime = next;
worldManager.start();
engine.getGenerationSessions().activateNextSession();
engine.lifecycleState = LifecycleState.RUNNING;
engine.getClosing().set(false);
@@ -1459,6 +1459,13 @@ public class IrisEngine implements Engine {
return closing.get();
}
@Override
public boolean isShuttingDown() {
LifecycleState state = lifecycleState;
return closed || state == LifecycleState.CLOSING
|| state == LifecycleState.CLOSED || state == LifecycleState.FAILED;
}
@Override
public void recycle() {
if (closing.get() || closed) {
@@ -195,6 +195,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return isClosed();
}
default boolean isShuttingDown() {
return isClosed() || isClosing();
}
double getMaxBiomeObjectDensity();
double getMaxBiomeDecoratorDensity();
@@ -131,8 +131,9 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void on(PlayerTeleportEvent e) {
Engine engine = getEngine();
if (!BukkitPlatform.isPaperServer()
|| !getEngine().isStudio()
|| !engine.isStudio()
|| e.getCause() != PlayerTeleportEvent.TeleportCause.COMMAND) {
return;
}
@@ -142,6 +143,10 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
if (destination == null || targetWorld == null || !targetWorld.equals(destination.getWorld())) {
return;
}
if (engine.isShuttingDown()) {
e.setCancelled(true);
return;
}
int chunkX = destination.getBlockX() >> 4;
int chunkZ = destination.getBlockZ() >> 4;
@@ -149,7 +154,7 @@ public abstract class EngineAssignedWorldManager extends EngineAssignedComponent
return;
}
runManagerTask("bukkit_world_manager_teleport_event", () -> teleportAsync(e));
teleportAsync(e);
}
@EventHandler
@@ -619,6 +619,7 @@ public final class GenerationBoundaryStore {
private final long[] regionKeys;
private final Long2ObjectOpenHashMap<String> shardHashes;
private final LinkedHashMap<Long, byte[]> masks;
private final ThreadLocal<RegionMask> lastRegion = new ThreadLocal<>();
private MaskSource(
Path directory,
@@ -634,13 +635,21 @@ public final class GenerationBoundaryStore {
@Override
public boolean contains(int chunkX, int chunkZ) throws IOException {
long regionKey = packRegion(chunkX >> 5, chunkZ >> 5);
String hash = shardHashes.get(regionKey);
if (hash == null) {
return false;
RegionMask region = lastRegion.get();
if (region == null || region.key() != regionKey) {
String hash = shardHashes.get(regionKey);
byte[] loaded = null;
if (hash != null) {
synchronized (this) {
loaded = load(regionKey, hash);
}
}
region = new RegionMask(regionKey, loaded);
lastRegion.set(region);
}
byte[] mask;
synchronized (this) {
mask = load(regionKey, hash);
byte[] mask = region.mask();
if (mask == null) {
return false;
}
int localIndex = (Math.floorMod(chunkZ, REGION_SIDE) << 5)
| Math.floorMod(chunkX, REGION_SIDE);
@@ -686,6 +695,9 @@ public final class GenerationBoundaryStore {
}
return loaded;
}
private record RegionMask(long key, byte[] mask) {
}
}
private static final class LimitedInputStream extends InputStream {
@@ -46,6 +46,9 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.PriorityQueue;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.LongAdder;
import java.util.zip.CRC32;
public final class TerrainBoundarySignatureStore {
@@ -936,8 +939,9 @@ public final class TerrainBoundarySignatureStore {
private final Long2ObjectOpenHashMap<CellReference> cells;
private final Long2ObjectOpenHashMap<long[]> cellsBySuperCell;
private final LinkedHashMap<Long, List<TerrainBoundarySignature>> cache;
private long catalogProbes;
private long shardLoads;
private final Map<Long, CompletableFuture<List<TerrainBoundarySignature>>> pendingLoads = new HashMap<>();
private final LongAdder catalogProbes = new LongAdder();
private final LongAdder shardLoads = new LongAdder();
private CellSource(
Path directory,
@@ -951,7 +955,7 @@ public final class TerrainBoundarySignatureStore {
this.cache = new LinkedHashMap<>(MAXIMUM_CACHED_SHARDS, 0.75F, true);
}
private synchronized TerrainBoundarySignature signatureAt(int blockX, int blockZ) throws IOException {
private TerrainBoundarySignature signatureAt(int blockX, int blockZ) throws IOException {
int cellX = Math.floorDiv(blockX, CELL_SIZE);
int cellZ = Math.floorDiv(blockZ, CELL_SIZE);
long cellKey = pack(cellX, cellZ);
@@ -967,7 +971,7 @@ public final class TerrainBoundarySignatureStore {
return null;
}
private synchronized List<TerrainBoundarySignature> all() throws IOException {
private List<TerrainBoundarySignature> all() throws IOException {
ArrayList<TerrainBoundarySignature> signatures = new ArrayList<>();
for (long cellKey : cellKeys) {
signatures.addAll(load(cellKey, cells.get(cellKey)));
@@ -978,7 +982,7 @@ public final class TerrainBoundarySignatureStore {
return List.copyOf(signatures);
}
private synchronized List<TerrainBoundarySignature> nearestCandidatesForChunk(
private List<TerrainBoundarySignature> nearestCandidatesForChunk(
int chunkX,
int chunkZ,
int searchWidth
@@ -1019,7 +1023,7 @@ public final class TerrainBoundarySignatureStore {
return nearest.candidates();
}
private synchronized boolean intersectsTerrainBand(BlockBounds footprint, int width) throws IOException {
private boolean intersectsTerrainBand(BlockBounds footprint, int width) throws IOException {
double distanceSquared = (double) width * width;
return visitSuperCells(footprint.expanded(width - 1L), superKey -> {
if (footprint.distanceSquared(BlockBounds.cell(superKey, CELL_SIZE * SUPER_CELL_SIZE))
@@ -1050,7 +1054,7 @@ public final class TerrainBoundarySignatureStore {
long height = (long) maximumZ - minimumZ + 1L;
if (width > cellsBySuperCell.size() / height) {
for (long key : cellsBySuperCell.keySet()) {
catalogProbes++;
catalogProbes.increment();
if (cellX(key) >= minimumX && cellX(key) <= maximumX
&& cellZ(key) >= minimumZ && cellZ(key) <= maximumZ
&& visitor.visit(key)) {
@@ -1061,7 +1065,7 @@ public final class TerrainBoundarySignatureStore {
}
for (long x = minimumX; x <= maximumX; x++) {
for (long z = minimumZ; z <= maximumZ; z++) {
catalogProbes++;
catalogProbes.increment();
long key = pack((int) x, (int) z);
if (cellsBySuperCell.containsKey(key) && visitor.visit(key)) {
return true;
@@ -1072,33 +1076,61 @@ public final class TerrainBoundarySignatureStore {
}
private List<TerrainBoundarySignature> load(long cellKey, CellReference reference) throws IOException {
List<TerrainBoundarySignature> cached = cache.get(cellKey);
if (cached != null) {
return cached;
CompletableFuture<List<TerrainBoundarySignature>> pending;
boolean loader;
synchronized (this) {
List<TerrainBoundarySignature> cached = cache.get(cellKey);
if (cached != null) {
return cached;
}
pending = pendingLoads.get(cellKey);
loader = pending == null;
if (loader) {
pending = new CompletableFuture<>();
pendingLoads.put(cellKey, pending);
}
}
Path shard = directory.resolve(shardFileName(reference.hash()));
requireRegularFile(shard, "Terrain boundary shard");
List<TerrainBoundarySignature> loaded = CellShard.read(
shard,
reference.hash(),
cellX(cellKey),
cellZ(cellKey)
);
if (loaded.size() != reference.count()) {
throw new IOException("Terrain boundary shard count does not match its catalog entry");
if (!loader) {
try {
return pending.join();
} catch (CompletionException failure) {
if (failure.getCause() instanceof IOException io) {
throw io;
}
throw failure;
}
}
try {
Path shard = directory.resolve(shardFileName(reference.hash()));
requireRegularFile(shard, "Terrain boundary shard");
List<TerrainBoundarySignature> loaded = CellShard.read(
shard, reference.hash(), cellX(cellKey), cellZ(cellKey));
if (loaded.size() != reference.count()) {
throw new IOException("Terrain boundary shard count does not match its catalog entry");
}
synchronized (this) {
shardLoads.increment();
cache.put(cellKey, loaded);
trim(cache);
}
pending.complete(loaded);
return loaded;
} catch (IOException | RuntimeException | Error failure) {
pending.completeExceptionally(failure);
throw failure;
} finally {
synchronized (this) {
pendingLoads.remove(cellKey, pending);
}
}
shardLoads++;
cache.put(cellKey, loaded);
trim(cache);
return loaded;
}
private synchronized long catalogProbeCount() {
return catalogProbes;
private long catalogProbeCount() {
return catalogProbes.sum();
}
private synchronized long shardLoadCount() {
return shardLoads;
private long shardLoadCount() {
return shardLoads.sum();
}
private synchronized int cachedShardCount() {
@@ -3,21 +3,26 @@ package art.arcane.iris.engine.history;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
final class TransitionBoundarySampler {
private static final int NEIGHBOUR_COUNT = 4;
private static final int MAXIMUM_CACHED_CHUNKS = 8;
private static final int MAXIMUM_CACHED_CHUNKS = 512;
private static final int MAXIMUM_CACHED_CANDIDATES = 32_768;
private final int terrainWidth;
private final int searchWidth;
private final TerrainBoundarySignatureStore.Snapshot signatures;
private final LinkedHashMap<Long, CandidateIndex> chunkCandidates;
private final Map<Long, CompletableFuture<CandidateIndex>> pendingCandidates = new HashMap<>();
private final ThreadLocal<ChunkCache> cache;
private int cachedCandidates;
private long candidateBuildCount;
TransitionBoundarySampler(
@@ -35,12 +40,7 @@ final class TransitionBoundarySampler {
}
TransitionGenerationPlan.TerrainSample sample(int blockX, int blockZ) {
ChunkCache chunkCache = cache.get();
int chunkX = Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE);
if (chunkCache.chunkX != chunkX || chunkCache.chunkZ != chunkZ) {
chunkCache.reset(chunkX, chunkZ, candidatesForChunk(chunkX, chunkZ));
}
ChunkCache chunkCache = chunkCache(blockX, blockZ);
int localX = Math.floorMod(blockX, GenerationBoundary.CHUNK_SIZE);
int localZ = Math.floorMod(blockZ, GenerationBoundary.CHUNK_SIZE);
int index = localX * GenerationBoundary.CHUNK_SIZE + localZ;
@@ -58,9 +58,7 @@ final class TransitionBoundarySampler {
}
BoundaryGeometryInfluence geometryAt(int blockX, int blockZ) {
CandidateIndex candidates = candidatesForChunk(
Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE),
Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE));
CandidateIndex candidates = chunkCache(blockX, blockZ).candidates;
Nearest nearest = candidates.nearest(blockX, blockZ, square(terrainWidth));
if (nearest.count == 0 || nearest.distancesSquared[0] >= square(terrainWidth)) {
return BoundaryGeometryInfluence.none();
@@ -132,33 +130,59 @@ final class TransitionBoundarySampler {
private CandidateIndex candidatesForChunk(int chunkX, int chunkZ) {
long chunkKey = pack(chunkX, chunkZ);
CompletableFuture<CandidateIndex> pending;
boolean builder;
synchronized (this) {
CandidateIndex existing = chunkCandidates.get(chunkKey);
if (existing != null) {
return existing;
}
pending = pendingCandidates.get(chunkKey);
builder = pending == null;
if (builder) {
pending = new CompletableFuture<>();
pendingCandidates.put(chunkKey, pending);
}
}
CandidateIndex built = CandidateIndex.build(signatures.nearestCandidatesForChunk(
chunkX,
chunkZ,
searchWidth
));
synchronized (this) {
CandidateIndex raced = chunkCandidates.get(chunkKey);
if (raced != null) {
return raced;
}
chunkCandidates.put(chunkKey, built);
candidateBuildCount++;
while (chunkCandidates.size() > MAXIMUM_CACHED_CHUNKS) {
Iterator<Map.Entry<Long, CandidateIndex>> entries = chunkCandidates.entrySet().iterator();
entries.next();
entries.remove();
if (!builder) {
return pending.join();
}
try {
CandidateIndex built = CandidateIndex.build(signatures.nearestCandidatesForChunk(
chunkX, chunkZ, searchWidth));
synchronized (this) {
chunkCandidates.put(chunkKey, built);
cachedCandidates += built.size;
candidateBuildCount++;
while (chunkCandidates.size() > MAXIMUM_CACHED_CHUNKS
|| cachedCandidates > MAXIMUM_CACHED_CANDIDATES) {
Iterator<Map.Entry<Long, CandidateIndex>> entries = chunkCandidates.entrySet().iterator();
cachedCandidates -= entries.next().getValue().size;
entries.remove();
}
}
pending.complete(built);
return built;
} catch (RuntimeException | Error failure) {
pending.completeExceptionally(failure);
throw failure;
} finally {
synchronized (this) {
pendingCandidates.remove(chunkKey, pending);
}
}
}
private ChunkCache chunkCache(int blockX, int blockZ) {
ChunkCache chunkCache = cache.get();
int chunkX = Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE);
if (chunkCache.chunkX != chunkX || chunkCache.chunkZ != chunkZ) {
chunkCache.reset(chunkX, chunkZ, candidatesForChunk(chunkX, chunkZ));
}
return chunkCache;
}
private static double weightedHeight(Nearest nearest, boolean surface) {
if (nearest.distancesSquared[0] <= 1D) {
return surface
@@ -208,12 +232,14 @@ final class TransitionBoundarySampler {
}
private static final class CandidateIndex {
private static final CandidateIndex EMPTY = new CandidateIndex(null);
private static final CandidateIndex EMPTY = new CandidateIndex(null, 0);
private final Node root;
private final int size;
private CandidateIndex(Node root) {
private CandidateIndex(Node root, int size) {
this.root = root;
this.size = size;
}
private static CandidateIndex build(List<TerrainBoundarySignature> candidates) {
@@ -221,7 +247,7 @@ final class TransitionBoundarySampler {
return EMPTY;
}
TerrainBoundarySignature[] values = candidates.toArray(new TerrainBoundarySignature[0]);
return new CandidateIndex(build(values, 0, values.length, 0));
return new CandidateIndex(build(values, 0, values.length, 0), values.length);
}
private Nearest nearest(int blockX, int blockZ, double maximumDistanceSquared) {
@@ -22,13 +22,16 @@ import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.compat.CompatStatus;
import art.arcane.iris.core.compat.ContentGate;
import art.arcane.iris.core.loader.IrisRegistrant;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.engine.data.cache.AtomicCache;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.IrisPlatform;
import art.arcane.iris.spi.IrisPlatforms;
import art.arcane.iris.spi.IrisServices;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.B;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.data.VectorMap;
import art.arcane.iris.util.common.math.AxisAlignedBB;
import art.arcane.iris.util.common.math.IrisBlockVector;
@@ -288,6 +291,10 @@ public class IrisObject extends IrisRegistrant {
states.remove(v);
} else {
BlockData data = block.getBlockData();
ExternalDataSVC externalData = IrisServices.getOrNull(ExternalDataSVC.class);
if (externalData != null) {
data = externalData.captureBlockData(data);
}
blocks.put(v, BukkitBlockState.of(data));
TileData state = TileData.getTileState(block, legacy);
if (state != null) {
@@ -363,10 +370,10 @@ public class IrisObject extends IrisRegistrant {
public void place(Location at) {
readLock.lock();
try {
for (var entry : blocks) {
var i = entry.getKey();
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : blocks) {
IrisBlockVector i = entry.getKey();
Block b = at.clone().add(0, getCenter().getY(), 0).add(i.getX(), i.getY(), i.getZ()).getBlock();
b.setBlockData((BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle(), false);
placeBlock(b, (BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle());
if (states.containsKey(i)) {
IrisLogging.info(Objects.requireNonNull(states.get(i)).toString());
@@ -381,10 +388,10 @@ public class IrisObject extends IrisRegistrant {
public void placeCenterY(Location at) {
readLock.lock();
try {
for (var entry : blocks) {
var i = entry.getKey();
for (Map.Entry<IrisBlockVector, PlatformBlockState> entry : blocks) {
IrisBlockVector i = entry.getKey();
Block b = at.clone().add(getCenter().getX(), getCenter().getY(), getCenter().getZ()).add(i.getX(), i.getY(), i.getZ()).getBlock();
b.setBlockData((BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle(), false);
placeBlock(b, (BlockData) Objects.requireNonNull(entry.getValue()).nativeHandle());
if (states.containsKey(i)) {
Objects.requireNonNull(states.get(i)).toBukkitTry(b);
@@ -406,6 +413,7 @@ public class IrisObject extends IrisRegistrant {
}
}
public int volume() {
return blocks.size();
}
@@ -429,4 +437,18 @@ public class IrisObject extends IrisRegistrant {
public CompatStatus evaluateCompat(ContentGate gate) {
return CompatStatus.OK;
}
private static void placeBlock(Block block, BlockData data) {
if (data instanceof IrisCustomData custom) {
ExternalDataSVC external = IrisServices.getOrNull(ExternalDataSVC.class);
if (external != null && external.placeBlock(block, custom.getCustom())) {
return;
}
block.setBlockData(custom.getBase(), false);
IrisLogging.warnOnce("object-paste:external:" + custom.getCustom(),
"Direct placement of %s is unavailable; only its base block was placed.", custom.getCustom());
return;
}
block.setBlockData(data, false);
}
}
@@ -138,7 +138,7 @@ public final class IrisObjectIO {
for (int i = 0; i < s; i++) {
IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort());
PlatformBlockState data = resolvePaletteState(self, din.readUTF());
if (isStructureMarker(data)) {
if (isExcludedObjectBlock(data)) {
continue;
}
self.blocks.put(pos, data);
@@ -151,7 +151,7 @@ public final class IrisObjectIO {
int size = din.readInt();
for (int i = 0; i < size; i++) {
self.states.put(new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()), TileData.read(din));
readTile(self, din);
}
} catch (Throwable e) {
IrisLogging.reportError(e);
@@ -189,7 +189,7 @@ public final class IrisObjectIO {
for (i = 0; i < s; i++) {
IrisBlockVector pos = new IrisBlockVector(din.readShort(), din.readShort(), din.readShort());
PlatformBlockState data = resolved[din.readShort()];
if (isStructureMarker(data)) {
if (isExcludedObjectBlock(data)) {
continue;
}
self.blocks.put(pos, data);
@@ -198,7 +198,15 @@ public final class IrisObjectIO {
s = din.readInt();
for (i = 0; i < s; i++) {
self.states.put(new IrisBlockVector(din.readShort(), din.readShort(), din.readShort()), TileData.read(din));
readTile(self, din);
}
}
private static void readTile(IrisObject self, DataInputStream input) throws IOException {
IrisBlockVector position = new IrisBlockVector(input.readShort(), input.readShort(), input.readShort());
TileData tile = TileData.read(input);
if (self.blocks.get(position) != null) {
self.states.put(position, tile);
}
}
@@ -251,12 +259,13 @@ public final class IrisObjectIO {
return B.getState(key);
}
private static boolean isStructureMarker(PlatformBlockState data) {
private static boolean isExcludedObjectBlock(PlatformBlockState data) {
if (data == null) {
return false;
}
String material = IrisObjectShaping.materialKey(data);
return material.equals("minecraft:jigsaw") || material.equals("minecraft:structure_block") || material.equals("minecraft:structure_void");
return material.equals("minecraft:jigsaw") || material.equals("minecraft:structure_block")
|| material.equals("minecraft:structure_void") || material.equals("minecraft:moving_piston");
}
/**
@@ -24,6 +24,7 @@ import art.arcane.iris.core.compat.MissingBlockState;
import art.arcane.iris.platform.bukkit.BukkitBlockState;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
@@ -72,6 +73,7 @@ public class IrisObjectRotation {
private static final class Faces {
private static final List<BlockFace> WALL_FACES = List.of(BlockFace.NORTH, BlockFace.SOUTH, BlockFace.EAST, BlockFace.WEST);
}
@Desc("If this rotator is enabled or not")
@@ -298,7 +300,16 @@ public class IrisObjectRotation {
}
BlockData original = (BlockData) state.nativeHandle();
if (!canRotate() || !canRotateBlockData(original)) {
if (!canRotate()) {
return state;
}
if (original instanceof IrisCustomData custom) {
BlockData rotated = BukkitBlockState.rotateCustomData(this, custom, spinx, spiny, spinz);
if (rotated != null) {
return rotated == original ? state : BukkitBlockState.of(rotated);
}
}
if (!canRotateBlockData(original)) {
return state;
}
@@ -317,6 +328,12 @@ public class IrisObjectRotation {
}
public BlockData rotate(BlockData dd, int spinxx, int spinyy, int spinzz) {
if (canRotate() && dd instanceof IrisCustomData custom) {
BlockData rotated = BukkitBlockState.rotateCustomData(this, custom, spinxx, spinyy, spinzz);
if (rotated != null) {
return rotated;
}
}
BlockData d = dd;
try {
int spinx = (int) (90D * (Math.ceil(Math.abs((spinxx % 360D) / 90D))));
@@ -1122,13 +1122,23 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
Runnable activeOperation = Objects.requireNonNull(operation, "Exclusive control operation");
CompletableFuture<Void> outward = Objects.requireNonNull(future, "Exclusive control future");
TimeUnit activeUnit = Objects.requireNonNull(unit, "Exclusive control timeout unit");
if (outward.isCancelled()) {
return;
}
ExclusiveControlAcquisition acquisition = new ExclusiveControlAcquisition(Thread.currentThread());
outward.whenComplete((ignored, completionFailure) -> {
if (outward.isCancelled()) {
acquisition.cancel();
}
});
boolean acquired = false;
Throwable failure = null;
try {
acquired = activeGate.tryAcquireExclusive(
acquisitionTimeout,
activeUnit,
outward::isCancelled);
try {
acquired = activeGate.tryAcquireExclusive(acquisitionTimeout, activeUnit);
} finally {
acquisition.stopWaiting();
}
if (!acquired) {
if (!outward.isCancelled()) {
failure = new TimeoutException("Timed out waiting for exclusive Iris generation control after "
@@ -1138,7 +1148,9 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
activeOperation.run();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (!acquisition.interruptedByCancellation()) {
Thread.currentThread().interrupt();
}
if (!outward.isCancelled()) {
failure = e;
}
@@ -1447,30 +1459,12 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
permits.acquire(permitCount);
}
boolean tryAcquireExclusive(
long timeout,
TimeUnit unit,
BooleanSupplier cancelled
) throws InterruptedException {
boolean tryAcquireExclusive(long timeout, TimeUnit unit) throws InterruptedException {
if (timeout <= 0L) {
throw new IllegalArgumentException("Exclusive generation control timeout must be positive.");
}
TimeUnit activeUnit = Objects.requireNonNull(unit, "Exclusive generation control timeout unit");
BooleanSupplier cancellation = Objects.requireNonNull(cancelled, "Exclusive generation control cancellation");
long timeoutNanos = activeUnit.toNanos(timeout);
long started = System.nanoTime();
long remainingNanos = timeoutNanos;
long cancellationPollNanos = TimeUnit.MILLISECONDS.toNanos(50L);
while (!cancellation.getAsBoolean() && remainingNanos > 0L) {
if (permits.tryAcquire(
permitCount,
Math.min(remainingNanos, cancellationPollNanos),
TimeUnit.NANOSECONDS)) {
return true;
}
remainingNanos = timeoutNanos - (System.nanoTime() - started);
}
return false;
return permits.tryAcquire(permitCount, timeout, activeUnit);
}
void releaseExclusive() {
@@ -1491,6 +1485,34 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun
}
}
private static final class ExclusiveControlAcquisition {
private final Thread waiter;
private boolean waiting = true;
private boolean cancellationInterrupt;
private ExclusiveControlAcquisition(Thread waiter) {
this.waiter = waiter;
}
private synchronized void cancel() {
if (waiting && !waiter.isInterrupted()) {
cancellationInterrupt = true;
waiter.interrupt();
}
}
private synchronized void stopWaiting() {
waiting = false;
if (cancellationInterrupt) {
Thread.interrupted();
}
}
private synchronized boolean interruptedByCancellation() {
return cancellationInterrupt;
}
}
static final class GenerationStageExclusivePermit implements AutoCloseable {
private final Semaphore permits;
private final int permitCount;
@@ -18,14 +18,24 @@
package art.arcane.iris.platform.bukkit;
import art.arcane.iris.core.link.Identifier;
import art.arcane.iris.core.nms.INMS;
import art.arcane.iris.core.nms.container.Pair;
import art.arcane.iris.core.service.ExternalDataSVC;
import art.arcane.iris.engine.object.IrisObjectRotation;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.data.IrisCustomData;
import art.arcane.iris.util.common.math.IrisBlockVector;
import art.arcane.volmlib.util.collection.KMap;
import org.bukkit.Axis;
import org.bukkit.Bukkit;
import org.bukkit.Tag;
import org.bukkit.block.BlockFace;
import org.bukkit.block.data.BlockData;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -33,6 +43,15 @@ import java.util.concurrent.ConcurrentHashMap;
* Interned Bukkit adapter for a neutral block state backed by BlockData.
*/
public final class BukkitBlockState implements PlatformBlockState {
private static final Map<String, BlockFace> CUSTOM_NAMED_FACES = Map.of(
"north", BlockFace.NORTH, "south", BlockFace.SOUTH,
"east", BlockFace.EAST, "west", BlockFace.WEST,
"up", BlockFace.UP, "down", BlockFace.DOWN);
private static final List<BlockFace> CUSTOM_ROTATION_FACES = List.of(
BlockFace.SOUTH, BlockFace.SOUTH_SOUTH_WEST, BlockFace.SOUTH_WEST, BlockFace.WEST_SOUTH_WEST,
BlockFace.WEST, BlockFace.WEST_NORTH_WEST, BlockFace.NORTH_WEST, BlockFace.NORTH_NORTH_WEST,
BlockFace.NORTH, BlockFace.NORTH_NORTH_EAST, BlockFace.NORTH_EAST, BlockFace.EAST_NORTH_EAST,
BlockFace.EAST, BlockFace.EAST_SOUTH_EAST, BlockFace.SOUTH_EAST, BlockFace.SOUTH_SOUTH_EAST);
private static final ConcurrentHashMap<String, BukkitBlockState> CACHE = new ConcurrentHashMap<>();
// Front cache keyed on the BlockData itself (CraftBlockData equals/hashCode delegate to
// the canonical NMS state): a hit skips getAsString(), which built the full property
@@ -82,6 +101,64 @@ public final class BukkitBlockState implements PlatformBlockState {
return state;
}
public static BlockData rotateCustomData(IrisObjectRotation objectRotation, IrisCustomData custom, int spinxx, int spinyy, int spinzz) {
Pair<Identifier, KMap<String, String>> parsed = ExternalDataSVC.parseState(custom.getCustom());
KMap<String, String> original = parsed.getB();
if (original.isEmpty()) {
return null;
}
KMap<String, String> rotated = new KMap<>(original);
int spinx = (int) (90D * Math.ceil(Math.abs((spinxx % 360D) / 90D)));
int spiny = (int) (90D * Math.ceil(Math.abs((spinyy % 360D) / 90D)));
int spinz = (int) (90D * Math.ceil(Math.abs((spinzz % 360D) / 90D)));
boolean oriented = false;
String facing = original.get("facing");
if (facing != null && CUSTOM_NAMED_FACES.containsKey(facing)) {
BlockFace face = objectRotation.getFace(rotateFace(objectRotation, CUSTOM_NAMED_FACES.get(facing), spinx, spiny, spinz));
rotated.put("facing", face.name().toLowerCase(Locale.ROOT));
oriented = true;
}
Axis axis = switch (original.getOrDefault("axis", "")) {
case "x" -> Axis.X;
case "y" -> Axis.Y;
case "z" -> Axis.Z;
default -> null;
};
if (axis != null) {
Axis result = objectRotation.getAxis(rotateFace(objectRotation, objectRotation.faceForAxis(axis), spinx, spiny, spinz));
rotated.put("axis", result.name().toLowerCase(Locale.ROOT));
oriented = true;
}
int rotation = rotationIndex(original.get("rotation"));
if (rotation >= 0) {
BlockFace face = objectRotation.getHexFace(rotateFace(objectRotation, CUSTOM_ROTATION_FACES.get(rotation), spinx, spiny, spinz));
int result = CUSTOM_ROTATION_FACES.indexOf(face);
if (result >= 0) {
rotated.put("rotation", Integer.toString(result));
}
oriented = true;
}
for (Map.Entry<String, BlockFace> entry : CUSTOM_NAMED_FACES.entrySet()) {
String value = original.get(entry.getKey());
if (value == null) {
continue;
}
String destination = objectRotation.getFace(rotateFace(objectRotation, entry.getValue(), spinx, spiny, spinz)).name().toLowerCase(Locale.ROOT);
if (original.containsKey(destination)) {
rotated.put(destination, value);
}
oriented = true;
}
if (!oriented) {
return null;
}
if (rotated.equals(original)) {
return custom;
}
BlockData resolved = BukkitBlockResolution.resolveOrNull(ExternalDataSVC.buildState(parsed.getA(), rotated).toString());
return resolved instanceof IrisCustomData ? resolved : custom;
}
@Override
public boolean equals(Object other) {
if (this == other) {
@@ -369,9 +446,17 @@ public final class BukkitBlockState implements PlatformBlockState {
@Override
public PlatformBlockState withProperty(String name, String value) {
// Re-attach the custom identity (as the proxy's own merge/clone cases do) after
// editing the base block, so auto-waterlogging cannot turn custom blocks into vanilla.
if (data instanceof IrisCustomData custom) {
if (ExternalDataSVC.parseState(custom.getCustom()).getB().containsKey(name)) {
String merged = mergeProperty(key, name, value);
BlockData resolved = BukkitBlockResolution.resolveOrNull(merged);
if (!(resolved instanceof IrisCustomData)) {
throw new IllegalArgumentException("Cannot resolve custom block state " + merged);
}
return of(resolved);
}
// Re-attach the custom identity (as the proxy's own merge/clone cases do) after
// editing the base block, so auto-waterlogging cannot turn custom blocks into vanilla.
String merged = mergeProperty(custom.getBase().getAsString(), name, value);
BlockData resolved = Bukkit.createBlockData(merged);
return of(IrisCustomData.of(resolved, custom.getCustom()));
@@ -383,4 +468,20 @@ public final class BukkitBlockState implements PlatformBlockState {
public Object nativeHandle() {
return data;
}
private static IrisBlockVector rotateFace(IrisObjectRotation objectRotation, BlockFace face, int spinx, int spiny, int spinz) {
return objectRotation.rotate(new IrisBlockVector(face.getModX(), face.getModY(), face.getModZ()), spinx, spiny, spinz);
}
private static int rotationIndex(String value) {
if (value == null) {
return -1;
}
try {
int rotation = Integer.parseInt(value);
return rotation >= 0 && rotation < CUSTOM_ROTATION_FACES.size() ? rotation : -1;
} catch (NumberFormatException ignored) {
return -1;
}
}
}