RFC: Native BlockStates

This commit is contained in:
RecursivePineapple committed 2026-08-22 10:39:35 -04:00
1 parent e729fc64d4
commit b1681dbcfc
31 files changed
+2480 -3

No files matched your search

+2
View File
@@ -35,6 +35,8 @@ dependencies {
compileOnly("com.github.GTNewHorizons:lwjgl3ify:3.0.31") { transitive = false }
compileOnly("com.gtnewhorizons.retrofuturabootstrap:RetroFuturaBootstrap:1.1.1") { transitive = false }
devOnlyNonPublishable("com.falsepattern:chunkapi-mc1.7.10:0.8.3:dev")
runtimeOnlyNonPublishable("com.github.GTNewHorizons:NotEnoughItems:2.8.118-GTNH:dev") { transitive = false }
runtimeOnlyNonPublishable("com.github.GTNewHorizons:CodeChickenCore:1.4.17:dev") { transitive = false }
runtimeOnlyNonPublishable("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-999-GTNH") {
@@ -12,6 +12,7 @@ import net.minecraftforge.common.util.FakePlayer;
import com.gtnewhorizon.gtnhlib.blockstate.command.BlockStateCommand;
import com.gtnewhorizon.gtnhlib.blockstate.init.BlockPropertyInit;
import com.gtnewhorizon.gtnhlib.blockstate.storage.NativeBlockStates;
import com.gtnewhorizon.gtnhlib.brigadier.BrigadierApi;
import com.gtnewhorizon.gtnhlib.brigadier.BrigadierCommandWrapper;
import com.gtnewhorizon.gtnhlib.chat.ChatComponentCustomRegistry;
@@ -31,8 +32,10 @@ import com.gtnewhorizon.gtnhlib.keybind.SyncedKeybind;
import com.gtnewhorizon.gtnhlib.network.NetworkHandler;
import com.gtnewhorizon.gtnhlib.network.PacketMessageAboveHotbar;
import com.gtnewhorizon.gtnhlib.network.PacketViewDistance;
import com.gtnewhorizon.gtnhlib.tags.ITagRegistrar;
import com.gtnewhorizon.gtnhlib.teams.TeamAdminCommand;
import com.gtnewhorizon.gtnhlib.teams.TeamCommand;
import com.gtnewhorizon.gtnhlib.blockstate.example.ExampleBlockStateBlock;
import com.gtnewhorizon.gtnhlib.test.block.BlockRngTest;
import com.gtnewhorizon.gtnhlib.test.block.BlockTest;
import com.gtnewhorizon.gtnhlib.test.block.BlockTestLectern;
@@ -75,6 +78,7 @@ public class CommonProxy {
BlockTestTintMul.register();
BlockRngTest.register();
BlockWeightedRngTest.register();
ExampleBlockStateBlock.register();
}
if (GTNHLibConfig.enableTestItems) {
@@ -105,6 +109,7 @@ public class CommonProxy {
AutoEventBus.executePhase(Phase.INIT);
NetworkHandler.init();
ConfigurationManager.onInit();
NativeBlockStates.init();
if ((boolean) Launch.blackboard.get("fml.deobfuscatedEnvironment")) {
SyncedKeybind.createConfigurable("gtnhlib.test_keybind", "debug", 0).registerGlobalListener(
@@ -17,6 +17,10 @@ public class GTNHLibConfig {
@Config.DefaultBoolean(true)
public static boolean enableMoreFlowerPottage;
@Config.Comment("Enable the ability to directly store BlockStates in the world")
@Config.DefaultBoolean(true)
public static boolean enableNativeBlockStates;
@Config.Comment("Larger values take more RAM, but require less model rebuilding (may reduce lag spikes). Unless you are very short on RAM, reducing this is not advised.")
@Config.DefaultInt(1000)
@Config.RangeInt(min = 1, max = 1_000_000)
@@ -0,0 +1,17 @@
package com.gtnewhorizon.gtnhlib.blockstate.core;
import org.jetbrains.annotations.Nullable;
@FunctionalInterface
public interface BlockPropertyValuePredicate {
/// Called for each block property value in a block state.
/// @param name The property name
/// @param state The property value's current state
/// @param property The property, or null if this value is deferred
/// @param value The value. The exact value [Class] depends on the property value's state - may be a [String] if
/// deferred.
@SuppressWarnings("rawtypes")
boolean test(String name, BlockPropertyState state, @Nullable BlockProperty property, Object value);
}
@@ -4,10 +4,12 @@ import java.util.Map;
import net.minecraft.block.Block;
import net.minecraft.item.ItemStack;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
import com.google.gson.JsonObject;
import com.gtnewhorizon.gtnhlib.geometry.TransformLike;
import cpw.mods.fml.common.registry.GameRegistry;
@@ -31,7 +33,7 @@ import cpw.mods.fml.common.registry.GameRegistry;
/// in the world and may depend on state that is not captured by a [BlockState] such as tile data, leading to erroneous
/// property value modification/removal.
/// 3. [BlockState]s have opt-in pooling. Use
/// [BlockPropertyRegistry#getBlockState(BlockStatePool, IBlockAccess, int, int, int)] and
/// [BlockPropertyRegistry#getBlockState(BlockStatePool, IBlockAccess , int, int, int)] and
/// [BlockPropertyRegistry#getBlockState(BlockStatePool, ItemStack)] to pool states, which significantly reduce
/// allocations. [#close()] must be called to return a [BlockState] to the pool. [BlockStatePool]s cannot be overfilled,
/// so there's no need to worry about memory leaks. [BlockState#close()] can be elided for non-pooled [BlockState]s.
@@ -47,6 +49,13 @@ public interface BlockState extends AutoCloseable, Cloneable {
/// Creates an exact copy of this state, within the same pool (if pooled).
BlockState clone();
/// Creates an exact copy of this state, within another pool.
/// Note that if this clone is between threads, both pools must be locked to avoid thread safety issues.
BlockState clone(@Nullable BlockStatePool otherPool);
/// Counts the number of valid properties in this state. Includes all property states.
int size();
/// Clears this [BlockState] and sets its block to the given block reference. The [BlockState] will not have any
/// properties after this method returns.
void reset(Block block);
@@ -71,6 +80,9 @@ public interface BlockState extends AutoCloseable, Cloneable {
@Nullable
BlockPropertyState getPropertyState(String name);
/// Returns true when any properties in this BlockState are unvalidated or deferred.
boolean needsReification();
/// Returns true if this state contains the given property (by reference). Deferred properties are not included.
default boolean hasProperty(BlockProperty<?> property) {
return getPropertyState(property) != null;
@@ -89,6 +101,9 @@ public interface BlockState extends AutoCloseable, Cloneable {
/// Removes deferred properties as well.
void removeProperty(String name);
/// Removes properties when they match a predicate.
void removeIf(BlockPropertyValuePredicate test);
/// Gets the value of a property. Does not include deferred properties. Includes unvalidated properties.
<T> T getPropertyValue(BlockProperty<T> property);
@@ -115,9 +130,25 @@ public interface BlockState extends AutoCloseable, Cloneable {
/// [BlockProperty#parse(String)]. Other types are undefined behaviour, but they will likely crash or log a message.
<T> void setPropertyValue(String name, @Nullable T value);
/// Inserts all property values in `source` into `this`.
/// When two properties exist with the same name, the property in `source` takes priority over the property in
/// `this`.
void putAll(BlockState source);
/// Copies all properties stored in this BlockState into a map. Key=Property Name, Value=Property Value (as text).
Map<String, String> toMap();
/// Iterates over all properties in this BlockState and inserts them into a json object.
/// Deferred properties are stored as strings. If a deferred value is not already a string, [Object#toString()] is
/// called on it.
JsonObject toJson();
/// Iterates over all values in the given json object and tries to load them into this BlockState.
/// Properties in this BlockState that are not in the object are ignored.
/// Deferred values may be converted to unvalidated values via the same semantics as
/// [#setPropertyValue(String, Object)].
void fromJson(JsonObject obj);
/// Transforms properties based on a given transform. Ignores deferred properties. Treats unvalidated properties the
/// same as normal properties.
void transform(TransformLike transform);
@@ -23,6 +23,7 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.google.gson.JsonObject;
import com.gtnewhorizon.gtnhlib.GTNHLib;
import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyRegistry;
import com.gtnewhorizon.gtnhlib.geometry.DirectionTransform;
@@ -278,6 +279,25 @@ public class BlockStateImpl implements BlockState {
return cloned.copy(this);
}
@Override
public BlockState clone(@Nullable BlockStatePool otherPool) {
BlockStateImpl cloned = otherPool != null ? otherPool.getInstance() : new BlockStateImpl();
return cloned.copy(this);
}
@Override
public int size() {
int count = 0;
for (int i = 0; i < entries.size(); i++) {
PropertyEntry entry = entries.get(i);
if (entry.name != null) count++;
}
return count;
}
private int findIndex(BlockProperty prop) {
for (int i = 0; i < entries.size(); i++) {
PropertyEntry entry = entries.get(i);
@@ -442,6 +462,19 @@ public class BlockStateImpl implements BlockState {
return index == -1 ? null : entryState(entries.get(index));
}
@Override
public boolean needsReification() {
for (int i = 0; i < entries.size(); i++) {
PropertyEntry entry = entries.get(i);
if (entry.name == null) continue;
if (entry.property == null || !entry.validated) return true;
}
return false;
}
@Override
public void removeProperty(BlockProperty<?> property) {
int index = findIndex(property);
@@ -454,6 +487,31 @@ public class BlockStateImpl implements BlockState {
if (index != -1) entries.get(index).reset();
}
@Override
public void removeIf(BlockPropertyValuePredicate test) {
for (int i = 0; i < entries.size(); i++) {
BlockStateImpl.PropertyEntry entry = entries.get(i);
if (entry.name == null) continue;
BlockPropertyState state;
if (!entry.isDeferred()) {
if (entry.validated) {
state = BlockPropertyState.NORMAL;
} else {
state = BlockPropertyState.UNVALIDATED;
}
} else {
state = BlockPropertyState.DEFERRED;
}
if (test.test(entry.name, state, entry.property, entry.value)) {
entry.reset();
}
}
}
@Override
public <T> T getPropertyValue(BlockProperty<T> property) {
int index = findIndex(property);
@@ -529,6 +587,42 @@ public class BlockStateImpl implements BlockState {
}
}
@Override
public void putAll(BlockState source) {
if (source instanceof BlockStateImpl impl) {
var other = impl.entries;
for (int i = 0; i < other.size(); i++) {
BlockStateImpl.PropertyEntry otherEntry = other.get(i);
if (otherEntry.name == null) continue;
int index = findIndex(otherEntry.name);
if (index == -1) {
index = findOrAddFreeIndex();
}
entries.get(index).copy(otherEntry);
}
} else {
source.forEachValue((name, state, property, value) -> {
int index = findIndex(name);
if (index == -1) {
index = findOrAddFreeIndex();
}
PropertyEntry entry = entries.get(index);
entry.name = name;
entry.property = property;
entry.value = value;
entry.validated = state == BlockPropertyState.NORMAL;
});
}
}
@Override
public Map<String, String> toMap() {
Map<String, String> out = new Object2ObjectOpenHashMap<>(entries.size());
@@ -548,6 +642,62 @@ public class BlockStateImpl implements BlockState {
return out;
}
@Override
public JsonObject toJson() {
JsonObject obj = new JsonObject();
for (int i = 0; i < entries.size(); i++) {
PropertyEntry entry = entries.get(i);
if (entry.name == null) continue;
if (entry.property != null) {
//noinspection unchecked
obj.add(entry.name, entry.property.serialize(entry.value));
} else {
obj.addProperty(entry.name, entry.value == null ? null : entry.value.toString());
}
}
return obj;
}
@Override
public void fromJson(JsonObject obj) {
for (var e : obj.entrySet()) {
var name = e.getKey();
var value = e.getValue();
int index = findIndex(name);
if (index == -1) {
index = findOrAddFreeIndex();
entries.get(index).deferred(name, value);
} else {
PropertyEntry entry = entries.get(index);
if (entry.property != null) {
if (entry.property.getType() != String.class) {
entry.value = entry.property.deserialize(value);
} else if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
entry.value = entry.property.parse(value.getAsString());
} else {
GTNHLib.LOG.warn(
"Tried to set property on BlockState to invalid type. Name={}, Value={}, Type={}, Expected={}",
name,
value,
value.getClass(),
entry.property.getType(),
new IllegalBlockStateException());
}
} else {
entry.deferred(name, value);
}
}
}
}
@Override
public void transform(TransformLike transform) {
for (int i = 0; i < entries.size(); i++) {
@@ -598,6 +748,8 @@ public class BlockStateImpl implements BlockState {
entry.reset();
continue;
}
entry.validated = true;
}
if (!entry.property.hasTrait(SupportsWorld) || !entry.property.hasTrait(WorldMutable)) {
@@ -650,6 +802,8 @@ public class BlockStateImpl implements BlockState {
continue;
}
entry.validated = true;
if (!match.hasTrait(SupportsWorld) || !match.hasTrait(WorldMutable)) {
GTNHLib.LOG.warn(
"Tried to set immutable or non-world-supporting property on block. Block={}, Property Name={}, Property={}, Value={}",
@@ -0,0 +1,20 @@
package com.gtnewhorizon.gtnhlib.blockstate.core;
import net.minecraft.block.Block;
import net.minecraft.world.World;
import com.gtnewhorizon.gtnhlib.blockstate.storage.NativeBlockStateAware;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockStateStorage;
/// Implemented on [World]s to expose [BlockState] setters and getters.
public interface BlockStateWorld extends IBlockStateAccess {
/// Sets the block in a given location to the provided block state.
/// This updates the [Block] in the voxel to whatever [BlockState#getBlock()] returns, and sets the metadata to
/// whatever [BlockState#getBlockMeta(int)] returns (with an existing meta of 0, or the previous metadata if the
/// block was already correct). Block equality can be modified by implementing [NativeBlockStateAware] on the [Block] and
/// overriding [NativeBlockStateAware#isSameBlock(Block)].
/// This also stores the [BlockState] into the [BlockStateStorage]
boolean setBlockState(int x, int y, int z, BlockState state, int flags);
}
@@ -0,0 +1,14 @@
package com.gtnewhorizon.gtnhlib.blockstate.core;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
/// Implemented on something that extends [IBlockAccess] (typically [World]s, but may be other world-like objects).
public interface IBlockStateAccess extends IBlockAccess {
/// Gets the native state from a given voxel.
@Nullable
BlockState getBlockState(int x, int y, int z);
}
@@ -0,0 +1,321 @@
package com.gtnewhorizon.gtnhlib.blockstate.example;
import java.lang.reflect.Type;
import java.util.Random;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.client.renderer.texture.IIconRegister;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.passive.EntityPig;
import net.minecraft.entity.passive.EntitySheep;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.item.ItemStack;
import net.minecraft.util.IIcon;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import org.jetbrains.annotations.Nullable;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockPropertyTrait;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockState;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockStateImpl;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockStatePool;
import com.gtnewhorizon.gtnhlib.blockstate.core.MetaBlockProperty;
import com.gtnewhorizon.gtnhlib.blockstate.properties.DirectionBlockProperty;
import com.gtnewhorizon.gtnhlib.blockstate.properties.FloatBlockProperty;
import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyRegistry;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockStateStorage;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockState_IBAExt;
import com.gtnewhorizon.gtnhlib.blockstate.storage.NativeBlockStateAware;
import com.gtnewhorizon.gtnhlib.blockstate.storage.NativeBlockStates;
import com.gtnewhorizon.gtnhlib.eventbus.EventBusSubscriber;
import com.gtnewhorizon.gtnhlib.tags.TagEvent.RegisterBlockTagsEvent;
import com.gtnewhorizon.gtnhlib.tags.TagEvent.RegisterEntityTagsEvent;
import com.gtnewhorizon.gtnhlib.tags.TagEvent.RegisterItemTagsEvent;
import com.gtnewhorizon.gtnhlib.util.DirectionUtil;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.registry.GameRegistry;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
/// Test block demonstrating native BlockState storage.
///
/// Metadata: bits 0-1 store horizontal facing (N=0, S=1, E=2, W=3). That's it —
/// heat lives entirely in extended block state, not in metadata.
///
/// When fire is placed in the facing direction, heat increases by 0.05/tick.
/// Without fire, heat decreases by 0.02/tick.
/// Color tints from cold blue → orange → white-hot based on the float heat value read
/// directly from extended state, including during chunk rendering (via MixinChunkCache_BlockStates).
public class ExampleBlockStateBlock extends Block implements NativeBlockStateAware {
private static final int FACING_MASK = 0b0011;
/// Horizontal facing (N/S/E/W) stored in metadata bits 0-1.
public static final HorizontalFacingProperty FACING = new HorizontalFacingProperty();
/// Heat level [0.0, 1.0] stored in extended block state.
/// Has arbitrarily fine precision — far more than the 16 discrete values metadata could hold.
public static final HeatProperty HEAT = new HeatProperty();
public static final ExampleBlockStateBlock BLOCK_LIT = new ExampleBlockStateBlock(true);
public static final ExampleBlockStateBlock BLOCK_UNLIT = new ExampleBlockStateBlock(false);
private final boolean lit;
public ExampleBlockStateBlock(boolean lit) {
super(Material.rock);
setHardness(1.5f);
setLightLevel(lit ? 1f : 0f);
this.lit = lit;
}
public static void register() {
BLOCK_LIT.setBlockName("native_state_test_lit");
GameRegistry.registerBlock(BLOCK_LIT, "native_state_test_lit");
BLOCK_LIT.registerProperties();
BLOCK_UNLIT.setBlockName("native_state_test");
GameRegistry.registerBlock(BLOCK_UNLIT, "native_state_test");
BLOCK_UNLIT.registerProperties();
}
public void registerProperties() {
BlockPropertyRegistry.registerBlockItemProperty(this, FACING, ForgeDirection.NORTH);
BlockPropertyRegistry.registerProperty(this, HEAT);
}
@Override
public BlockState getDefaultState(@Nullable BlockStatePool pool) {
BlockStateImpl state = new BlockStateImpl();
state.setBlock(this);
state.setPropertyValue(FACING, ForgeDirection.NORTH);
state.setPropertyValue(HEAT, 0f);
return state;
}
@Override
public boolean isSameBlock(Block newBlock) {
return newBlock == BLOCK_LIT || newBlock == BLOCK_UNLIT;
}
@Override
public Block getBlockForState(BlockState state) {
return state.getPropertyValue(HEAT) > 0.5F ? BLOCK_LIT : BLOCK_UNLIT;
}
@Override
public int getMetaForState(BlockState state) {
return state.getBlockMeta(0);
}
@Override
public void onBlockAdded(World world, int x, int y, int z) {
world.scheduleBlockUpdate(x, y, z, this, 20);
}
@Override
public void onBlockPlacedBy(World world, int x, int y, int z, EntityLivingBase placer, ItemStack stack) {
ForgeDirection dir = DirectionUtil.yawToDirection(placer.rotationYaw);
world.setBlockMetadataWithNotify(x, y, z, encodeFacing(dir), 2);
}
@Override
public void updateTick(World world, int x, int y, int z, Random rand) {
if (!NativeBlockStates.isEnabled() || world.isRemote) return;
ForgeDirection facing = decodeFacing(world.getBlockMetadata(x, y, z) & FACING_MASK);
BlockState state = NativeBlockStates.getBlockState(null, world, x, y, z);
if (state == null) return;
Float heat = state.getPropertyValue(HEAT);
if (heat == null) heat = 0f;
boolean hasFire = world.getBlock(
x + facing.offsetX, y + facing.offsetY, z + facing.offsetZ) == Blocks.fire;
float newHeat = Math.max(0f, Math.min(1f, heat + (hasFire ? 0.05f : -0.02f)));
if (newHeat != heat) {
state.setPropertyValue(HEAT, newHeat);
NativeBlockStates.setBlockState(world, x, y, z, state, 3);
}
world.scheduleBlockUpdate(x, y, z, this, 20);
}
// --- Rendering ---
@SideOnly(Side.CLIENT)
@Override
public int colorMultiplier(IBlockAccess worldIn, int x, int y, int z) {
BlockStateStorage storage = BlockState_IBAExt.get(worldIn, x, y, z);
if (storage == null) return COLD_COLOR;
BlockState state = storage.getBlockStateUnsafe(x & 0xF, y & 0xF, z & 0xF);
if (state == null) return COLD_COLOR;
Float heat = state.getPropertyValue(HEAT);
return heatToColor(heat != null ? heat : 0f);
}
@SideOnly(Side.CLIENT)
private IIcon front, top;
@SideOnly(Side.CLIENT)
@Override
public void registerBlockIcons(IIconRegister reg) {
this.blockIcon = reg.registerIcon("furnace_side");
this.front = reg.registerIcon(lit ? "furnace_front_on" : "furnace_front_off");
this.top = reg.registerIcon("furnace_top");
}
@SideOnly(Side.CLIENT)
@Override
public IIcon getIcon(int side, int meta) {
ForgeDirection facing = FACING.getValue(meta);
ForgeDirection side2 = ForgeDirection.getOrientation(side);
if (side2 == facing) return front;
if (side2.offsetY != 0) return top;
return blockIcon;
}
@SideOnly(Side.CLIENT)
@Override
public IIcon getIcon(IBlockAccess worldIn, int x, int y, int z, int side) {
ForgeDirection facing = FACING.getValue(worldIn, x, y, z);
ForgeDirection side2 = ForgeDirection.getOrientation(side);
if (side2 == facing) return front;
if (side2.offsetY != 0) return top;
return blockIcon;
}
private static final int COLD_COLOR = 0x4488FF;
private static int heatToColor(float heat) {
// 0.0 → cold blue (0x4488FF)
// 0.5 → orange (0xFF6600)
// 1.0 → white-hot (0xFFFFFF)
int r, g, b;
if (heat < 0.5f) {
float t = heat * 2f;
r = lerp(0x44, 0xFF, t);
g = lerp(0x88, 0x66, t);
b = lerp(0xFF, 0x00, t);
} else {
float t = (heat - 0.5f) * 2f;
r = 0xFF;
g = lerp(0x66, 0xFF, t);
b = lerp(0x00, 0xFF, t);
}
return r << 16 | g << 8 | b;
}
private static int lerp(int a, int b, float t) {
return Math.round(a + (b - a) * t);
}
// --- Meta helpers ---
private static int encodeFacing(ForgeDirection dir) {
return switch (dir) {
case SOUTH -> 1;
case EAST -> 2;
case WEST -> 3;
default -> 0; // NORTH
};
}
private static ForgeDirection decodeFacing(int bits) {
return switch (bits) {
case 1 -> ForgeDirection.SOUTH;
case 2 -> ForgeDirection.EAST;
case 3 -> ForgeDirection.WEST;
default -> ForgeDirection.NORTH;
};
}
// --- Property implementations ---
private static final class HorizontalFacingProperty
implements DirectionBlockProperty, MetaBlockProperty<ForgeDirection> {
@Override
public String getName() {
return "facing";
}
@Override
public boolean isValidDirection(ForgeDirection value) {
return value == ForgeDirection.NORTH || value == ForgeDirection.SOUTH
|| value == ForgeDirection.EAST || value == ForgeDirection.WEST;
}
@Override
public boolean hasTrait(BlockPropertyTrait trait) {
return switch (trait) {
case SupportsWorld, WorldMutable, OnlyNeedsMeta, SupportsStacks, StackMutable -> true;
default -> false;
};
}
@Override
public boolean needsExisting() {
return false; // facing occupies the only metadata bits; no preservation needed
}
@Override
public int getMeta(ForgeDirection value, int existing) {
return encodeFacing(value);
}
@Override
public ForgeDirection getValue(int meta) {
return decodeFacing(meta & FACING_MASK);
}
}
private static final class HeatProperty implements FloatBlockProperty {
@Override
public String getName() {
return "heat";
}
@Override
public Type getType() {
return float.class;
}
@Override
public boolean hasTrait(BlockPropertyTrait trait) {
return switch (trait) {
case SupportsWorld, WorldMutable -> true;
default -> false;
};
}
@Override
public Float getValue(IBlockAccess world, int x, int y, int z) {
BlockStateStorage storage = BlockState_IBAExt.get(world, x, y, z);
if (storage == null) return 0f;
BlockState state = storage.getBlockStateUnsafe(x & 0xF, y & 0xF, z & 0xF);
if (state == null) return 0f;
Float val = state.getPropertyValue(this);
return val != null ? val : 0f;
}
@Override
public void setValue(World world, int x, int y, int z, Float value) {
if (!NativeBlockStates.isEnabled()) return;
BlockState state = NativeBlockStates.getBlockState(null, world, x, y, z);
if (state == null) return;
state.setPropertyValue(this, value);
NativeBlockStates.setBlockState(world, x, y, z, state, 3);
}
}
}
@@ -14,6 +14,7 @@ import com.gtnewhorizon.gtnhlib.blockstate.core.MetaBlockProperty;
import com.gtnewhorizon.gtnhlib.blockstate.properties.IntegerBlockProperty;
import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyFactory;
import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyRegistry;
import com.gtnewhorizon.gtnhlib.blockstate.storage.NativeBlockStateAware;
public class BlockPropertyInit {
@@ -25,12 +26,20 @@ public class BlockPropertyInit {
@Override
public BlockProperty<Integer> getProperty(IBlockAccess world, int x, int y, int z, Block block, int meta,
@Nullable TileEntity tile) {
return metaProperty;
if (!(block instanceof NativeBlockStateAware)) {
return metaProperty;
} else {
return null;
}
}
@Override
public BlockProperty<Integer> getProperty(ItemStack stack, Item item, int meta) {
return metaProperty;
if (!(Block.getBlockFromItem(item) instanceof NativeBlockStateAware)) {
return metaProperty;
} else {
return null;
}
}
});
@@ -0,0 +1,403 @@
package com.gtnewhorizon.gtnhlib.blockstate.storage;
import java.nio.ByteBuffer;
import java.util.Objects;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.network.PacketBuffer;
import net.minecraft.network.play.server.S23PacketBlockChange;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.falsepattern.chunk.api.DataManager;
import com.falsepattern.chunk.api.DataManager.BlockPacketDataManager;
import com.falsepattern.chunk.api.DataManager.CubicPacketDataManager;
import com.falsepattern.chunk.api.DataManager.PacketDataManager;
import com.falsepattern.chunk.api.DataManager.SubChunkDataManager;
import com.google.gson.JsonObject;
import com.gtnewhorizon.gtnhlib.GTNHLib;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockState;
import com.gtnewhorizon.gtnhlib.util.EBSUtil;
import com.gtnewhorizon.gtnhlib.util.JsonUtil;
import io.netty.buffer.Unpooled;
@SuppressWarnings({ "resource", "UnstableApiUsage" })
public class BSSDataManager implements DataManager, PacketDataManager, CubicPacketDataManager, BlockPacketDataManager,
SubChunkDataManager {
public static final int MAX_BYTES_PER_STATE = 1024;
private static final ThreadLocal<PacketBuffer> STATE_BUFFERS = ThreadLocal.withInitial(() -> new PacketBuffer(Unpooled.buffer(MAX_BYTES_PER_STATE)));
@Contract(pure = true)
@Override
public String domain() {
return GTNHLib.MODID;
}
@Contract(pure = true)
@Override
public String id() {
return "blockstate";
}
@Contract(pure = true)
@Override
public @NotNull String version() {
return "1.0.0";
}
@Contract(pure = true)
@Override
public @Nullable String newInstallDescription() {
return "GTNHLib native block state storage. This allows mods to emulate modern's BlockStates without metadata shims.";
}
@Contract(pure = true)
@Override
public @NotNull String uninstallMessage() {
return "GTNHLib's native block state storage has been uninstalled. Any blocks that use this will be corrupted.";
}
@Contract(pure = true)
@Override
public @Nullable String versionChangeMessage(String priorVersion) {
return null;
}
@Contract(mutates = "param1")
@Override
public void writeBlockToPacket(Chunk chunk, int x, int y, int z, S23PacketBlockChange packet) {
// Server world -> S23
if (!(packet.field_148883_d instanceof NativeBlockStateAware)) return;
if (!(packet instanceof BlockState_S23Ext ext)) return;
ExtendedBlockStorage ebs = EBSUtil.getEBS(chunk, y >> 4);
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, false);
if (storage == null) return;
BlockState state = storage.getBlockStateUnsafe(x & 0xF, y & 0xF, z & 0xF);
ext.gtnhlib$setBlockState(state == null ? null : state.toJson());
}
@Contract(mutates = "param2")
@Override
public void writeBlockPacketToBuffer(S23PacketBlockChange packet, PacketBuffer buffer) {
// S23 -> buffer
if (!(packet.field_148883_d instanceof NativeBlockStateAware)) return;
if (!(packet instanceof BlockState_S23Ext ext)) return;
JsonUtil.writeBSON(buffer, ext.gtnhlib$getBlockState());
}
@Contract(mutates = "param1,param2")
@Override
public void readBlockPacketFromBuffer(S23PacketBlockChange packet, PacketBuffer buffer) {
// Buffer -> S23
if (!(packet.field_148883_d instanceof NativeBlockStateAware bsa)) return;
if (!(packet instanceof BlockState_S23Ext ext)) return;
try {
ext.gtnhlib$setBlockState(JsonUtil.readBSON(buffer));
} catch (Throwable t) {
GTNHLib.LOG.error(
"Failed to load BlockState from S23PacketBlockChange (X={}, Y={}, Z={}, Block={})",
packet.func_148879_d(),
packet.func_148878_e(),
packet.func_148877_f(),
bsa,
t);
}
}
@Contract(mutates = "param1,param5")
@Override
public void readBlockFromPacket(Chunk chunk, int x, int y, int z, S23PacketBlockChange packet) {
// S23 -> client world
if (!(packet instanceof BlockState_S23Ext ext)) return;
NativeBlockStateAware bsa = packet.field_148883_d instanceof NativeBlockStateAware bsa2 ? bsa2 : null;
ExtendedBlockStorage ebs = EBSUtil.getEBS(chunk, y >> 4);
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, bsa != null);
if (bsa == null) {
if (storage != null && storage.hasState(x & 0xF, y & 0xF, z & 0xF)) {
storage.setBlockState(x & 0xF, y & 0xF, z & 0xF, null);
if (storage.isEmpty()) {
BlockStateStorage.removeStorage(ebs);
}
}
return;
}
Objects.requireNonNull(storage, "Storage should not be null");
BlockState blockState = bsa.getDefaultState(null);
blockState.fromJson(ext.gtnhlib$getBlockState());
if (blockState.needsReification()) {
blockState.reify(chunk.worldObj, (chunk.xPosition << 4) + x, y, (chunk.zPosition << 4) + z);
}
storage.setBlockState(x & 0xF, y & 0xF, z & 0xF, blockState);
chunk.func_150807_a(x & 0xF, y, z & 0xF, bsa.getBlockForState(blockState), bsa.getMetaForState(blockState));
chunk.worldObj.markBlockForUpdate((chunk.xPosition << 4) + x, y, (chunk.zPosition << 4) + z);
}
@Contract(pure = true)
@Override
public int maxPacketSizeCubic() {
return MAX_BYTES_PER_STATE * 16 * 16 * 16;
}
@Contract(mutates = "param3")
@Override
public void writeToBuffer(Chunk chunk, ExtendedBlockStorage ebs, ByteBuffer buffer) {
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, false);
PacketBuffer packet = new PacketBuffer(Unpooled.wrappedBuffer(buffer));
packet.clear();
if (storage == null) {
packet.writeVarIntToBuffer(0);
buffer.position(buffer.position() + packet.writerIndex());
return;
}
var states = storage.getBlockStates();
packet.writeVarIntToBuffer(states.size());
PacketBuffer stateBuffer = STATE_BUFFERS.get();
states.forEach((loc, state) -> {
stateBuffer.clear();
stateBuffer.writeShort(loc);
JsonUtil.writeBSON(stateBuffer, state.toJson());
int stateLen = stateBuffer.writerIndex();
if (stateLen > MAX_BYTES_PER_STATE) {
GTNHLib.LOG.error("Block state tried to sync too many bytes: reduce the number of bytes used, or increase the limit in the GTNHLib config. (Bytes used={}, State={})", stateLen, state);
stateBuffer.clear();
stateBuffer.writeShort(loc);
JsonUtil.writeBSON(stateBuffer, new JsonObject());
stateLen = stateBuffer.writerIndex();
}
packet.writeVarIntToBuffer(stateLen);
packet.writeBytes(stateBuffer, 0, stateLen);
});
buffer.position(buffer.position() + packet.writerIndex());
}
@Contract(mutates = "param1,param2,param3")
@Override
public void readFromBuffer(Chunk chunk, ExtendedBlockStorage ebs, ByteBuffer buffer) {
PacketBuffer packet = new PacketBuffer(Unpooled.wrappedBuffer(buffer));
packet.writerIndex(buffer.remaining()).readerIndex(0);
int stateCount = packet.readVarIntFromBuffer();
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, stateCount > 0);
if (stateCount == 0 && storage != null) {
// We have a storage, but we don't want to have any states, remove the storage to clear them
BlockStateStorage.removeStorage(ebs);
buffer.position(buffer.position() + packet.readerIndex());
return;
}
if (storage == null) {
// No storage and we aren't adding any new states, do nothing
buffer.position(buffer.position() + packet.readerIndex());
return;
}
PacketBuffer stateBuffer = STATE_BUFFERS.get();
for (int i = 0; i < stateCount; i++) {
stateBuffer.clear();
int stateLen = packet.readVarIntFromBuffer();
packet.readBytes(stateBuffer, 0, stateLen);
stateBuffer.readerIndex(0).writerIndex(stateLen);
short loc = stateBuffer.readShort();
JsonObject props = JsonUtil.readBSON(stateBuffer);
int rx = BlockStateStorage.unpackX(loc);
int ry = BlockStateStorage.unpackY(loc);
int rz = BlockStateStorage.unpackZ(loc);
if (!(ebs.getBlockByExtId(rx, ry, rz) instanceof NativeBlockStateAware bsa)) {
continue;
}
try {
BlockState state = bsa.getDefaultState(null);
state.fromJson(props);
if (state.needsReification()) {
state.reify(chunk.worldObj, (chunk.xPosition << 4) + rx, ebs.getYLocation() + ry, (chunk.zPosition << 4) + rz);
}
storage.setBlockState(rx, ry, rz, state);
} catch (Throwable t) {
GTNHLib.LOG.error(
"Failed to load BlockState (World={}, X={}, Y={}, Z={}, Block={})",
chunk.worldObj,
(chunk.xPosition << 4) + rx,
ebs.getYLocation() + ry,
(chunk.zPosition << 4) + rz,
bsa,
t);
}
}
buffer.position(buffer.position() + packet.readerIndex());
}
@Contract(pure = true)
@Override
public int maxPacketSize() {
return MAX_BYTES_PER_STATE * 16 * 16 * 16 * 16;
}
@Contract(mutates = "param4")
@Override
public void writeToBuffer(Chunk chunk, int subChunkMask, boolean forceUpdate, ByteBuffer buffer) {
var ebses = chunk.getBlockStorageArray();
for (int i = 0; i < ebses.length; i++) {
if ((subChunkMask & 1 << i) == 0) continue;
var ebs = ebses[i];
writeToBuffer(chunk, ebs, buffer);
}
}
@Contract(mutates = "param1,param4")
@Override
public void readFromBuffer(Chunk chunk, int subChunkMask, boolean forceUpdate, ByteBuffer buffer) {
var ebses = chunk.getBlockStorageArray();
for (int i = 0; i < ebses.length; i++) {
if ((subChunkMask & 1 << i) == 0) continue;
var ebs = ebses[i];
readFromBuffer(chunk, ebs, buffer);
}
}
@Contract(mutates = "param3")
@Override
public void writeSubChunkToNBT(Chunk chunk, ExtendedBlockStorage ebs, NBTTagCompound nbt) {
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, false);
if (storage == null) return;
var states = storage.getBlockStates();
if (states.isEmpty()) return;
PacketBuffer stateBuffer = STATE_BUFFERS.get();
NBTTagList list = new NBTTagList();
states.forEach((loc, state) -> {
stateBuffer.clear();
JsonUtil.writeBSON(stateBuffer, state.toJson());
int len = stateBuffer.writerIndex();
byte[] data = new byte[len];
stateBuffer.getBytes(0, data);
NBTTagCompound entry = new NBTTagCompound();
entry.setShort("loc", loc);
entry.setByteArray("data", data);
list.appendTag(entry);
});
nbt.setTag("states", list);
}
@Contract(mutates = "param2")
@Override
public void readSubChunkFromNBT(Chunk chunk, ExtendedBlockStorage ebs, NBTTagCompound nbt) {
if (!nbt.hasKey("states")) return;
NBTTagList list = nbt.getTagList("states", 10); // 10 = TAG_Compound
if (list.tagCount() == 0) return;
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, true);
if (storage == null) return;
PacketBuffer stateBuffer = STATE_BUFFERS.get();
for (int i = 0; i < list.tagCount(); i++) {
NBTTagCompound entry = list.getCompoundTagAt(i);
short loc = entry.getShort("loc");
byte[] data = entry.getByteArray("data");
stateBuffer.clear();
stateBuffer.writeBytes(data);
stateBuffer.readerIndex(0);
JsonObject props = JsonUtil.readBSON(stateBuffer);
int rx = BlockStateStorage.unpackX(loc);
int ry = BlockStateStorage.unpackY(loc);
int rz = BlockStateStorage.unpackZ(loc);
if (!(ebs.getBlockByExtId(rx, ry, rz) instanceof NativeBlockStateAware bsa)) continue;
try {
BlockState state = bsa.getDefaultState(null);
state.fromJson(props);
// EBS is not in the world at this point so we can't reify the state here
storage.setBlockState(rx, ry, rz, state);
} catch (Throwable t) {
GTNHLib.LOG.error(
"Failed to load BlockState (World={}, X={}, Y={}, Z={}, Block={})",
chunk.worldObj,
(chunk.xPosition << 4) + rx,
ebs.getYLocation() + ry,
(chunk.zPosition << 4) + rz,
bsa,
t);
}
}
}
@Contract(mutates = "param3")
@Override
public void cloneSubChunk(Chunk fromChunk, ExtendedBlockStorage from, ExtendedBlockStorage to) {
if (!(to instanceof BlockState_EBSExt toExt)) return;
BlockStateStorage storage = BlockStateStorage.getStorage(from, false);
toExt.gtnhlib$setBlockStateStorage(storage == null ? null : storage.clone());
}
}
@@ -0,0 +1,12 @@
package com.gtnewhorizon.gtnhlib.blockstate.storage;
import org.jetbrains.annotations.Nullable;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockState;
public interface BlockSnapshot_Ext {
@Nullable BlockState gtnhlib$getCapturedState();
void gtnhlib$setCapturedState(@Nullable BlockState state);
}
@@ -0,0 +1,117 @@
package com.gtnewhorizon.gtnhlib.blockstate.storage;
import java.util.BitSet;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
import org.jetbrains.annotations.Nullable;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockState;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockStatePool;
import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap;
import lombok.Getter;
import lombok.SneakyThrows;
/// This is the core data structure for native block states. It is the block state equivalent of an [ExtendedBlockStorage],
/// except instead of using discrete IDs it relies on a Short2Object map with bitpacked keys.
/// This does not manage the lifetime of native states at all - that is typically handled by the various mixins, or [BSSDataManager].
public class BlockStateStorage implements Cloneable {
/// A bit will be true when the corresponding voxel contains a block state.
/// This is used to skip pointless map operations, i.e. when getting a state from a location that doesn't have one.
@Getter
private BitSet presence = new BitSet(16*16*16);
@Getter
private Short2ObjectOpenHashMap<BlockState> blockStates = new Short2ObjectOpenHashMap<>();
public static short pack(int x, int y, int z) {
x &= 0xF;
y &= 0xF;
z &= 0xF;
return (short) (y << 8 | z << 4 | x);
}
public static int unpackX(int l) {
return l & 0xF;
}
public static int unpackY(int l) {
return l >> 8 & 0xF;
}
public static int unpackZ(int l) {
return l >> 4 & 0xF;
}
public boolean hasState(int rx, int ry, int rz) {
return presence.get(pack(rx, ry, rz));
}
/// Allocation-free getter. Returned value must be treated as immutable and not modified.
public @Nullable BlockState getBlockStateUnsafe(int rx, int ry, int rz) {
if (!hasState(rx, ry, rz)) return null;
return blockStates.get(pack(rx, ry, rz));
}
public @Nullable BlockState getBlockState(int rx, int ry, int rz, @Nullable BlockStatePool pool) {
BlockState state = getBlockStateUnsafe(rx, ry, rz);
return state == null ? null : state.clone(pool);
}
public void setBlockState(int rx, int ry, int rz, @Nullable BlockState state) {
if (state == null) {
blockStates.remove(pack(rx, ry, rz));
presence.clear(pack(rx, ry, rz));
} else {
blockStates.put(pack(rx, ry, rz), state.clone(null));
presence.set(pack(rx, ry, rz));
}
}
public boolean isEmpty() {
return blockStates.isEmpty();
}
@SneakyThrows
@Override
public BlockStateStorage clone() {
BlockStateStorage copy = (BlockStateStorage) super.clone();
copy.blockStates = copy.blockStates.clone();
copy.presence = (BitSet) copy.presence.clone();
copy.blockStates.replaceAll((loc, state) -> state.clone(null));
return copy;
}
public static @Nullable BlockStateStorage getStorage(ExtendedBlockStorage ebs, boolean createIfMissing) {
if (ebs == null) return null;
if (!(ebs instanceof BlockState_EBSExt ext)) return null;
BlockStateStorage storage = ext.gtnhlib$getBlockStateStorage();
if (createIfMissing && storage == null) {
storage = new BlockStateStorage();
ext.gtnhlib$setBlockStateStorage(storage);
}
return storage;
}
public static boolean removeStorage(ExtendedBlockStorage ebs) {
if (ebs == null) return false;
if (!(ebs instanceof BlockState_EBSExt ext)) return false;
if (ext.gtnhlib$getBlockStateStorage() != null) {
ext.gtnhlib$setBlockStateStorage(null);
return true;
} else {
return false;
}
}
}
@@ -0,0 +1,8 @@
package com.gtnewhorizon.gtnhlib.blockstate.storage;
public interface BlockState_EBSExt {
BlockStateStorage gtnhlib$getBlockStateStorage();
void gtnhlib$setBlockStateStorage(BlockStateStorage storage);
}
@@ -0,0 +1,22 @@
package com.gtnewhorizon.gtnhlib.blockstate.storage;
import net.minecraft.world.IBlockAccess;
import org.jetbrains.annotations.ApiStatus.Internal;
import org.jetbrains.annotations.Nullable;
/// Implemented on [IBlockAccess] types (World, ChunkCache) to expose [BlockStateStorage] lookups
/// without requiring a cast to [net.minecraft.world.World]. This allows block rendering code
/// (which only receives [IBlockAccess]) to read extended block state.
@Internal
public interface BlockState_IBAExt {
@Nullable BlockStateStorage gtnhlib$getBlockStateStorage(int x, int y, int z);
static @Nullable BlockStateStorage get(IBlockAccess iba, int x, int y, int z) {
if (iba instanceof BlockState_IBAExt ext) {
return ext.gtnhlib$getBlockStateStorage(x, y, z);
}
return null;
}
}
@@ -0,0 +1,13 @@
package com.gtnewhorizon.gtnhlib.blockstate.storage;
import org.jetbrains.annotations.ApiStatus.Internal;
import com.google.gson.JsonObject;
@Internal
public interface BlockState_S23Ext {
JsonObject gtnhlib$getBlockState();
void gtnhlib$setBlockState(JsonObject state);
}
@@ -0,0 +1,7 @@
package com.gtnewhorizon.gtnhlib.blockstate.storage;
import com.gtnewhorizon.gtnhlib.blockstate.core.IBlockStateAccess;
public interface BlockState_WorldExt extends IBlockStateAccess {
}
@@ -0,0 +1,63 @@
package com.gtnewhorizon.gtnhlib.blockstate.storage;
import net.minecraft.block.Block;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.World;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockState;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockStatePool;
import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyRegistry;
/// Implemented on [Block]s that can have a [BlockState] stored in the native block state storage system.
/// Blocks without this interface will never have a native [BlockState].
/// <p>
/// When a block implements this interface, the [BlockState] in a [BlockStateStorage] is always the source-of-truth for
/// a given block property's value. [#getMetaForState(BlockState, int)] is used to update the metadata value in the world so
/// that meta block properties work transparently. Non-meta block properties MUST use the [BlockState] in the
/// [BlockStateStorage] as their backing data source - relying on other fields (such as [TileEntity] fields) for their
/// value is undefined behaviour, and likely will not work well.
/// <p>
/// Implementing this interface on a [Block] precludes third-party property registration via
/// [BlockPropertyRegistry]. The two systems are mutually exclusive and cannot operate in tandem.
public interface NativeBlockStateAware {
/// Gets the default state for a [Block]. Must create a new instance (optionally from the pool, if one is provided).
@Contract("_ -> new")
BlockState getDefaultState(@Nullable BlockStatePool pool);
/// Called when a [Block] changes in the world to determine whether the next block is logically equivalent to the
/// current block (`this`). When this returns false, the [BlockState] will be discarded and optionally re-created if
/// the new block also implements [NativeBlockStateAware].
/// If this returns true, `newBlock` must also implement [NativeBlockStateAware].
/// This function must be commutative - `this.isSameBlock(other) == other.isSameBlock(this)` must always be true.
default boolean isSameBlock(Block newBlock) {
return newBlock == this;
}
/// Called when a [Block] is initially placed into the world, to determine what [BlockState] should be used. This is
/// primarily used for third-party compatibility, in case something places your block with a specific meta value.
default BlockState getInitialState(@Nullable BlockStatePool pool, Block placedBlock, int placedMeta) {
return getDefaultState(pool);
}
/// Called when the block or metadata for a location changes, without changing its identity. This is only called when
/// [#isSameBlock(Block)] returns true.
/// @returns Null when nothing should be updated, or a new [BlockState] otherwise
@Nullable
default BlockState onBlockChanged(@Nullable BlockStatePool pool, BlockState state, Block oldBlock, Block newBlock, int oldMeta, int newMeta) {
return null;
}
/// Gets the [Block] for a given [BlockState], when the [BlockState] is placed into a [World].
default Block getBlockForState(BlockState state) {
return state.getBlock();
}
/// Gets the metadata for a given [BlockState], when the [BlockState] is placed into a [World].
default int getMetaForState(BlockState state) {
return state.getBlockMeta(0);
}
}
@@ -0,0 +1,63 @@
package com.gtnewhorizon.gtnhlib.blockstate.storage;
import net.minecraft.world.World;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
import org.jetbrains.annotations.Nullable;
import com.falsepattern.chunk.api.DataRegistry;
import com.gtnewhorizon.gtnhlib.GTNHLibConfig;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockState;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockStatePool;
import com.gtnewhorizon.gtnhlib.util.EBSUtil;
import cpw.mods.fml.common.Loader;
import lombok.Getter;
public class NativeBlockStates {
@Getter
private static boolean isEnabled;
public static void init() {
isEnabled = GTNHLibConfig.enableNativeBlockStates && Loader.isModLoaded("chunkapi");
if (isEnabled) {
DataRegistry.registerDataManager(new BSSDataManager(), 10);
}
}
public static boolean setBlockState(World world, int x, int y, int z, @Nullable BlockState state, int flags) {
if (!isEnabled) {
throw new IllegalStateException("Native BlockState storage is not enabled");
}
if (state == null) {
if (!world.setBlockToAir(x, y, z)) return false;
} else {
if (!(state.getBlock() instanceof NativeBlockStateAware bsa)) return false;
world.setBlock(x, y, z, bsa.getBlockForState(state), bsa.getMetaForState(state), flags);
ExtendedBlockStorage ebs = EBSUtil.getEBS(world, x >> 4, y >> 4, z >> 4);
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, true);
storage.setBlockState(x & 0xF, y & 0xF, z & 0xF, state);
world.markBlockForUpdate(x, y, z);
}
return true;
}
public static BlockState getBlockState(BlockStatePool pool, World world, int x, int y, int z) {
if (!isEnabled) {
throw new IllegalStateException("Native BlockState storage is not enabled");
}
ExtendedBlockStorage ebs = EBSUtil.getEBS(world, x >> 4, y >> 4, z >> 4);
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, false);
if (storage == null) return null;
return storage.getBlockState(x & 0xF, y & 0xF, z & 0xF, pool);
}
}
@@ -75,6 +75,8 @@ public enum Mixins implements IMixins {
"flowerpotcompat.MixinBOPPlant",
"flowerpotcompat.MixinBOPMushroom")
.addRequiredMod(TargetMods.BIOMES_O_PLENTY)),
NATIVE_BLOCK_STATE(new MixinBuilder("Inject fields needed for native BlockState storage")
.addCommonMixins("MixinEBS_BlockStates", "MixinS23_BlockStates", "MixinChunk_BlockStates", "MixinBlockSnapshot_BlockStates", "MixinChunkCache_BlockStates", "MixinWorld_BlockStates").setPhase(Phase.EARLY)),
//
;
@@ -0,0 +1,71 @@
package com.gtnewhorizon.gtnhlib.mixins.early;
import net.minecraft.block.Block;
import net.minecraft.world.World;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
import net.minecraftforge.common.util.BlockSnapshot;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockState;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockSnapshot_Ext;
import com.gtnewhorizon.gtnhlib.blockstate.storage.NativeBlockStateAware;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockStateStorage;
import com.gtnewhorizon.gtnhlib.util.EBSUtil;
@Mixin(value = BlockSnapshot.class, remap = false)
public class MixinBlockSnapshot_BlockStates implements BlockSnapshot_Ext {
@Unique
private @Nullable BlockState gtnhlib$capturedState;
@Override
public @Nullable BlockState gtnhlib$getCapturedState() {
return gtnhlib$capturedState;
}
@Override
public void gtnhlib$setCapturedState(@Nullable BlockState state) {
gtnhlib$capturedState = state;
}
/// Captures the extended block state at snapshot creation time.
/// Constructor (World, int, int, int, Block, int, int) delegates here via this(...), so this covers all factory paths.
@Inject(method = "<init>(Lnet/minecraft/world/World;IIILnet/minecraft/block/Block;I)V", at = @At("TAIL"))
private void gtnhlib$captureBlockState(World world, int x, int y, int z, Block block, int meta, CallbackInfo ci) {
if (!(block instanceof NativeBlockStateAware)) return;
ExtendedBlockStorage ebs = EBSUtil.getEBS(world, x >> 4, y >> 4, z >> 4);
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, false);
if (storage == null) return;
gtnhlib$capturedState = storage.getBlockState(x & 0xF, y & 0xF, z & 0xF, null);
}
/// Writes the captured state back after restore(). The chunk mixin will have written the default state during
/// the setBlock call inside restore(); this overwrites it with the original.
@Inject(method = "restore(ZZ)Z", at = @At("TAIL"))
private void gtnhlib$restoreBlockState(boolean force, boolean applyPhysics, CallbackInfoReturnable<Boolean> cir) {
if (!cir.getReturnValueZ() || gtnhlib$capturedState == null) return;
BlockSnapshot self = (BlockSnapshot) (Object) this;
ExtendedBlockStorage ebs = EBSUtil.getEBS(self.world, self.x >> 4, self.y >> 4, self.z >> 4);
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, true);
if (storage != null) {
storage.setBlockState(self.x & 0xF, self.y & 0xF, self.z & 0xF, gtnhlib$capturedState);
}
}
@Inject(method = "restoreToLocation(Lnet/minecraft/world/World;IIIZZ)Z", at = @At("TAIL"))
private void gtnhlib$restoreBlockStateToLocation(World world, int x, int y, int z, boolean force, boolean applyPhysics, CallbackInfoReturnable<Boolean> cir) {
if (!cir.getReturnValueZ() || gtnhlib$capturedState == null) return;
ExtendedBlockStorage ebs = EBSUtil.getEBS(world, x >> 4, y >> 4, z >> 4);
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, true);
if (storage != null) {
storage.setBlockState(x & 0xF, y & 0xF, z & 0xF, gtnhlib$capturedState);
}
}
}
@@ -0,0 +1,30 @@
package com.gtnewhorizon.gtnhlib.mixins.early;
import net.minecraft.world.ChunkCache;
import net.minecraft.world.chunk.Chunk;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockState_IBAExt;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockStateStorage;
import com.gtnewhorizon.gtnhlib.util.EBSUtil;
@Mixin(ChunkCache.class)
public class MixinChunkCache_BlockStates implements BlockState_IBAExt {
@Shadow private int chunkX;
@Shadow private int chunkZ;
@Shadow private Chunk[][] chunkArray;
@Override
public @Nullable BlockStateStorage gtnhlib$getBlockStateStorage(int x, int y, int z) {
int cx = (x >> 4) - chunkX;
int cz = (z >> 4) - chunkZ;
if (cx < 0 || cz < 0 || cx >= chunkArray.length || cz >= chunkArray[cx].length) return null;
Chunk chunk = chunkArray[cx][cz];
if (chunk == null) return null;
return BlockStateStorage.getStorage(EBSUtil.getEBS(chunk, y >> 4), false);
}
}
@@ -0,0 +1,140 @@
package com.gtnewhorizon.gtnhlib.mixins.early;
import net.minecraft.block.Block;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.At.Shift;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockState;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockStateStorage;
import com.gtnewhorizon.gtnhlib.blockstate.storage.NativeBlockStateAware;
import com.gtnewhorizon.gtnhlib.util.EBSUtil;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.ref.LocalIntRef;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
@Mixin(Chunk.class)
public class MixinChunk_BlockStates {
@Unique
private BlockState gtnhlib$tempState;
@Inject(method = "func_150807_a", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/world/chunk/Chunk;getBlockMetadata(III)I", shift = Shift.AFTER, ordinal = 0))
private void gtnhlib$checkMetaChanged(
int x, int y, int z,
Block newBlock, int newMeta,
CallbackInfoReturnable<Boolean> cir,
@Local(name = "block1") Block oldBlock, @Local(type = int.class, name = "k1") int oldMeta,
@Local(type = Block.class, argsOnly = true, name = "p_150807_4_") LocalRef<Block> newBlockRef,
@Local(type = int.class, argsOnly = true, name = "p_150807_5_") LocalIntRef newMetaRef
) {
// When the metadata or block for a location changes, we need to hook into it in case `onBlockChanged` needs to be called.
// This will also intercept the provided block/meta and change it according to the [BlockState].
// This breaks a set->get assumption for Chunks (the set block will always be what's fetched on the next get) so I'm not sure this is worth it.
// In theory, the state should properly handle all mutations (i.e. changing to a lit Block from an unlit Block should set the lit property to true, which causes the lit Block to be placed) but it's very easy to get this wrong.
Chunk self = (Chunk) (Object) this;
boolean oldAware = oldBlock instanceof NativeBlockStateAware;
boolean newAware = newBlock instanceof NativeBlockStateAware;
if (!oldAware && !newAware) return;
if (oldBlock instanceof NativeBlockStateAware bsa && newAware && bsa.isSameBlock(newBlock)) {
// Old and new blocks are logically equivalent.
// Call onBlockChanged and optionally update the state.
ExtendedBlockStorage ebs = EBSUtil.getEBS(self, y >> 4);
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, true);
BlockState state = storage.getBlockStateUnsafe(x & 0xF, y & 0xF, z & 0xF);
if (state == null) {
state = bsa.getDefaultState(null);
}
BlockState newState = bsa.onBlockChanged(null, state, oldBlock, newBlock, oldMeta, newMeta);
if (newState == null) {
newState = state;
}
storage.setBlockState(x & 0xF, y & 0xF, z & 0xF, newState);
newBlockRef.set(bsa.getBlockForState(newState));
newMetaRef.set(bsa.getMetaForState(newState));
} else if (newBlock instanceof NativeBlockStateAware bsa) {
// New block is unrelated to the old block.
// Fetch its initial state, cache it, and intercept the requested block/meta for the correct values.
gtnhlib$tempState = bsa.getInitialState(null, newBlock, newMeta);
newBlockRef.set(bsa.getBlockForState(gtnhlib$tempState));
newMetaRef.set(bsa.getMetaForState(gtnhlib$tempState));
}
}
@Inject(method = "func_150807_a", at = @At(value = "RETURN"))
private void gtnhlib$resetTempState(
int x, int y, int z,
Block newBlock, int newMeta,
CallbackInfoReturnable<Boolean> cir
) {
// Reset cached state to avoid memory leaks/logic bugs
gtnhlib$tempState = null;
}
@Inject(method = "func_150807_a", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;onBlockAdded(Lnet/minecraft/world/World;III)V", shift = Shift.BEFORE))
private void gtnhlib$onBlockChanged(int x, int y, int z, Block newBlock, int newMetadata, CallbackInfoReturnable<Boolean> cir,
@Local(name = "block1") Block oldBlock) {
// This method detects newly placed [NativeBlockStateAware] Blocks and creates or removes the [BlockStateStorage] as needed.
Chunk self = (Chunk) (Object) this;
boolean oldAware = oldBlock instanceof NativeBlockStateAware;
boolean newAware = newBlock instanceof NativeBlockStateAware;
// Neither is aware, bail since we don't care about them
if (!oldAware && !newAware) return;
// Logical transition (e.g. furnace <-> lit_furnace) — preserve existing state
if (oldAware && newAware && ((NativeBlockStateAware) oldBlock).isSameBlock(newBlock)) return;
// Get storage; create it only if the incoming block will need it
ExtendedBlockStorage ebs = EBSUtil.getEBS(self, y >> 4);
BlockStateStorage storage = BlockStateStorage.getStorage(ebs, newAware);
// Clear the outgoing block's state since the two blocks are logically different.
// We don't want to pass the previous block's state to the new block
if (oldAware && storage != null) {
storage.setBlockState(x & 0xF, y & 0xF, z & 0xF, null);
// Incoming block is not Aware — remove storage if now empty
if (!newAware && storage.isEmpty()) {
BlockStateStorage.removeStorage(ebs);
}
}
// Install the default state for the incoming block.
// This ensures a valid state is present even when a third party places a
// BlockStateAware block via world.setBlock directly. NativeBlockStates will
// overwrite this with the caller-supplied state immediately after setBlock returns.
if (newAware && storage != null) {
NativeBlockStateAware bsa = (NativeBlockStateAware) newBlock;
// Consume the cached default state, or fetch another one if there is none
if (gtnhlib$tempState == null) {
gtnhlib$tempState = bsa.getDefaultState(null);
}
storage.setBlockState(x & 0xF, y & 0xF, z & 0xF, gtnhlib$tempState);
gtnhlib$tempState = null;
}
}
}
@@ -0,0 +1,26 @@
package com.gtnewhorizon.gtnhlib.mixins.early;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockStateStorage;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockState_EBSExt;
@Mixin(ExtendedBlockStorage.class)
public class MixinEBS_BlockStates implements BlockState_EBSExt {
@Unique
private BlockStateStorage gtnhlib$blockStates;
@Override
public BlockStateStorage gtnhlib$getBlockStateStorage() {
return gtnhlib$blockStates;
}
@Override
public void gtnhlib$setBlockStateStorage(BlockStateStorage storage) {
gtnhlib$blockStates = storage;
}
}
@@ -0,0 +1,26 @@
package com.gtnewhorizon.gtnhlib.mixins.early;
import net.minecraft.network.play.server.S23PacketBlockChange;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import com.google.gson.JsonObject;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockState_S23Ext;
@Mixin(S23PacketBlockChange.class)
public class MixinS23_BlockStates implements BlockState_S23Ext {
@Unique
private JsonObject gtnhlib$blockState;
@Override
public JsonObject gtnhlib$getBlockState() {
return gtnhlib$blockState;
}
@Override
public void gtnhlib$setBlockState(JsonObject state) {
gtnhlib$blockState = state;
}
}
@@ -0,0 +1,20 @@
package com.gtnewhorizon.gtnhlib.mixins.early;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockState_IBAExt;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockStateStorage;
import com.gtnewhorizon.gtnhlib.util.EBSUtil;
@Mixin(World.class)
public class MixinWorld_BlockStates implements BlockState_IBAExt {
@Override
public @Nullable BlockStateStorage gtnhlib$getBlockStateStorage(int x, int y, int z) {
return BlockStateStorage.getStorage(
EBSUtil.getEBS((World) (Object) this, x >> 4, y >> 4, z >> 4), false);
}
}
@@ -0,0 +1,20 @@
package com.gtnewhorizon.gtnhlib.util;
import net.minecraft.world.World;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.storage.ExtendedBlockStorage;
public class EBSUtil {
/// Gets an [ExtendedBlockStorage] from the world.
public static ExtendedBlockStorage getEBS(World world, int chunkX, int ebsY, int chunkZ) {
Chunk chunk = world.getChunkFromChunkCoords(chunkX, chunkZ);
return getEBS(chunk, ebsY);
}
/// Gets an [ExtendedBlockStorage] from a specific chunk. Note: this method is overwritten by cubic chunks.
public static ExtendedBlockStorage getEBS(Chunk chunk, int ebsY) {
return ebsY < 0 || ebsY >= chunk.getBlockStorageArray().length ? null : chunk.getBlockStorageArray()[ebsY];
}
}
@@ -1,6 +1,19 @@
package com.gtnewhorizon.gtnhlib.util;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.function.BiConsumer;
import net.minecraft.network.PacketBuffer;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import it.unimi.dsi.fastutil.bytes.ByteArrayList;
public class JsonUtil {
@@ -33,4 +46,200 @@ public class JsonUtil {
return defaul;
}
private static final int BSON_DOUBLE = 1;
private static final int BSON_UTF8_STRING = 2;
private static final int BSON_DOCUMENT = 3;
private static final int BSON_ARRAY = 4;
private static final int BSON_BOOLEAN = 8;
private static final int BSON_NULL = 10;
private static final int BSON_INT = 16;
private static final int BSON_LONG = 18;
public static void writeBSON(PacketBuffer buffer, JsonObject obj) {
buffer.writeInt(Integer.reverseBytes(obj.entrySet().size()));
for (var e : obj.entrySet()) {
writeBSONElement(buffer, e.getKey(), e.getValue());
}
buffer.writeByte(0);
}
private static void writeBSONElement(PacketBuffer buffer, String ename, JsonElement el) {
if (el.isJsonNull()) {
buffer.writeByte(BSON_NULL);
writeBsonCString(buffer, ename);
} else if (el instanceof JsonPrimitive prim) {
if (prim.isBoolean()) {
buffer.writeByte(BSON_BOOLEAN);
writeBsonCString(buffer, ename);
buffer.writeByte(prim.getAsBoolean() ? 1 : 0);
} else if (prim.isString()) {
byte[] utf8 = prim.getAsString().getBytes(StandardCharsets.UTF_8);
buffer.writeByte(BSON_UTF8_STRING);
writeBsonCString(buffer, ename);
buffer.writeInt(Integer.reverseBytes(utf8.length + 1));
buffer.writeBytes(utf8);
buffer.writeByte(0);
} else {
Class<? extends Number> type = prim.getAsNumber().getClass();
if (type == Integer.class || type == Short.class) {
buffer.writeByte(BSON_INT);
writeBsonCString(buffer, ename);
buffer.writeInt(Integer.reverseBytes(prim.getAsInt()));
} else if (type == Long.class || type == BigInteger.class) {
buffer.writeByte(BSON_LONG);
writeBsonCString(buffer, ename);
buffer.writeLong(Long.reverseBytes(prim.getAsLong()));
} else if (type == Float.class || type == Double.class || type == BigDecimal.class) {
buffer.writeByte(BSON_DOUBLE);
writeBsonCString(buffer, ename);
buffer.writeLong(Long.reverseBytes(Double.doubleToLongBits(prim.getAsDouble())));
} else {
// LazilyParsedNumber (Gson-parsed JSON) and other unknown Number types:
// inspect the string to decide int vs double.
String numStr = prim.getAsNumber().toString();
if (numStr.contains(".") || numStr.contains("e") || numStr.contains("E")) {
buffer.writeByte(BSON_DOUBLE);
writeBsonCString(buffer, ename);
buffer.writeLong(Long.reverseBytes(Double.doubleToLongBits(prim.getAsDouble())));
} else {
long lval = prim.getAsLong();
if (lval >= Integer.MIN_VALUE && lval <= Integer.MAX_VALUE) {
buffer.writeByte(BSON_INT);
writeBsonCString(buffer, ename);
buffer.writeInt(Integer.reverseBytes((int) lval));
} else {
buffer.writeByte(BSON_LONG);
writeBsonCString(buffer, ename);
buffer.writeLong(Long.reverseBytes(lval));
}
}
}
}
} else if (el.isJsonArray()) {
int key = 0;
buffer.writeByte(BSON_ARRAY);
writeBsonCString(buffer, ename);
buffer.writeInt(Integer.reverseBytes(el.getAsJsonArray().size()));
for (JsonElement el2 : el.getAsJsonArray()) {
writeBSONElement(buffer, Integer.toString(key++), el2);
}
buffer.writeByte(0);
} else if (el.isJsonObject()) {
buffer.writeByte(BSON_DOCUMENT);
writeBsonCString(buffer, ename);
writeBSON(buffer, el.getAsJsonObject());
}
}
public static JsonObject readBSON(PacketBuffer buffer) {
int len = Integer.reverseBytes(buffer.readInt());
JsonObject obj = new JsonObject();
BiConsumer<String, JsonElement> adder = obj::add;
for (int i = 0; i < len; i++) {
readBSONElement(buffer, adder);
}
buffer.readByte();
return obj;
}
private static void readBSONElement(PacketBuffer buffer, BiConsumer<String, JsonElement> adder) {
int type = buffer.readByte();
String ename = readBsonCString(buffer);
switch (type) {
case BSON_DOUBLE -> {
adder.accept(ename, new JsonPrimitive(Double.longBitsToDouble(Long.reverseBytes(buffer.readLong()))));
}
case BSON_UTF8_STRING -> {
int len = Integer.reverseBytes(buffer.readInt());
byte[] data = new byte[len];
buffer.readBytes(data);
adder.accept(ename, new JsonPrimitive(new String(data, 0, len - 1, StandardCharsets.UTF_8)));
}
case BSON_DOCUMENT -> {
adder.accept(ename, readBSON(buffer));
}
case BSON_ARRAY -> {
int len = Integer.reverseBytes(buffer.readInt());
JsonArray array = new JsonArray();
BiConsumer<String, JsonElement> adder2 = (key, value) -> array.add(value);
for (int i = 0; i < len; i++) {
readBSONElement(buffer, adder2);
}
buffer.readByte();
adder.accept(ename, array);
}
case BSON_BOOLEAN -> {
adder.accept(ename, new JsonPrimitive(buffer.readByte() != 0));
}
case BSON_NULL -> {
adder.accept(ename, JsonNull.INSTANCE);
}
case BSON_INT -> {
adder.accept(ename, new JsonPrimitive(Integer.reverseBytes(buffer.readInt())));
}
case BSON_LONG -> {
adder.accept(ename, new JsonPrimitive(Long.reverseBytes(buffer.readLong())));
}
default -> {
throw new IllegalStateException("Invalid bson code: " + type);
}
}
}
private static void writeBsonCString(PacketBuffer buffer, String str) {
byte[] ascii = str.getBytes(StandardCharsets.UTF_8);
for (byte b : ascii) {
if (b == 0) {
throw new IllegalArgumentException("cstring cannot contain 0: '" + str + "' / " + Arrays.toString(ascii));
}
}
buffer.writeBytes(ascii);
buffer.writeByte(0);
}
private static String readBsonCString(PacketBuffer buffer) {
final ByteArrayList bytes = new ByteArrayList();
byte b;
while ((b = buffer.readByte()) != 0) {
bytes.add(b);
}
return new String(bytes.toByteArray(), StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,50 @@
package com.gtnewhorizon.gtnhlib.world;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.world.IWorldAccess;
/// A subinterface of [IWorldAccess], but with all the methods stubbed to keep implementations clean.
public interface IStubbedWorldAccess extends IWorldAccess {
@Override
default void markBlockForUpdate(int x, int y, int z) {}
@Override
default void markBlockForRenderUpdate(int x, int y, int z) {}
@Override
default void markBlockRangeForRenderUpdate(int x1, int y1, int z1, int x2, int y2, int z2) {}
@Override
default void playSound(String soundName, double x, double y, double z, float volume, float pitch) {}
@Override
default void playSoundToNearExcept(
EntityPlayer player, String soundName, double x, double y, double z, float volume, float pitch) {}
@Override
default void spawnParticle(
String particleName, double x, double y, double z, double motionX, double motionY, double motionZ) {}
@Override
default void onEntityCreate(Entity entity) {}
@Override
default void onEntityDestroy(Entity entity) {}
@Override
default void playRecord(String recordName, int x, int y, int z) {}
@Override
default void broadcastSound(int soundId, int x, int y, int z, int userdata) {}
@Override
default void playAuxSFX(EntityPlayer player, int soundId, int x, int y, int z, int userdata) {}
@Override
default void destroyBlockPartially(int playerEntityId, int x, int y, int z, int blockDamage) {}
@Override
default void onStaticEntitiesChanged() {}
}
@@ -0,0 +1,333 @@
package com.gtnewhorizon.gtnhlib.blockstate;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockStateImpl;
import com.gtnewhorizon.gtnhlib.blockstate.storage.BlockStateStorage;
class BlockStateStorageTests {
private BlockStateStorage storage;
@BeforeEach
void setUp() {
storage = new BlockStateStorage();
}
// --- pack/unpack roundtrip ---
@Test
void pack_unpackX_allValues_roundtrip() {
for (int x = 0; x < 16; x++) {
Assertions.assertEquals(x, BlockStateStorage.unpackX(BlockStateStorage.pack(x, 0, 0)));
}
}
@Test
void pack_unpackY_allValues_roundtrip() {
for (int y = 0; y < 16; y++) {
Assertions.assertEquals(y, BlockStateStorage.unpackY(BlockStateStorage.pack(0, y, 0)));
}
}
@Test
void pack_unpackZ_allValues_roundtrip() {
for (int z = 0; z < 16; z++) {
Assertions.assertEquals(z, BlockStateStorage.unpackZ(BlockStateStorage.pack(0, 0, z)));
}
}
@Test
void pack_allPositions_roundtrip() {
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 16; y++) {
for (int z = 0; z < 16; z++) {
short key = BlockStateStorage.pack(x, y, z);
Assertions.assertEquals(x, BlockStateStorage.unpackX(key), "x mismatch at " + x + "," + y + "," + z);
Assertions.assertEquals(y, BlockStateStorage.unpackY(key), "y mismatch at " + x + "," + y + "," + z);
Assertions.assertEquals(z, BlockStateStorage.unpackZ(key), "z mismatch at " + x + "," + y + "," + z);
}
}
}
}
@Test
void pack_outOfRangeCoords_maskedTo4Bits() {
// Coordinates above 15 should be masked, same as 0-15 counterparts
Assertions.assertEquals(BlockStateStorage.pack(0, 0, 0), BlockStateStorage.pack(16, 16, 16));
Assertions.assertEquals(BlockStateStorage.pack(1, 2, 3), BlockStateStorage.pack(17, 18, 19));
}
// --- isEmpty ---
@Test
void newStorage_isEmpty() {
Assertions.assertTrue(storage.isEmpty());
}
@Test
void afterSet_notEmpty() {
storage.setBlockState(0, 0, 0, new BlockStateImpl());
Assertions.assertFalse(storage.isEmpty());
}
@Test
void afterSetThenClear_isEmpty() {
storage.setBlockState(0, 0, 0, new BlockStateImpl());
storage.setBlockState(0, 0, 0, null);
Assertions.assertTrue(storage.isEmpty());
}
@Test
void clearLastOfMultiple_isEmpty() {
storage.setBlockState(0, 0, 0, new BlockStateImpl());
storage.setBlockState(1, 1, 1, new BlockStateImpl());
storage.setBlockState(0, 0, 0, null);
Assertions.assertFalse(storage.isEmpty());
storage.setBlockState(1, 1, 1, null);
Assertions.assertTrue(storage.isEmpty());
}
// --- hasState ---
@Test
void newStorage_hasState_false() {
Assertions.assertFalse(storage.hasState(0, 0, 0));
}
@Test
void afterSet_hasState_true() {
storage.setBlockState(5, 7, 3, new BlockStateImpl());
Assertions.assertTrue(storage.hasState(5, 7, 3));
}
@Test
void hasState_differentPos_false() {
storage.setBlockState(5, 7, 3, new BlockStateImpl());
Assertions.assertFalse(storage.hasState(5, 7, 4));
}
@Test
void afterClear_hasState_false() {
storage.setBlockState(5, 7, 3, new BlockStateImpl());
storage.setBlockState(5, 7, 3, null);
Assertions.assertFalse(storage.hasState(5, 7, 3));
}
// --- getBlockState ---
@Test
void getBlockState_neverSet_returnsNull() {
Assertions.assertNull(storage.getBlockState(0, 0, 0, null));
}
@Test
void getBlockState_afterSet_returnsEqualState() {
BlockStateImpl state = new BlockStateImpl();
state.setPropertyValue("foo", "bar");
storage.setBlockState(3, 5, 7, state);
BlockStateImpl retrieved = (BlockStateImpl) storage.getBlockState(3, 5, 7, null);
Assertions.assertNotNull(retrieved);
Assertions.assertEquals("bar", retrieved.getPropertyValue("foo", true));
}
@Test
void getBlockState_returnsClone_notSameReference() {
BlockStateImpl state = new BlockStateImpl();
storage.setBlockState(0, 0, 0, state);
Assertions.assertNotSame(state, storage.getBlockState(0, 0, 0, null));
}
@Test
void getBlockState_afterClear_returnsNull() {
storage.setBlockState(0, 0, 0, new BlockStateImpl());
storage.setBlockState(0, 0, 0, null);
Assertions.assertNull(storage.getBlockState(0, 0, 0, null));
}
@Test
void getBlockState_multiplePositions_independent() {
BlockStateImpl stateA = new BlockStateImpl();
stateA.setPropertyValue("key", "a");
BlockStateImpl stateB = new BlockStateImpl();
stateB.setPropertyValue("key", "b");
storage.setBlockState(0, 0, 0, stateA);
storage.setBlockState(1, 1, 1, stateB);
Assertions.assertEquals("a", storage.getBlockState(0, 0, 0, null).getPropertyValue("key", true));
Assertions.assertEquals("b", storage.getBlockState(1, 1, 1, null).getPropertyValue("key", true));
}
@Test
void getBlockStateUnsafe_neverSet_returnsNull() {
Assertions.assertNull(storage.getBlockStateUnsafe(0, 0, 0));
}
@Test
void getBlockStateUnsafe_afterSet_returnsSameReference() {
BlockStateImpl state = new BlockStateImpl();
storage.setBlockState(0, 0, 0, state);
// setBlockState clones on write, so unsafe get returns the stored clone — not the original
Assertions.assertNotNull(storage.getBlockStateUnsafe(0, 0, 0));
}
// --- setBlockState overwrites ---
@Test
void setBlockState_twice_secondValueWins() {
BlockStateImpl first = new BlockStateImpl();
first.setPropertyValue("key", "first");
BlockStateImpl second = new BlockStateImpl();
second.setPropertyValue("key", "second");
storage.setBlockState(0, 0, 0, first);
storage.setBlockState(0, 0, 0, second);
Assertions.assertEquals("second", storage.getBlockState(0, 0, 0, null).getPropertyValue("key", true));
}
@Test
void setBlockState_nullOnEmpty_noException() {
Assertions.assertDoesNotThrow(() -> storage.setBlockState(0, 0, 0, null));
}
// --- clone ---
@Test
void clone_notSameReference() {
Assertions.assertNotSame(storage, storage.clone());
}
@Test
void clone_containsSameProperties() {
BlockStateImpl state = new BlockStateImpl();
state.setPropertyValue("key", "val");
storage.setBlockState(2, 4, 6, state);
BlockStateStorage copy = storage.clone();
Assertions.assertEquals("val", copy.getBlockState(2, 4, 6, null).getPropertyValue("key", true));
}
@Test
void clone_mutatingOriginalDoesNotAffectCopy() {
BlockStateImpl state = new BlockStateImpl();
state.setPropertyValue("key", "original");
storage.setBlockState(0, 0, 0, state);
BlockStateStorage copy = storage.clone();
BlockStateImpl updated = new BlockStateImpl();
updated.setPropertyValue("key", "changed");
storage.setBlockState(0, 0, 0, updated);
Assertions.assertEquals("original", copy.getBlockState(0, 0, 0, null).getPropertyValue("key", true));
}
@Test
void clone_mutatingCopyDoesNotAffectOriginal() {
BlockStateImpl state = new BlockStateImpl();
state.setPropertyValue("key", "original");
storage.setBlockState(0, 0, 0, state);
BlockStateStorage copy = storage.clone();
copy.setBlockState(0, 0, 0, null);
Assertions.assertNotNull(storage.getBlockState(0, 0, 0, null));
}
@Test
void clone_emptyStorage_cloneAlsoEmpty() {
Assertions.assertTrue(storage.clone().isEmpty());
}
// --- State machine simulations ---
// These verify the storage-level behaviour that MixinChunk_BlockStates produces
// for each case, without requiring a live Minecraft world.
/// Case A: logical transition (isSameBlock=true) — state preserved, no operations on storage.
@Test
void stateMachine_caseA_preservesState() {
BlockStateImpl original = new BlockStateImpl();
original.setPropertyValue("orientation", "north");
storage.setBlockState(0, 0, 0, original);
// Mixin returns early on isSameBlock=true — storage untouched
Assertions.assertEquals("north", storage.getBlockState(0, 0, 0, null).getPropertyValue("orientation", true));
}
/// Case B: Aware→different Aware — old state cleared, default written.
@Test
void stateMachine_caseB_clearsOldWritesDefault() {
BlockStateImpl oldState = new BlockStateImpl();
oldState.setPropertyValue("orientation", "north");
storage.setBlockState(0, 0, 0, oldState);
// Mixin: clear old state
storage.setBlockState(0, 0, 0, null);
// Mixin: write default for new block
BlockStateImpl defaultState = new BlockStateImpl();
storage.setBlockState(0, 0, 0, defaultState);
Assertions.assertNotNull(storage.getBlockState(0, 0, 0, null));
Assertions.assertNull(storage.getBlockState(0, 0, 0, null).getPropertyValue("orientation", true));
}
/// Case C: Aware→not Aware — state cleared; storage removed when empty.
@Test
void stateMachine_caseC_clearsAndEmptiesStorage() {
BlockStateImpl state = new BlockStateImpl();
state.setPropertyValue("key", "val");
storage.setBlockState(0, 0, 0, state);
// Mixin: clear old state
storage.setBlockState(0, 0, 0, null);
Assertions.assertNull(storage.getBlockState(0, 0, 0, null));
Assertions.assertTrue(storage.isEmpty());
}
/// Case C with multiple entries — other entries survive, storage not removed.
@Test
void stateMachine_caseC_otherPositionsSurvive() {
storage.setBlockState(0, 0, 0, new BlockStateImpl());
storage.setBlockState(1, 1, 1, new BlockStateImpl());
storage.setBlockState(0, 0, 0, null);
Assertions.assertFalse(storage.isEmpty());
Assertions.assertNotNull(storage.getBlockState(1, 1, 1, null));
}
/// Case D: not Aware→Aware — fresh storage, default state written.
@Test
void stateMachine_caseD_defaultStateWritten() {
// Storage is freshly allocated (simulating getStorage with createIfMissing=true)
Assertions.assertTrue(storage.isEmpty());
BlockStateImpl defaultState = new BlockStateImpl();
storage.setBlockState(0, 0, 0, defaultState);
Assertions.assertNotNull(storage.getBlockState(0, 0, 0, null));
}
/// NativeBlockStates.setBlockState overwrites the default written by the mixin.
@Test
void stateMachine_callerStateOverwritesDefault() {
// Mixin writes default (Case D or B)
storage.setBlockState(0, 0, 0, new BlockStateImpl());
// Caller writes real state
BlockStateImpl real = new BlockStateImpl();
real.setPropertyValue("orientation", "east");
storage.setBlockState(0, 0, 0, real);
Assertions.assertEquals("east", storage.getBlockState(0, 0, 0, null).getPropertyValue("orientation", true));
}
}
@@ -0,0 +1,265 @@
package com.gtnewhorizon.gtnhlib.test.util;
import static org.junit.jupiter.api.Assertions.*;
import io.netty.buffer.Unpooled;
import net.minecraft.network.PacketBuffer;
import org.junit.jupiter.api.Test;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.gtnewhorizon.gtnhlib.util.JsonUtil;
public class JsonUtilBsonTest {
private static PacketBuffer newBuffer() {
return new PacketBuffer(Unpooled.buffer());
}
// --- helpers ---
private static JsonObject roundtrip(JsonObject obj) {
PacketBuffer buf = newBuffer();
JsonUtil.writeBSON(buf, obj);
return JsonUtil.readBSON(buf);
}
// --- null ---
@Test
void roundtrip_null() {
JsonObject obj = new JsonObject();
obj.add("k", JsonNull.INSTANCE);
JsonObject result = roundtrip(obj);
assertTrue(result.get("k").isJsonNull());
}
// --- boolean ---
@Test
void roundtrip_boolean_true() {
JsonObject obj = new JsonObject();
obj.add("v", new JsonPrimitive(true));
assertTrue(roundtrip(obj).get("v").getAsBoolean());
}
@Test
void roundtrip_boolean_false() {
JsonObject obj = new JsonObject();
obj.add("v", new JsonPrimitive(false));
assertFalse(roundtrip(obj).get("v").getAsBoolean());
}
// --- string ---
@Test
void roundtrip_string() {
JsonObject obj = new JsonObject();
obj.add("s", new JsonPrimitive("hello"));
// BUG: current impl includes the BSON null terminator in the string length
// field but then reads all `len` bytes into the output, yielding "hello\0".
assertEquals("hello", roundtrip(obj).get("s").getAsString());
}
@Test
void roundtrip_string_empty() {
JsonObject obj = new JsonObject();
obj.add("s", new JsonPrimitive(""));
// BUG: same null-terminator issue — empty string becomes "\0".
assertEquals("", roundtrip(obj).get("s").getAsString());
}
@Test
void roundtrip_string_unicode() {
JsonObject obj = new JsonObject();
obj.add("s", new JsonPrimitive("héllo wörld \u4e2d\u6587"));
assertEquals("héllo wörld \u4e2d\u6587", roundtrip(obj).get("s").getAsString());
}
// --- int ---
@Test
void roundtrip_int() {
JsonObject obj = new JsonObject();
obj.add("zero", new JsonPrimitive(0));
obj.add("pos", new JsonPrimitive(42));
obj.add("neg", new JsonPrimitive(-1));
obj.add("min", new JsonPrimitive(Integer.MIN_VALUE));
obj.add("max", new JsonPrimitive(Integer.MAX_VALUE));
JsonObject r = roundtrip(obj);
assertEquals(0, r.get("zero").getAsInt());
assertEquals(42, r.get("pos").getAsInt());
assertEquals(-1, r.get("neg").getAsInt());
assertEquals(Integer.MIN_VALUE, r.get("min").getAsInt());
assertEquals(Integer.MAX_VALUE, r.get("max").getAsInt());
}
// --- long ---
@Test
void roundtrip_long() {
JsonObject obj = new JsonObject();
obj.add("v", new JsonPrimitive(123456789012345L));
obj.add("min", new JsonPrimitive(Long.MIN_VALUE));
obj.add("max", new JsonPrimitive(Long.MAX_VALUE));
JsonObject r = roundtrip(obj);
assertEquals(123456789012345L, r.get("v").getAsLong());
assertEquals(Long.MIN_VALUE, r.get("min").getAsLong());
assertEquals(Long.MAX_VALUE, r.get("max").getAsLong());
}
// --- double ---
@Test
void roundtrip_double() {
JsonObject obj = new JsonObject();
obj.add("pi", new JsonPrimitive(Math.PI));
obj.add("neg", new JsonPrimitive(-2.718281828));
obj.add("nan", new JsonPrimitive(Double.NaN));
obj.add("inf", new JsonPrimitive(Double.POSITIVE_INFINITY));
JsonObject r = roundtrip(obj);
assertEquals(Math.PI, r.get("pi").getAsDouble());
assertEquals(-2.718281828, r.get("neg").getAsDouble());
assertEquals(Double.NaN, r.get("nan").getAsDouble());
assertEquals(Double.POSITIVE_INFINITY, r.get("inf").getAsDouble());
}
// --- float (stored as BSON_DOUBLE) ---
@Test
void roundtrip_float() {
JsonObject obj = new JsonObject();
obj.add("v", new JsonPrimitive(1.5f));
JsonObject r = roundtrip(obj);
assertEquals(1.5f, r.get("v").getAsFloat(), 1e-7f);
}
// --- nested object ---
@Test
void roundtrip_nested_object() {
JsonObject inner = new JsonObject();
inner.add("x", new JsonPrimitive(10));
JsonObject obj = new JsonObject();
obj.add("inner", inner);
JsonObject r = roundtrip(obj);
JsonObject rInner = r.getAsJsonObject("inner");
assertNotNull(rInner);
assertEquals(10, rInner.get("x").getAsInt());
}
// --- array ---
@Test
void roundtrip_array_of_ints() {
JsonArray arr = new JsonArray();
arr.add(new JsonPrimitive(1));
arr.add(new JsonPrimitive(2));
arr.add(new JsonPrimitive(3));
JsonObject obj = new JsonObject();
obj.add("arr", arr);
JsonArray r = roundtrip(obj).getAsJsonArray("arr");
assertNotNull(r);
assertEquals(3, r.size());
assertEquals(1, r.get(0).getAsInt());
assertEquals(2, r.get(1).getAsInt());
assertEquals(3, r.get(2).getAsInt());
}
@Test
void roundtrip_empty_array() {
JsonObject obj = new JsonObject();
obj.add("arr", new JsonArray());
JsonArray r = roundtrip(obj).getAsJsonArray("arr");
assertNotNull(r);
assertEquals(0, r.size());
}
@Test
void roundtrip_array_mixed_types() {
JsonArray arr = new JsonArray();
arr.add(JsonNull.INSTANCE);
arr.add(new JsonPrimitive(true));
arr.add(new JsonPrimitive(7));
JsonObject obj = new JsonObject();
obj.add("arr", arr);
JsonArray r = roundtrip(obj).getAsJsonArray("arr");
assertTrue(r.get(0).isJsonNull());
assertTrue(r.get(1).getAsBoolean());
assertEquals(7, r.get(2).getAsInt());
}
// --- empty top-level object ---
@Test
void roundtrip_empty_object() {
assertEquals(0, roundtrip(new JsonObject()).entrySet().size());
}
// --- complex nested ---
@Test
void roundtrip_complex() {
JsonArray arr = new JsonArray();
arr.add(new JsonPrimitive(99L));
arr.add(JsonNull.INSTANCE);
JsonObject inner = new JsonObject();
inner.add("flag", new JsonPrimitive(false));
JsonObject obj = new JsonObject();
obj.add("count", new JsonPrimitive(5));
obj.add("arr", arr);
obj.add("sub", inner);
obj.add("nullVal", JsonNull.INSTANCE);
JsonObject r = roundtrip(obj);
assertEquals(5, r.get("count").getAsInt());
assertTrue(r.get("nullVal").isJsonNull());
assertFalse(r.getAsJsonObject("sub").get("flag").getAsBoolean());
JsonArray rArr = r.getAsJsonArray("arr");
assertEquals(99L, rArr.get(0).getAsLong());
assertTrue(rArr.get(1).isJsonNull());
}
// --- LazilyParsedNumber: numbers from a parsed JSON string ---
@Test
void roundtrip_parsed_json_int() {
// Gson stores parsed numbers as LazilyParsedNumber, not Integer/Long/etc.
// writeBSONElement's class-based switch falls through all cases for
// LazilyParsedNumber, silently dropping the element.
// This test documents that the roundtrip must preserve the value.
JsonObject obj = new Gson().fromJson("{\"n\": 42}", JsonObject.class);
JsonObject r = roundtrip(obj);
assertTrue(r.has("n"), "parsed int must survive roundtrip (LazilyParsedNumber bug)");
assertEquals(42, r.get("n").getAsInt());
}
@Test
void roundtrip_parsed_json_float() {
JsonObject obj = new Gson().fromJson("{\"v\": 1.5}", JsonObject.class);
JsonObject r = roundtrip(obj);
assertTrue(r.has("v"), "parsed float must survive roundtrip (LazilyParsedNumber bug)");
assertEquals(1.5, r.get("v").getAsDouble(), 1e-9);
}
}