mirror of
https://github.com/GTNewHorizons/GTNHLib.git
synced 2026-11-13 17:04:22 +00:00
RFC: Native BlockStates
This commit is contained in:
1 parent
e729fc64d4
commit
b1681dbcfc
31 files changed
+2480
-3
No files matched your search
@@ -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)
|
||||
|
||||
+17
@@ -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);
|
||||
}
|
||||
+321
@@ -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)),
|
||||
//
|
||||
;
|
||||
|
||||
|
||||
+71
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user