9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-19 15:09:15 +00:00

Merge branch 'Xiao-MoMi:dev' into dev

This commit is contained in:
jhqwqmc
2025-11-29 00:09:40 +08:00
committed by GitHub
77 changed files with 1362 additions and 627 deletions

View File

@@ -42,7 +42,7 @@ public class MythicItemDrop extends ItemDrop implements IItemDrop {
context = ItemBuildContext.of(player);
}
}
int amountInt = MiscUtils.fastFloor(amount + 0.5F);
int amountInt = MiscUtils.floor(amount + 0.5F);
ItemStack itemStack = this.customItem.buildItemStack(context, amountInt);
return adapt(itemStack).amount(amountInt);
}

View File

@@ -42,6 +42,7 @@ import java.util.*;
public final class BukkitBlockManager extends AbstractBlockManager {
public static final Set<Object> CLIENT_SIDE_NOTE_BLOCKS = new HashSet<>(2048, 0.6f);
private static final Object BLOCK_POS$ZERO = LocationUtils.toBlockPos(0,0,0);
private static final Object ALWAYS_FALSE = FastNMS.INSTANCE.method$StatePredicate$always(false);
private static final Object ALWAYS_TRUE = FastNMS.INSTANCE.method$StatePredicate$always(true);
private static BukkitBlockManager instance;
@@ -80,12 +81,14 @@ public final class BukkitBlockManager extends AbstractBlockManager {
@Override
public void init() {
super.init();
this.initMirrorRegistry();
this.initFireBlock();
this.deceiveBukkitRegistry();
this.markVanillaNoteBlocks();
this.findViewBlockingVanillaBlocks();
Arrays.fill(this.immutableBlockStates, EmptyBlock.INSTANCE.defaultState());
this.plugin.networkManager().registerBlockStatePacketListeners(this.blockStateMappings); // 一定要预先初始化一次预防id超出上限
this.plugin.networkManager().registerBlockStatePacketListeners(this.blockStateMappings, this::isViewBlockingBlock); // 一定要预先初始化一次预防id超出上限
}
public static BukkitBlockManager instance() {
@@ -125,7 +128,7 @@ public final class BukkitBlockManager extends AbstractBlockManager {
@Override
public void delayedLoad() {
this.plugin.networkManager().registerBlockStatePacketListeners(this.blockStateMappings); // 重置方块映射表
this.plugin.networkManager().registerBlockStatePacketListeners(this.blockStateMappings, this::isViewBlockingBlock); // 重置方块映射表
super.delayedLoad();
this.cachedVisualBlockStatePacket = VisualBlockStatePacket.create();
for (BukkitServerPlayer player : BukkitNetworkManager.instance().onlineUsers()) {
@@ -223,7 +226,7 @@ public final class BukkitBlockManager extends AbstractBlockManager {
protected void applyPlatformSettings(ImmutableBlockState state) {
DelegatingBlockState nmsState = (DelegatingBlockState) state.customBlockState().literalObject();
nmsState.setBlockState(state);
Object nmsVisualState = state.vanillaBlockState().literalObject();
Object nmsVisualState = state.visualBlockState().literalObject();
BlockSettings settings = state.settings();
try {
@@ -240,8 +243,15 @@ public final class BukkitBlockManager extends AbstractBlockManager {
boolean useShapeForLightOcclusion = settings.useShapeForLightOcclusion() == Tristate.UNDEFINED ? CoreReflections.field$BlockStateBase$useShapeForLightOcclusion.getBoolean(nmsVisualState) : settings.useShapeForLightOcclusion().asBoolean();
CoreReflections.field$BlockStateBase$useShapeForLightOcclusion.set(nmsState, useShapeForLightOcclusion);
CoreReflections.field$BlockStateBase$isRedstoneConductor.set(nmsState, settings.isRedstoneConductor().asBoolean() ? ALWAYS_TRUE : ALWAYS_FALSE);
boolean suffocating = settings.isSuffocating() == Tristate.UNDEFINED ? (canBlockView(state.visualBlockState())) : (settings.isSuffocating().asBoolean());
CoreReflections.field$BlockStateBase$isSuffocating.set(nmsState, suffocating ? ALWAYS_TRUE : ALWAYS_FALSE);
CoreReflections.field$BlockStateBase$isSuffocating.set(nmsState, settings.isSuffocating().asBoolean() ? ALWAYS_TRUE : ALWAYS_FALSE);
CoreReflections.field$BlockStateBase$isViewBlocking.set(nmsState, settings.isViewBlocking() == Tristate.UNDEFINED ? settings.isSuffocating().asBoolean() ? ALWAYS_TRUE : ALWAYS_FALSE : (settings.isViewBlocking().asBoolean() ? ALWAYS_TRUE : ALWAYS_FALSE));
CoreReflections.field$BlockStateBase$isViewBlocking.set(nmsState,
settings.isViewBlocking() == Tristate.UNDEFINED ?
(suffocating ? ALWAYS_TRUE : ALWAYS_FALSE) :
(settings.isViewBlocking().asBoolean() ? ALWAYS_TRUE : ALWAYS_FALSE)
);
DelegatingBlock nmsBlock = (DelegatingBlock) BlockStateUtils.getBlockOwner(nmsState);
ObjectHolder<BlockShape> shapeHolder = nmsBlock.shapeDelegate();
@@ -291,9 +301,16 @@ public final class BukkitBlockManager extends AbstractBlockManager {
this.burnableBlocks.add(nmsBlock);
}
Key vanillaBlockId = state.vanillaBlockState().ownerId();
Key vanillaBlockId = state.visualBlockState().ownerId();
BlockGenerator.field$CraftEngineBlock$isNoteBlock().set(nmsBlock, vanillaBlockId.equals(BlockKeys.NOTE_BLOCK));
BlockGenerator.field$CraftEngineBlock$isTripwire().set(nmsBlock, vanillaBlockId.equals(BlockKeys.TRIPWIRE));
if (vanillaBlockId.equals(BlockKeys.BARRIER)) {
state.setRestoreBlockState(createBlockState("minecraft:glass"));
} else {
state.setRestoreBlockState(state.visualBlockState());
}
// 根据客户端的状态决定其是否阻挡视线
super.viewBlockingBlocks[state.customBlockState().registryId()] = canBlockView(state.visualBlockState());
} catch (ReflectiveOperationException e) {
this.plugin.logger().warn("Failed to apply platform block settings for block state " + state, e);
}
@@ -379,6 +396,23 @@ public final class BukkitBlockManager extends AbstractBlockManager {
}
}
public boolean canBlockView(BlockStateWrapper wrapper) {
Object blockState = wrapper.literalObject();
if (!BlockStateUtils.isOcclude(blockState)) {
return false;
}
return FastNMS.INSTANCE.method$BlockStateBase$isCollisionShapeFullBlock(blockState, CoreReflections.instance$EmptyBlockGetter$INSTANCE, BLOCK_POS$ZERO);
}
private void findViewBlockingVanillaBlocks() {
for (int i = 0; i < this.vanillaBlockStateCount; i++) {
BlockStateWrapper blockState = BlockRegistryMirror.byId(i);
if (canBlockView(blockState)) {
this.viewBlockingBlocks[i] = true;
}
}
}
@Override
protected void setVanillaBlockTags(Key id, List<String> tags) {
Object block = FastNMS.INSTANCE.method$Registry$getValue(MBuiltInRegistries.BLOCK, KeyUtils.toResourceLocation(id));

View File

@@ -20,7 +20,7 @@ public class BukkitCustomBlockStateWrapper extends AbstractBlockStateWrapper imp
@Override
public BlockStateWrapper visualBlockState() {
return getImmutableBlockState().map(ImmutableBlockState::vanillaBlockState).orElse(null);
return getImmutableBlockState().map(ImmutableBlockState::visualBlockState).orElse(null);
}
@Override

View File

@@ -180,7 +180,7 @@ public class BukkitBlockBehavior extends AbstractBlockBehavior {
public boolean isPathFindable(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
Optional<ImmutableBlockState> optionalCustomState = BlockStateUtils.getOptionalCustomBlockState(args[0]);
if (optionalCustomState.isEmpty()) return false;
BlockStateWrapper vanillaState = optionalCustomState.get().vanillaBlockState();
BlockStateWrapper vanillaState = optionalCustomState.get().visualBlockState();
if (vanillaState == null) return false;
return FastNMS.INSTANCE.method$BlockStateBase$isPathFindable(vanillaState.literalObject(), VersionHelper.isOrAbove1_20_5() ? null : args[1], VersionHelper.isOrAbove1_20_5() ? null : args[2], args[isPathFindable$type]);
}

View File

@@ -135,7 +135,7 @@ public class CropBlockBehavior extends BukkitBlockBehavior {
if (isMaxAge(state))
return InteractionResult.PASS;
boolean sendSwing = false;
Object visualState = state.vanillaBlockState().literalObject();
Object visualState = state.visualBlockState().literalObject();
Object visualStateBlock = BlockStateUtils.getBlockOwner(visualState);
if (CoreReflections.clazz$BonemealableBlock.isInstance(visualStateBlock)) {
boolean is = FastNMS.INSTANCE.method$BonemealableBlock$isValidBonemealTarget(visualStateBlock, context.getLevel().serverWorld(), LocationUtils.toBlockPos(context.getClickedPos()), visualState);
@@ -158,7 +158,7 @@ public class CropBlockBehavior extends BukkitBlockBehavior {
}
ImmutableBlockState customState = optionalCustomState.get();
boolean sendParticles = false;
Object visualState = customState.vanillaBlockState().literalObject();
Object visualState = customState.visualBlockState().literalObject();
Object visualStateBlock = BlockStateUtils.getBlockOwner(visualState);
if (CoreReflections.clazz$BonemealableBlock.isInstance(visualStateBlock)) {
boolean is = FastNMS.INSTANCE.method$BonemealableBlock$isValidBonemealTarget(visualStateBlock, level, pos, visualState);

View File

@@ -147,7 +147,7 @@ public class FenceGateBlockBehavior extends BukkitBlockBehavior implements IsPat
Player player = context.getPlayer();
if (player == null) return;
this.toggle(state, context.getLevel(), context.getClickedPos(), player);
if (!InteractUtils.isInteractable((org.bukkit.entity.Player) player.platformPlayer(), BlockStateUtils.fromBlockData(state.vanillaBlockState().literalObject()), context.getHitResult(), (Item<ItemStack>) context.getItem())) {
if (!InteractUtils.isInteractable((org.bukkit.entity.Player) player.platformPlayer(), BlockStateUtils.fromBlockData(state.visualBlockState().literalObject()), context.getHitResult(), (Item<ItemStack>) context.getItem())) {
player.swingHand(context.getHand());
}
}

View File

@@ -60,7 +60,7 @@ public class GrassBlockBehavior extends BukkitBlockBehavior {
}
boolean sendParticles = false;
ImmutableBlockState customState = optionalCustomState.get();
Object visualState = customState.vanillaBlockState().literalObject();
Object visualState = customState.visualBlockState().literalObject();
Object visualStateBlock = BlockStateUtils.getBlockOwner(visualState);
if (CoreReflections.clazz$BonemealableBlock.isInstance(visualStateBlock)) {
boolean is = FastNMS.INSTANCE.method$BonemealableBlock$isValidBonemealTarget(visualStateBlock, level, blockPos, visualState);
@@ -93,7 +93,7 @@ public class GrassBlockBehavior extends BukkitBlockBehavior {
if (!block.isEmpty())
return InteractionResult.PASS;
boolean sendSwing = false;
Object visualState = state.vanillaBlockState().literalObject();
Object visualState = state.visualBlockState().literalObject();
Object visualStateBlock = BlockStateUtils.getBlockOwner(visualState);
if (CoreReflections.clazz$BonemealableBlock.isInstance(visualStateBlock)) {
boolean is = FastNMS.INSTANCE.method$BonemealableBlock$isValidBonemealTarget(visualStateBlock, context.getLevel().serverWorld(), LocationUtils.toBlockPos(context.getClickedPos()), visualState);

View File

@@ -115,7 +115,7 @@ public class SaplingBlockBehavior extends BukkitBlockBehavior {
}
ImmutableBlockState customState = optionalCustomState.get();
boolean sendParticles = false;
Object visualState = customState.vanillaBlockState().literalObject();
Object visualState = customState.visualBlockState().literalObject();
Object visualStateBlock = BlockStateUtils.getBlockOwner(visualState);
if (CoreReflections.clazz$BonemealableBlock.isInstance(visualStateBlock)) {
boolean is = FastNMS.INSTANCE.method$BonemealableBlock$isValidBonemealTarget(visualStateBlock, level, blockPos, visualState);
@@ -153,7 +153,7 @@ public class SaplingBlockBehavior extends BukkitBlockBehavior {
if (ItemUtils.isEmpty(item) || !item.vanillaId().equals(ItemKeys.BONE_MEAL) || player == null || player.isAdventureMode())
return InteractionResult.PASS;
boolean sendSwing = false;
Object visualState = state.vanillaBlockState().literalObject();
Object visualState = state.visualBlockState().literalObject();
Object visualStateBlock = BlockStateUtils.getBlockOwner(visualState);
if (CoreReflections.clazz$BonemealableBlock.isInstance(visualStateBlock)) {
boolean is = FastNMS.INSTANCE.method$BonemealableBlock$isValidBonemealTarget(visualStateBlock, context.getLevel().serverWorld(), LocationUtils.toBlockPos(context.getClickedPos()), visualState);

View File

@@ -33,7 +33,7 @@ public class SeatBlockBehavior extends BukkitBlockBehavior implements EntityBloc
@Override
public BlockEntity createBlockEntity(BlockPos pos, ImmutableBlockState state) {
return new SeatBlockEntity(pos, state, this.seats);
return new SeatBlockEntity(pos, state, this.seats, this.directionProperty);
}
@Override

View File

@@ -120,7 +120,7 @@ public class TrapDoorBlockBehavior extends BukkitBlockBehavior implements IsPath
Player player = context.getPlayer();
if (player == null) return;
this.toggle(state, context.getLevel(), context.getClickedPos(), player);
if (!InteractUtils.isInteractable((org.bukkit.entity.Player) player.platformPlayer(), BlockStateUtils.fromBlockData(state.vanillaBlockState().literalObject()), context.getHitResult(), (Item<ItemStack>) context.getItem())) {
if (!InteractUtils.isInteractable((org.bukkit.entity.Player) player.platformPlayer(), BlockStateUtils.fromBlockData(state.visualBlockState().literalObject()), context.getHitResult(), (Item<ItemStack>) context.getItem())) {
player.swingHand(context.getHand());
}
}

View File

@@ -15,10 +15,12 @@ import net.momirealms.sparrow.nbt.CompoundTag;
public class SeatBlockEntity extends BlockEntity implements SeatOwner {
private final Seat<SeatBlockEntity>[] seats;
private final Property<HorizontalDirection> facing;
@SuppressWarnings("unchecked")
public SeatBlockEntity(BlockPos pos, ImmutableBlockState blockState, SeatConfig[] seats) {
public SeatBlockEntity(BlockPos pos, ImmutableBlockState blockState, SeatConfig[] seats, Property<HorizontalDirection> directionProperty) {
super(BukkitBlockEntityTypes.SEAT, pos, blockState);
this.facing = directionProperty;
this.seats = new Seat[seats.length];
for (int i = 0; i < seats.length; i++) {
this.seats[i] = new BukkitSeat<>(this, seats[i]);
@@ -38,10 +40,9 @@ public class SeatBlockEntity extends BlockEntity implements SeatOwner {
}
public boolean spawnSeat(Player player) {
Property<?> facing = super.blockState.owner().value().getProperty("facing");
int yRot = 0;
if (facing != null && facing.valueClass() == HorizontalDirection.class) {
HorizontalDirection direction = (HorizontalDirection) super.blockState.get(facing);
if (this.facing != null) {
HorizontalDirection direction = super.blockState.get(facing);
yRot = switch (direction) {
case NORTH -> 0;
case SOUTH -> 180;

View File

@@ -61,6 +61,7 @@ public class BukkitSeatManager implements SeatManager, Listener {
@Override
public void delayedInit() {
Bukkit.getPluginManager().registerEvents(this.dismountListener, this.plugin.javaPlugin());
Bukkit.getPluginManager().registerEvents(this, this.plugin.javaPlugin());
}
@Override

View File

@@ -101,7 +101,7 @@ public class AxeItemBehavior extends ItemBehavior {
// resend swing if it's not interactable on client side
if (!InteractUtils.isInteractable(
bukkitPlayer, BlockStateUtils.fromBlockData(customState.vanillaBlockState().literalObject()),
bukkitPlayer, BlockStateUtils.fromBlockData(customState.visualBlockState().literalObject()),
context.getHitResult(), item
) || player.isSecondaryUseActive()) {
player.swingHand(context.getHand());

View File

@@ -112,7 +112,7 @@ public class BlockItemBehavior extends BlockBoundItemBehavior {
} else {
ImmutableBlockState customState = optionalCustomState.get();
// custom block
if (!AdventureModeUtils.canPlace(context.getItem(), context.getLevel(), againstPos, Config.simplifyAdventurePlaceCheck() ? customState.vanillaBlockState().literalObject() : againstBlockState)) {
if (!AdventureModeUtils.canPlace(context.getItem(), context.getLevel(), againstPos, Config.simplifyAdventurePlaceCheck() ? customState.visualBlockState().literalObject() : againstBlockState)) {
return InteractionResult.FAIL;
}
}

View File

@@ -77,10 +77,10 @@ public class FlintAndSteelItemBehavior extends ItemBehavior {
// 点击对象为自定义方块
ImmutableBlockState immutableBlockState = BukkitBlockManager.instance().getImmutableBlockStateUnsafe(stateId);
// 原版外观也可燃
if (BlockStateUtils.isBurnable(immutableBlockState.vanillaBlockState().literalObject())) {
if (BlockStateUtils.isBurnable(immutableBlockState.visualBlockState().literalObject())) {
return InteractionResult.PASS;
}
BlockData vanillaBlockState = BlockStateUtils.fromBlockData(immutableBlockState.vanillaBlockState().literalObject());
BlockData vanillaBlockState = BlockStateUtils.fromBlockData(immutableBlockState.visualBlockState().literalObject());
// 点击的是方块上面则只需要判断shift和可交互
if (direction == Direction.UP) {
// 客户端层面必须可交互

View File

@@ -167,7 +167,7 @@ public class ItemEventListener implements Listener {
// fix client side issues
if (action.isRightClick() && hitResult != null &&
InteractUtils.canPlaceVisualBlock(player, BlockStateUtils.fromBlockData(immutableBlockState.vanillaBlockState().literalObject()), hitResult, itemInHand)) {
InteractUtils.canPlaceVisualBlock(player, BlockStateUtils.fromBlockData(immutableBlockState.visualBlockState().literalObject()), hitResult, itemInHand)) {
player.updateInventory();
}
@@ -272,13 +272,13 @@ public class ItemEventListener implements Listener {
if (immutableBlockState != null) {
// client won't have sounds if the clientside block is interactable
// so we should check and resend sounds on BlockPlaceEvent
BlockData craftBlockData = BlockStateUtils.fromBlockData(immutableBlockState.vanillaBlockState().literalObject());
BlockData craftBlockData = BlockStateUtils.fromBlockData(immutableBlockState.visualBlockState().literalObject());
if (InteractUtils.isInteractable(player, craftBlockData, hitResult, itemInHand)) {
if (!serverPlayer.isSecondaryUseActive()) {
serverPlayer.setResendSound();
}
} else {
if (BlockStateUtils.isReplaceable(immutableBlockState.customBlockState().literalObject()) && !BlockStateUtils.isReplaceable(immutableBlockState.vanillaBlockState().literalObject())) {
if (BlockStateUtils.isReplaceable(immutableBlockState.customBlockState().literalObject()) && !BlockStateUtils.isReplaceable(immutableBlockState.visualBlockState().literalObject())) {
serverPlayer.setResendSwing();
}
}

View File

@@ -78,7 +78,7 @@ public class DebugCleanCacheCommand extends BukkitCommandFeature<CommandSender>
Set<BlockStateWrapper> ids = new HashSet<>();
for (CustomBlock customBlock : instance.loadedBlocks().values()) {
for (ImmutableBlockState state : customBlock.variantProvider().states()) {
ids.add(state.vanillaBlockState());
ids.add(state.visualBlockState());
}
}
VisualBlockStateAllocator visualBlockStateAllocator = instance.blockParser().visualBlockStateAllocator();

View File

@@ -283,10 +283,10 @@ public final class WorldStorageInjector {
if (Config.enableLightSystem()) {
if (previousImmutableBlockState.isEmpty()) {
// 原版块到自定义块,只需要判断新块是否和客户端视觉一致
updateLight(holder, newImmutableBlockState.vanillaBlockState().literalObject(), newState, x, y, z);
updateLight(holder, newImmutableBlockState.visualBlockState().literalObject(), newState, x, y, z);
} else {
// 自定义块到自定义块
updateLight$complex(holder, newImmutableBlockState.vanillaBlockState().literalObject(), newState, previousState, x, y, z);
updateLight$complex(holder, newImmutableBlockState.visualBlockState().literalObject(), newState, previousState, x, y, z);
}
}
} else {
@@ -311,7 +311,7 @@ public final class WorldStorageInjector {
}
if (Config.enableLightSystem()) {
// 自定义块到原版块,只需要判断旧块是否和客户端一直
BlockStateWrapper wrapper = previous.vanillaBlockState();
BlockStateWrapper wrapper = previous.visualBlockState();
if (wrapper != null) {
updateLight(holder, wrapper.literalObject(), previousState, x, y, z);
}

View File

@@ -23,6 +23,7 @@ import net.kyori.adventure.nbt.api.BinaryTagHolder;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.DataComponentValue;
import net.kyori.adventure.text.event.HoverEvent;
import net.momirealms.craftengine.bukkit.api.BukkitAdaptors;
import net.momirealms.craftengine.bukkit.api.CraftEngineBlocks;
import net.momirealms.craftengine.bukkit.api.CraftEngineFurniture;
import net.momirealms.craftengine.bukkit.api.event.FurnitureAttemptBreakEvent;
@@ -91,9 +92,12 @@ import net.momirealms.craftengine.core.plugin.text.component.ComponentProvider;
import net.momirealms.craftengine.core.util.*;
import net.momirealms.craftengine.core.world.*;
import net.momirealms.craftengine.core.world.chunk.CEChunk;
import net.momirealms.craftengine.core.world.chunk.ChunkStatus;
import net.momirealms.craftengine.core.world.chunk.Palette;
import net.momirealms.craftengine.core.world.chunk.PalettedContainer;
import net.momirealms.craftengine.core.world.chunk.client.ClientChunk;
import net.momirealms.craftengine.core.world.chunk.client.ClientSection;
import net.momirealms.craftengine.core.world.chunk.client.PackedOcclusionStorage;
import net.momirealms.craftengine.core.world.chunk.client.SingularOcclusionStorage;
import net.momirealms.craftengine.core.world.chunk.packet.BlockEntityData;
import net.momirealms.craftengine.core.world.chunk.packet.MCSection;
import net.momirealms.sparrow.nbt.CompoundTag;
@@ -127,6 +131,7 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.BiFunction;
import java.util.function.Predicate;
public class BukkitNetworkManager implements NetworkManager, Listener, PluginMessageListener {
private static BukkitNetworkManager instance;
@@ -290,7 +295,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
}
}
public void registerBlockStatePacketListeners(int[] blockStateMappings) {
public void registerBlockStatePacketListeners(int[] blockStateMappings, Predicate<Integer> occlusionPredicate) {
int stoneId = BlockStateUtils.blockStateToId(MBlocks.STONE$defaultState);
int vanillaBlocks = BlockStateUtils.vanillaBlockStateCount();
int[] newMappings = new int[blockStateMappings.length];
@@ -320,10 +325,11 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
newMappings,
newMappingsMOD,
newMappings.length,
RegistryUtils.currentBiomeRegistrySize()
RegistryUtils.currentBiomeRegistrySize(),
occlusionPredicate
), this.packetIds.clientboundLevelChunkWithLightPacket(), "ClientboundLevelChunkWithLightPacket");
registerS2CGamePacketListener(new SectionBlockUpdateListener(newMappings, newMappingsMOD), this.packetIds.clientboundSectionBlocksUpdatePacket(), "ClientboundSectionBlocksUpdatePacket");
registerS2CGamePacketListener(new BlockUpdateListener(newMappings, newMappingsMOD), this.packetIds.clientboundBlockUpdatePacket(), "ClientboundBlockUpdatePacket");
registerS2CGamePacketListener(new SectionBlockUpdateListener(newMappings, newMappingsMOD, occlusionPredicate), this.packetIds.clientboundSectionBlocksUpdatePacket(), "ClientboundSectionBlocksUpdatePacket");
registerS2CGamePacketListener(new BlockUpdateListener(newMappings, newMappingsMOD, occlusionPredicate), this.packetIds.clientboundBlockUpdatePacket(), "ClientboundBlockUpdatePacket");
registerS2CGamePacketListener(
VersionHelper.isOrAbove1_21_4() ?
new LevelParticleListener1_21_4(newMappings, newMappingsMOD) :
@@ -1098,7 +1104,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
if (player.isAdventureMode()) {
if (Config.simplifyAdventureBreakCheck()) {
ImmutableBlockState state = BukkitBlockManager.instance().getImmutableBlockStateUnsafe(stateId);
if (!player.canBreak(pos, state.vanillaBlockState().literalObject())) {
if (!player.canBreak(pos, state.visualBlockState().literalObject())) {
player.preventMiningBlock();
return;
}
@@ -1360,7 +1366,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
Key itemId = state.settings().itemId();
// no item available
if (itemId == null) return;
Object vanillaBlock = FastNMS.INSTANCE.method$BlockState$getBlock(state.vanillaBlockState().literalObject());
Object vanillaBlock = FastNMS.INSTANCE.method$BlockState$getBlock(state.visualBlockState().literalObject());
Object vanillaBlockItem = FastNMS.INSTANCE.method$Block$asItem(vanillaBlock);
if (vanillaBlockItem == null) return;
Key addItemId = KeyUtils.namespacedKey2Key(item.getType().getKey());
@@ -1435,9 +1441,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
Object location = FastNMS.INSTANCE.field$ResourceKey$location(dimensionKey);
World world = Bukkit.getWorld(Objects.requireNonNull(NamespacedKey.fromString(location.toString())));
if (world != null) {
int sectionCount = (world.getMaxHeight() - world.getMinHeight()) / 16;
player.setClientSideSectionCount(sectionCount);
player.setClientSideDimension(Key.of(location.toString()));
player.setClientSideWorld(BukkitAdaptors.adapt(world));
} else {
CraftEngine.instance().logger().warn("Failed to handle ClientboundLoginPacket: World " + location + " does not exist");
}
@@ -1465,10 +1469,9 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
Object location = FastNMS.INSTANCE.field$ResourceKey$location(dimensionKey);
World world = Bukkit.getWorld(Objects.requireNonNull(NamespacedKey.fromString(location.toString())));
if (world != null) {
int sectionCount = (world.getMaxHeight() - world.getMinHeight()) / 16;
player.setClientSideSectionCount(sectionCount);
player.setClientSideDimension(Key.of(location.toString()));
player.setClientSideWorld(BukkitAdaptors.adapt(world));
player.clearTrackedChunks();
player.clearTrackedBlockEntities();
} else {
CraftEngine.instance().logger().warn("Failed to handle ClientboundRespawnPacket: World " + location + " does not exist");
}
@@ -1990,17 +1993,15 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
private final IntIdentityList biomeList;
private final IntIdentityList blockList;
private final boolean needsDowngrade;
private final Predicate<Integer> occlusionPredicate;
public LevelChunkWithLightListener(int[] blockStateMapper, int[] modBlockStateMapper, int blockRegistrySize, int biomeRegistrySize) {
public LevelChunkWithLightListener(int[] blockStateMapper, int[] modBlockStateMapper, int blockRegistrySize, int biomeRegistrySize, Predicate<Integer> occlusionPredicate) {
this.blockStateMapper = blockStateMapper;
this.modBlockStateMapper = modBlockStateMapper;
this.biomeList = new IntIdentityList(biomeRegistrySize);
this.blockList = new IntIdentityList(blockRegistrySize);
this.needsDowngrade = MiscUtils.ceilLog2(BlockStateUtils.vanillaBlockStateCount()) != MiscUtils.ceilLog2(blockRegistrySize);
}
public int remapBlockState(int stateId, boolean enableMod) {
return enableMod ? this.modBlockStateMapper[stateId] : this.blockStateMapper[stateId];
this.occlusionPredicate = occlusionPredicate;
}
@Override
@@ -2012,6 +2013,8 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ);
boolean named = !VersionHelper.isOrAbove1_20_2();
int[] remapper = user.clientModEnabled() ? this.modBlockStateMapper : this.blockStateMapper;
// 读取区块数据
int heightmapsCount = 0;
Map<Integer, long[]> heightmapsMap = null;
@@ -2033,36 +2036,94 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
buf.readBytes(chunkDataBytes);
// 客户端侧section数量很重要不能读取此时玩家所在的真实世界包具有滞后性
int count = player.clientSideSectionCount();
net.momirealms.craftengine.core.world.World clientSideWorld = player.clientSideWorld();
WorldHeight worldHeight = clientSideWorld.worldHeight();
int count = worldHeight.getSectionsCount();
MCSection[] sections = new MCSection[count];
FriendlyByteBuf chunkDataByteBuf = new FriendlyByteBuf(Unpooled.wrappedBuffer(chunkDataBytes));
boolean hasChangedAnyBlock = false;
boolean hasGlobalPalette = false;
// 创建客户端侧世界(只在开启实体情况下创建)
ClientSection[] clientSections = Config.enableEntityCulling() ? new ClientSection[count] : null;
for (int i = 0; i < count; i++) {
MCSection mcSection = new MCSection(user.clientBlockList(), this.blockList, this.biomeList);
mcSection.readPacket(chunkDataByteBuf);
PalettedContainer<Integer> container = mcSection.blockStateContainer();
// 重定向生物群系
if (remapBiomes(user, mcSection.biomeContainer())) {
hasChangedAnyBlock = true;
}
Palette<Integer> palette = container.data().palette();
if (palette.canRemap()) {
if (palette.remapAndCheck(s -> remapBlockState(s, user.clientModEnabled()))) {
// 重定向方块
if (palette.remapAndCheck(s -> remapper[s])) {
hasChangedAnyBlock = true;
}
// 处理客户端侧哪些方块有阻挡
if (clientSections != null) {
int size = palette.getSize();
// 单个元素的情况下,使用优化的存储方案
if (size == 1) {
clientSections[i] = new ClientSection(new SingularOcclusionStorage(this.occlusionPredicate.test(palette.get(0))));
} else {
boolean hasOcclusions = false;
boolean hasNoOcclusions = false;
for (int h = 0; h < size; h++) {
int entry = palette.get(h);
if (this.occlusionPredicate.test(entry)) {
hasOcclusions = true;
} else {
hasNoOcclusions = true;
}
}
// 两种情况都有,那么需要一个个遍历处理视线遮挡数据
if (hasOcclusions && hasNoOcclusions) {
PackedOcclusionStorage storage = new PackedOcclusionStorage(false);
clientSections[i] = new ClientSection(storage);
for (int j = 0; j < 4096; j++) {
int state = container.get(j);
storage.set(j, this.occlusionPredicate.test(state));
}
}
// 全遮蔽或全透视则使用优化存储方案
else {
clientSections[i] = new ClientSection(new SingularOcclusionStorage(hasOcclusions));
}
}
}
} else {
hasGlobalPalette = true;
PackedOcclusionStorage storage = null;
if (clientSections != null) {
storage = new PackedOcclusionStorage(false);
clientSections[i] = new ClientSection(storage);
}
for (int j = 0; j < 4096; j++) {
int state = container.get(j);
int newState = remapBlockState(state, user.clientModEnabled());
// 重定向方块
int newState = remapper[state];
if (newState != state) {
container.set(j, newState);
hasChangedAnyBlock = true;
}
// 写入视线遮挡数据
if (storage != null) {
storage.set(j, this.occlusionPredicate.test(state));
}
}
}
sections[i] = mcSection;
}
@@ -2127,13 +2188,17 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
}
// 记录加载的区块
player.addTrackedChunk(chunkPos.longKey, new ChunkStatus());
player.addTrackedChunk(chunkPos.longKey, new ClientChunk(clientSections, worldHeight));
// 生成方块实体
CEWorld ceWorld = BukkitWorldManager.instance().getWorld(player.world().uuid());
CEChunk ceChunk = ceWorld.getChunkAtIfLoaded(chunkPos.longKey);
if (ceChunk != null) {
ceChunk.spawnBlockEntities(player);
CEWorld ceWorld = clientSideWorld.storageWorld();
// 世界可能被卸载,因为包滞后
if (ceWorld != null) {
CEChunk ceChunk = ceWorld.getChunkAtIfLoaded(chunkPos.longKey);
if (ceChunk != null) {
// 生成方块实体
ceChunk.spawnBlockEntities(player);
}
}
}
}
@@ -2141,63 +2206,64 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
public static class SectionBlockUpdateListener implements ByteBufferPacketListener {
private final int[] blockStateMapper;
private final int[] modBlockStateMapper;
private final Predicate<Integer> occlusionPredicate;
public SectionBlockUpdateListener(int[] blockStateMapper, int[] modBlockStateMapper) {
public SectionBlockUpdateListener(int[] blockStateMapper, int[] modBlockStateMapper, Predicate<Integer> occlusionPredicate) {
this.blockStateMapper = blockStateMapper;
this.modBlockStateMapper = modBlockStateMapper;
this.occlusionPredicate = occlusionPredicate;
}
@Override
public void onPacketSend(NetWorkUser user, ByteBufPacketEvent event) {
if (user.clientModEnabled()) {
FriendlyByteBuf buf = event.getBuffer();
long pos = buf.readLong();
int blocks = buf.readVarInt();
short[] positions = new short[blocks];
int[] states = new int[blocks];
for (int i = 0; i < blocks; i++) {
long k = buf.readVarLong();
positions[i] = (short) ((int) (k & 4095L));
states[i] = modBlockStateMapper[((int) (k >>> 12))];
}
buf.clear();
buf.writeVarInt(event.packetID());
buf.writeLong(pos);
buf.writeVarInt(blocks);
for (int i = 0; i < blocks; i++) {
buf.writeVarLong((long) states[i] << 12 | positions[i]);
}
event.setChanged(true);
} else {
FriendlyByteBuf buf = event.getBuffer();
long pos = buf.readLong();
int blocks = buf.readVarInt();
short[] positions = new short[blocks];
int[] states = new int[blocks];
for (int i = 0; i < blocks; i++) {
long k = buf.readVarLong();
positions[i] = (short) ((int) (k & 4095L));
states[i] = blockStateMapper[((int) (k >>> 12))];
}
buf.clear();
buf.writeVarInt(event.packetID());
buf.writeLong(pos);
buf.writeVarInt(blocks);
for (int i = 0; i < blocks; i++) {
buf.writeVarLong((long) states[i] << 12 | positions[i]);
}
event.setChanged(true);
int[] remapper = user.clientModEnabled() ? this.modBlockStateMapper : this.blockStateMapper;
FriendlyByteBuf buf = event.getBuffer();
long sPos = buf.readLong();
int blocks = buf.readVarInt();
short[] positions = new short[blocks];
int[] states = new int[blocks];
// 获取客户端侧区域
ClientSection clientSection = null;
if (Config.enableEntityCulling()) {
SectionPos sectionPos = SectionPos.of(sPos);
ClientChunk trackedChunk = user.getTrackedChunk(sectionPos.asChunkPos().longKey);
clientSection = trackedChunk.sectionById(sectionPos.y);
}
for (int i = 0; i < blocks; i++) {
long k = buf.readVarLong();
short posIndex = (short) ((int) (k & 4095L));
positions[i] = posIndex;
int beforeState = ((int) (k >>> 12));
states[i] = remapper[beforeState];
if (clientSection != null) {
// 设置遮蔽状态
BlockPos pos = SectionPos.unpackSectionRelativePos(posIndex);
clientSection.setOccluding(pos.x, pos.y, pos.z, this.occlusionPredicate.test(beforeState));
}
}
buf.clear();
buf.writeVarInt(event.packetID());
buf.writeLong(sPos);
buf.writeVarInt(blocks);
for (int i = 0; i < blocks; i++) {
buf.writeVarLong((long) states[i] << 12 | positions[i]);
}
event.setChanged(true);
}
}
public static class BlockUpdateListener implements ByteBufferPacketListener {
private final int[] blockStateMapper;
private final int[] modBlockStateMapper;
private final Predicate<Integer> occlusionPredicate;
public BlockUpdateListener(int[] blockStateMapper, int[] modBlockStateMapper) {
public BlockUpdateListener(int[] blockStateMapper, int[] modBlockStateMapper, Predicate<Integer> occlusionPredicate) {
this.blockStateMapper = blockStateMapper;
this.modBlockStateMapper = modBlockStateMapper;
this.occlusionPredicate = occlusionPredicate;
}
@Override
@@ -2205,6 +2271,12 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
FriendlyByteBuf buf = event.getBuffer();
BlockPos pos = buf.readBlockPos();
int before = buf.readVarInt();
if (Config.enableEntityCulling()) {
ClientChunk trackedChunk = user.getTrackedChunk(ChunkPos.asLong(pos.x >> 4, pos.z >> 4));
if (trackedChunk != null) {
trackedChunk.setOccluding(pos.x, pos.y, pos.z, this.occlusionPredicate.test(before));
}
}
if (user.clientModEnabled() && !BlockStateUtils.isVanillaBlock(before)) {
return;
}
@@ -2378,6 +2450,13 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
if (eventId != WorldEvents.BLOCK_BREAK_EFFECT) return;
BlockPos blockPos = buf.readBlockPos();
int state = buf.readInt();
// 移除不透明设置
if (Config.enableEntityCulling()) {
ClientChunk trackedChunk = user.getTrackedChunk(ChunkPos.asLong(blockPos.x >> 4, blockPos.z >> 4));
if (trackedChunk != null) {
trackedChunk.setOccluding(blockPos.x, blockPos.y, blockPos.z, false);
}
}
boolean global = buf.readBoolean();
int newState = user.clientModEnabled() ? modBlockStateMapper[state] : blockStateMapper[state];
Object blockState = BlockStateUtils.idToBlockState(state);

View File

@@ -103,7 +103,7 @@ public record VisualBlockStatePacket(int[] data) implements ModPacket {
for (int i = 0; i < serverSideBlockCount; i++) {
ImmutableBlockState state = BukkitBlockManager.instance().getImmutableBlockStateUnsafe(i + vanillaBlockStateCount);
if (state.isEmpty()) continue;
mappings[state.customBlockState().registryId() - vanillaBlockStateCount] = state.vanillaBlockState().registryId();
mappings[state.customBlockState().registryId() - vanillaBlockStateCount] = state.visualBlockState().registryId();
}
return new VisualBlockStatePacket(mappings);
}

View File

@@ -4593,4 +4593,26 @@ public final class CoreReflections {
"world.level.levelgen.feature.Feature"
)
);
public static final Class<?> clazz$EmptyBlockGetter = requireNonNull(
BukkitReflectionUtils.findReobfOrMojmapClass(
"world.level.BlockAccessAir",
"world.level.EmptyBlockGetter"
)
);
public static final Method method$EmptyBlockGetter$values = requireNonNull(
ReflectionUtils.getStaticMethod(clazz$EmptyBlockGetter, clazz$EmptyBlockGetter.arrayType())
);
public static final Object instance$EmptyBlockGetter$INSTANCE;
static {
try {
Object[] values = (Object[]) method$EmptyBlockGetter$values.invoke(null);
instance$EmptyBlockGetter$INSTANCE = values[0];
} catch (ReflectiveOperationException e) {
throw new ReflectionInitException("Failed to init EmptyBlockGetter$INSTANCE", e);
}
}
}

View File

@@ -23,6 +23,7 @@ import net.momirealms.craftengine.core.advancement.AdvancementType;
import net.momirealms.craftengine.core.block.BlockStateWrapper;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.entity.BlockEntity;
import net.momirealms.craftengine.core.block.entity.render.ConstantBlockEntityRenderer;
import net.momirealms.craftengine.core.entity.data.EntityData;
import net.momirealms.craftengine.core.entity.player.GameMode;
import net.momirealms.craftengine.core.entity.player.InteractionHand;
@@ -31,6 +32,7 @@ import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.context.CooldownData;
import net.momirealms.craftengine.core.plugin.entityculling.EntityCulling;
import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
import net.momirealms.craftengine.core.plugin.network.ConnectionState;
import net.momirealms.craftengine.core.plugin.network.EntityPacketHandler;
@@ -39,7 +41,8 @@ import net.momirealms.craftengine.core.sound.SoundSource;
import net.momirealms.craftengine.core.util.*;
import net.momirealms.craftengine.core.world.*;
import net.momirealms.craftengine.core.world.World;
import net.momirealms.craftengine.core.world.chunk.ChunkStatus;
import net.momirealms.craftengine.core.world.chunk.client.ClientChunk;
import net.momirealms.craftengine.core.world.chunk.client.VirtualCullableObject;
import net.momirealms.craftengine.core.world.collision.AABB;
import org.bukkit.*;
import org.bukkit.attribute.Attribute;
@@ -85,8 +88,7 @@ public class BukkitServerPlayer extends Player {
private Reference<org.bukkit.entity.Player> playerRef;
private Reference<Object> serverPlayerRef;
// client side dimension info
private int sectionCount;
private Key clientSideDimension;
private World clientSideWorld;
// check main hand/offhand interaction
private int lastSuccessfulInteraction;
// to prevent duplicated events
@@ -124,7 +126,7 @@ public class BukkitServerPlayer extends Player {
// cooldown data
private CooldownData cooldownData;
// tracked chunks
private ConcurrentLong2ReferenceChainedHashTable<ChunkStatus> trackedChunks;
private ConcurrentLong2ReferenceChainedHashTable<ClientChunk> trackedChunks;
// entity view
private Map<Integer, EntityPacketHandler> entityTypeView;
// 通过指令或api设定的语言
@@ -138,6 +140,10 @@ public class BukkitServerPlayer extends Player {
private boolean isHackedBreak;
// 上一次停止挖掘包发出的时间
private int lastStopMiningTick;
// 跟踪到的方块实体渲染器
private final Map<BlockPos, VirtualCullableObject> trackedBlockEntityRenderers = new ConcurrentHashMap<>();
private final EntityCulling culling;
public BukkitServerPlayer(BukkitCraftEngine plugin, @Nullable Channel channel) {
this.channel = channel;
@@ -151,6 +157,7 @@ public class BukkitServerPlayer extends Player {
}
}
}
this.culling = new EntityCulling(this, 64, 0.5);
}
public void setPlayer(org.bukkit.entity.Player player) {
@@ -477,21 +484,13 @@ public class BukkitServerPlayer extends Player {
}
@Override
public int clientSideSectionCount() {
return sectionCount;
}
public void setClientSideSectionCount(int sectionCount) {
this.sectionCount = sectionCount;
public World clientSideWorld() {
return this.clientSideWorld;
}
@Override
public Key clientSideDimension() {
return clientSideDimension;
}
public void setClientSideDimension(Key clientSideDimension) {
this.clientSideDimension = clientSideDimension;
public void setClientSideWorld(World world) {
this.clientSideWorld = world;
}
public void setConnectionState(ConnectionState connectionState) {
@@ -564,6 +563,15 @@ public class BukkitServerPlayer extends Player {
this.predictNextBlockToMine();
}
}
if (Config.enableEntityCulling()) {
long nano1 = System.nanoTime();
for (VirtualCullableObject cullableObject : this.trackedBlockEntityRenderers.values()) {
boolean visible = this.culling.isVisible(cullableObject.cullable.aabb(), LocationUtils.toVec3d(platformPlayer().getEyeLocation()));
cullableObject.setShown(this, visible);
}
long nano2 = System.nanoTime();
//CraftEngine.instance().logger().info("EntityCulling took " + (nano2 - nano1) / 1_000_000d + "ms");
}
}
private void updateGUI() {
@@ -646,7 +654,7 @@ public class BukkitServerPlayer extends Player {
// instant break
boolean custom = immutableBlockState != null;
if (custom && getDestroyProgress(state, pos) >= 1f) {
BlockStateWrapper vanillaBlockState = immutableBlockState.vanillaBlockState();
BlockStateWrapper vanillaBlockState = immutableBlockState.visualBlockState();
// if it's not an instant break on client side, we should resend level event
if (vanillaBlockState != null && getDestroyProgress(vanillaBlockState.literalObject(), pos) < 1f) {
Object levelEventPacket = FastNMS.INSTANCE.constructor$ClientboundLevelEventPacket(
@@ -811,7 +819,7 @@ public class BukkitServerPlayer extends Player {
// for simplified adventure break, switch mayBuild temporarily
if (isAdventureMode() && Config.simplifyAdventureBreakCheck()) {
// check the appearance state
if (canBreak(hitPos, customState.vanillaBlockState().literalObject())) {
if (canBreak(hitPos, customState.visualBlockState().literalObject())) {
// Error might occur so we use try here
try {
FastNMS.INSTANCE.field$Player$mayBuild(serverPlayer, true);
@@ -1180,12 +1188,12 @@ public class BukkitServerPlayer extends Player {
}
@Override
public ChunkStatus getTrackedChunk(long chunkPos) {
public ClientChunk getTrackedChunk(long chunkPos) {
return this.trackedChunks.get(chunkPos);
}
@Override
public void addTrackedChunk(long chunkPos, ChunkStatus chunkStatus) {
public void addTrackedChunk(long chunkPos, ClientChunk chunkStatus) {
this.trackedChunks.put(chunkPos, chunkStatus);
}
@@ -1300,4 +1308,36 @@ public class BukkitServerPlayer extends Player {
public void sendTotemAnimation(Item<?> totem, @Nullable SoundData sound, boolean silent) {
PlayerUtils.sendTotemAnimation(this, totem, sound, silent);
}
@Override
public void addTrackedBlockEntities(Map<BlockPos, ConstantBlockEntityRenderer> renders) {
for (Map.Entry<BlockPos, ConstantBlockEntityRenderer> entry : renders.entrySet()) {
this.trackedBlockEntityRenderers.put(entry.getKey(), new VirtualCullableObject(entry.getValue()));
}
}
@Override
public void addTrackedBlockEntity(BlockPos blockPos, ConstantBlockEntityRenderer renderer) {
this.trackedBlockEntityRenderers.put(blockPos, new VirtualCullableObject(renderer));
}
@Override
public VirtualCullableObject getTrackedBlockEntity(BlockPos blockPos) {
return this.trackedBlockEntityRenderers.get(blockPos);
}
@Override
public void removeTrackedBlockEntities(Collection<BlockPos> renders) {
for (BlockPos render : renders) {
VirtualCullableObject remove = this.trackedBlockEntityRenderers.remove(render);
if (remove != null && remove.isShown()) {
remove.cullable().hide(this);
}
}
}
@Override
public void clearTrackedBlockEntities() {
this.trackedBlockEntityRenderers.clear();
}
}

View File

@@ -60,7 +60,7 @@ public final class EntityUtils {
Object serverLevel = BukkitAdaptors.adapt(player.getWorld()).serverWorld();
Object serverPlayer = FastNMS.INSTANCE.method$CraftPlayer$getHandle(player);
for (Object pose : List.of(CoreReflections.instance$Pose$STANDING, CoreReflections.instance$Pose$CROUCHING, CoreReflections.instance$Pose$SWIMMING)) {
BlockPos pos = new BlockPos(MiscUtils.fastFloor(x), MiscUtils.fastFloor(y), MiscUtils.fastFloor(z));
BlockPos pos = new BlockPos(MiscUtils.floor(x), MiscUtils.floor(y), MiscUtils.floor(z));
try {
double floorHeight = (double) CoreReflections.method$BlockGetter$getBlockFloorHeight.invoke(serverLevel, LocationUtils.toBlockPos(pos));
if (pos.y() + floorHeight > y + 0.75 || !isBlockFloorValid(floorHeight)) {

View File

@@ -6,6 +6,7 @@ import net.momirealms.craftengine.bukkit.plugin.injector.WorldStorageInjector;
import net.momirealms.craftengine.bukkit.plugin.reflection.minecraft.CoreReflections;
import net.momirealms.craftengine.bukkit.util.BlockStateUtils;
import net.momirealms.craftengine.bukkit.util.LocationUtils;
import net.momirealms.craftengine.core.block.BlockStateWrapper;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
@@ -296,9 +297,12 @@ public class BukkitWorldManager implements WorldManager, Listener {
for (int z = 0; z < 16; z++) {
for (int y = 0; y < 16; y++) {
ImmutableBlockState customState = ceSection.getBlockState(x, y, z);
if (!customState.isEmpty() && customState.vanillaBlockState() != null) {
FastNMS.INSTANCE.method$LevelChunkSection$setBlockState(section, x, y, z, customState.vanillaBlockState().literalObject(), false);
unsaved = true;
if (!customState.isEmpty()) {
BlockStateWrapper wrapper = customState.restoreBlockState();
if (wrapper != null) {
FastNMS.INSTANCE.method$LevelChunkSection$setBlockState(section, x, y, z, wrapper.literalObject(), false);
unsaved = true;
}
}
}
}

View File

@@ -552,11 +552,10 @@ chunk-system:
remove: []
convert: {}
#client-optimization:
# # Using server-side ray tracing algorithms to hide certain entities and reduce client-side rendering pressure.
# entity-culling:
# enable: false
# whitelist-entities: []
client-optimization:
# Using server-side ray tracing algorithms to hide certain entities and reduce client-side rendering pressure.
entity-culling:
enable: false
# Enables or disables debug mode
debug:

View File

@@ -178,7 +178,7 @@ templates:
# template: default:loot_table/ore
# arguments:
# ore_block: the ore block
# ore_drop: the drops of the ore
# ore_drop: the drops of the ore material
# ore_drop_count: the amount of the ore materials
# exp: the exp to drop
default:loot_table/ore:

View File

@@ -87,6 +87,7 @@ warning.config.type.quaternionf: "<yellow>Issue found in file <arg:0> - Failed t
warning.config.type.vector3f: "<yellow>Issue found in file <arg:0> - Failed to load '<arg:1>': Cannot cast '<arg:2>' to Vector3f type for option '<arg:3>'.</yellow>"
warning.config.type.vec3d: "<yellow>Issue found in file <arg:0> - Failed to load '<arg:1>': Cannot cast '<arg:2>' to Vec3d type for option '<arg:3>'.</yellow>"
warning.config.type.map: "<yellow>Issue found in file <arg:0> - Failed to load '<arg:1>': Cannot cast '<arg:2>' to Map type for option '<arg:3>'.</yellow>"
warning.config.type.aabb: "<yellow>Issue found in file <arg:0> - Failed to load '<arg:1>': Cannot cast '<arg:2>' to AABB type for option '<arg:3>'.</yellow>"
warning.config.type.snbt.invalid_syntax: "<yellow>Issue found in file <arg:0> - Failed to load '<arg:1>': Invalid snbt syntax '<arg:2>'.</yellow>"
warning.config.number.missing_type: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is missing the required 'type' argument for number argument.</yellow>"
warning.config.number.invalid_type: "<yellow>Issue found in file <arg:0> - The config '<arg:1>' is using an invalid number argument type '<arg:2>'.</yellow>"

View File

@@ -85,6 +85,8 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
// 临时存储哪些视觉方块被使用了
protected final Set<BlockStateWrapper> tempVisualBlockStatesInUse = new HashSet<>();
protected final Set<Key> tempVisualBlocksInUse = new HashSet<>();
// 能遮挡视线的方块
protected final boolean[] viewBlockingBlocks;
// 声音映射表,和使用了哪些视觉方块有关
protected Map<Key, Key> soundReplacements = Map.of();
// 是否使用了透明方块模型
@@ -102,6 +104,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
this.tempVanillaBlockStateModels = new JsonElement[vanillaBlockStateCount];
this.blockParser = new BlockParser(this.autoVisualBlockStateCandidates);
this.blockStateMappingParser = new BlockStateMappingParser();
this.viewBlockingBlocks = new boolean[vanillaBlockStateCount + customBlockCount];
Arrays.fill(this.blockStateMappings, -1);
}
@@ -232,6 +235,10 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
public abstract BlockBehavior createBlockBehavior(CustomBlock customBlock, List<Map<String, Object>> behaviorConfig);
public boolean isViewBlockingBlock(int stateId) {
return this.viewBlockingBlocks[stateId];
}
protected abstract void updateTags();
protected abstract boolean isVanillaBlock(Key id);
@@ -593,7 +600,11 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
this.arrangeModelForStateAndVerify(visualBlockState, parseBlockModel(modelConfig));
}
}
BlockStateAppearance blockStateAppearance = new BlockStateAppearance(visualBlockState, parseBlockEntityRender(appearanceSection.get("entity-renderer")));
BlockStateAppearance blockStateAppearance = new BlockStateAppearance(
visualBlockState,
parseBlockEntityRender(appearanceSection.get("entity-renderer")),
ResourceConfigUtils.getAsAABB(appearanceSection.getOrDefault("aabb", 1), "aabb")
);
appearances.put(appearanceName, blockStateAppearance);
if (anyAppearance == null) {
anyAppearance = blockStateAppearance;
@@ -631,7 +642,8 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
continue;
}
for (ImmutableBlockState possibleState : possibleStates) {
possibleState.setVanillaBlockState(appearance.blockState());
possibleState.setVisualBlockState(appearance.blockState());
possibleState.setEstimatedBoundingBox(appearance.estimateAABB());
appearance.blockEntityRenderer().ifPresent(possibleState::setConstantRenderers);
}
}
@@ -650,11 +662,12 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
}
state.setBehavior(blockBehavior);
int internalId = state.customBlockState().registryId();
BlockStateWrapper visualState = state.vanillaBlockState();
BlockStateWrapper visualState = state.visualBlockState();
// 校验,为未绑定外观的强行添加外观
if (visualState == null) {
visualState = anyAppearance.blockState();
state.setVanillaBlockState(visualState);
state.setVisualBlockState(visualState);
state.setEstimatedBoundingBox(anyAppearance.estimateAABB());
anyAppearance.blockEntityRenderer().ifPresent(state::setConstantRenderers);
}
int appearanceId = visualState.registryId();

View File

@@ -49,6 +49,7 @@ public final class BlockKeys {
public static final Key TWISTING_VINES = Key.of("minecraft:twisting_vines");
public static final Key KELP = Key.of("minecraft:kelp");
public static final Key CHORUS_PLANT = Key.of("minecraft:chorus_plant");
public static final Key BARRIER = Key.of("minecraft:barrier");
public static final Key CHEST = Key.of("minecraft:chest");
public static final Key BARREL = Key.of("minecraft:barrel");

View File

@@ -22,4 +22,8 @@ public final class BlockRegistryMirror {
public static BlockStateWrapper stoneState() {
return stoneState;
}
public static BlockStateWrapper[] blockStates() {
return blockStates;
}
}

View File

@@ -2,8 +2,11 @@ package net.momirealms.craftengine.core.block;
import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityElement;
import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityElementConfig;
import net.momirealms.craftengine.core.world.collision.AABB;
import java.util.Optional;
public record BlockStateAppearance(BlockStateWrapper blockState, Optional<BlockEntityElementConfig<? extends BlockEntityElement>[]> blockEntityRenderer) {
public record BlockStateAppearance(BlockStateWrapper blockState,
Optional<BlockEntityElementConfig<? extends BlockEntityElement>[]> blockEntityRenderer,
AABB estimateAABB) {
}

View File

@@ -17,9 +17,11 @@ import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.world.CEWorld;
import net.momirealms.craftengine.core.world.World;
import net.momirealms.craftengine.core.world.collision.AABB;
import net.momirealms.sparrow.nbt.CompoundTag;
import net.momirealms.sparrow.nbt.NBT;
import net.momirealms.sparrow.nbt.Tag;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -33,12 +35,15 @@ public final class ImmutableBlockState {
private CompoundTag tag;
private BlockStateWrapper customBlockState;
private BlockStateWrapper vanillaBlockState;
private BlockStateWrapper visualBlockState;
// 安全的在卸载ce后还原的方块
private BlockStateWrapper restoreBlockState;
private BlockBehavior behavior;
private BlockSettings settings;
private BlockEntityType<? extends BlockEntity> blockEntityType;
@Nullable
private BlockEntityElementConfig<? extends BlockEntityElement>[] renderers;
private AABB estimatedBoundingBox;
ImmutableBlockState(
Holder.Reference<CustomBlock> owner,
@@ -84,6 +89,14 @@ public final class ImmutableBlockState {
this.renderers = renderers;
}
public void setEstimatedBoundingBox(AABB aabb) {
this.estimatedBoundingBox = aabb;
}
public AABB estimatedBoundingBox() {
return estimatedBoundingBox;
}
public boolean hasBlockEntity() {
return this.blockEntityType != null;
}
@@ -96,16 +109,30 @@ public final class ImmutableBlockState {
return this.customBlockState;
}
@Deprecated
public BlockStateWrapper vanillaBlockState() {
return this.vanillaBlockState;
return this.visualBlockState;
}
public BlockStateWrapper visualBlockState() {
return this.visualBlockState;
}
@ApiStatus.Internal
public BlockStateWrapper restoreBlockState() {
return this.restoreBlockState;
}
public void setCustomBlockState(@NotNull BlockStateWrapper customBlockState) {
this.customBlockState = customBlockState;
}
public void setVanillaBlockState(@NotNull BlockStateWrapper vanillaBlockState) {
this.vanillaBlockState = vanillaBlockState;
public void setVisualBlockState(@NotNull BlockStateWrapper visualBlockState) {
this.visualBlockState = visualBlockState;
}
public void setRestoreBlockState(BlockStateWrapper restoreBlockState) {
this.restoreBlockState = restoreBlockState;
}
public CompoundTag getNbtToSave() {

View File

@@ -2,16 +2,21 @@ package net.momirealms.craftengine.core.block.entity.render;
import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityElement;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.world.Cullable;
import net.momirealms.craftengine.core.world.collision.AABB;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Experimental
public class ConstantBlockEntityRenderer {
public class ConstantBlockEntityRenderer implements Cullable {
private final BlockEntityElement[] elements;
public final AABB aabb;
public ConstantBlockEntityRenderer(BlockEntityElement[] elements) {
public ConstantBlockEntityRenderer(BlockEntityElement[] elements, AABB aabb) {
this.elements = elements;
this.aabb = aabb;
}
@Override
public void show(Player player) {
for (BlockEntityElement element : this.elements) {
if (element != null) {
@@ -20,6 +25,7 @@ public class ConstantBlockEntityRenderer {
}
}
@Override
public void hide(Player player) {
for (BlockEntityElement element : this.elements) {
if (element != null) {
@@ -47,4 +53,9 @@ public class ConstantBlockEntityRenderer {
public BlockEntityElement[] elements() {
return this.elements;
}
@Override
public AABB aabb() {
return this.aabb;
}
}

View File

@@ -2,6 +2,7 @@ package net.momirealms.craftengine.core.entity.player;
import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.core.advancement.AdvancementType;
import net.momirealms.craftengine.core.block.entity.render.ConstantBlockEntityRenderer;
import net.momirealms.craftengine.core.entity.AbstractEntity;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.plugin.context.CooldownData;
@@ -9,14 +10,14 @@ import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
import net.momirealms.craftengine.core.sound.SoundData;
import net.momirealms.craftengine.core.sound.SoundSource;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.world.BlockPos;
import net.momirealms.craftengine.core.world.Position;
import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.craftengine.core.world.WorldPosition;
import net.momirealms.craftengine.core.world.*;
import net.momirealms.craftengine.core.world.chunk.client.VirtualCullableObject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
public abstract class Player extends AbstractEntity implements NetWorkUser {
private static final Key TYPE = Key.of("minecraft:player");
@@ -35,6 +36,8 @@ public abstract class Player extends AbstractEntity implements NetWorkUser {
@Override
public abstract Object serverPlayer();
public abstract void setClientSideWorld(World world);
public abstract float getDestroyProgress(Object blockState, BlockPos pos);
public abstract void setClientSideCanBreakBlock(boolean canBreak);
@@ -198,6 +201,16 @@ public abstract class Player extends AbstractEntity implements NetWorkUser {
public abstract void sendTotemAnimation(Item<?> totem, @Nullable SoundData sound, boolean silent);
public abstract void addTrackedBlockEntities(Map<BlockPos, ConstantBlockEntityRenderer> renders);
public abstract void addTrackedBlockEntity(BlockPos blockPos, ConstantBlockEntityRenderer renderer);
public abstract VirtualCullableObject getTrackedBlockEntity(BlockPos blockPos);
public abstract void removeTrackedBlockEntities(Collection<BlockPos> renders);
public abstract void clearTrackedBlockEntities();
@Override
public void remove() {
}

View File

@@ -44,7 +44,7 @@ public class LootPool<T> {
}
if (this.compositeCondition.test(context)) {
Consumer<Item<T>> consumer = LootFunction.decorate(this.compositeFunction, lootConsumer, context);
int i = this.rolls.getInt(context) + MiscUtils.fastFloor(this.bonusRolls.getFloat(context) * context.luck());
int i = this.rolls.getInt(context) + MiscUtils.floor(this.bonusRolls.getFloat(context) * context.luck());
for (int j = 0; j < i; ++j) {
this.addRandomItem(createFunctionApplier(consumer, context), context);
}

View File

@@ -21,7 +21,6 @@ import net.momirealms.craftengine.core.plugin.compatibility.CompatibilityManager
import net.momirealms.craftengine.core.plugin.compatibility.PluginTaskRegistry;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.config.template.TemplateManager;
import net.momirealms.craftengine.core.plugin.config.template.TemplateManagerImpl;
import net.momirealms.craftengine.core.plugin.context.GlobalVariableManager;
import net.momirealms.craftengine.core.plugin.dependency.Dependencies;
import net.momirealms.craftengine.core.plugin.dependency.Dependency;

View File

@@ -203,6 +203,8 @@ public class Config {
protected boolean emoji$contexts$sign;
protected int emoji$max_emojis_per_parse;
protected boolean client_optimization$entity_culling$enable;
public Config(CraftEngine plugin) {
this.plugin = plugin;
this.configVersion = PluginProperties.getValue("config");
@@ -562,6 +564,9 @@ public class Config {
emoji$contexts$sign = config.getBoolean("emoji.contexts.sign", true);
emoji$max_emojis_per_parse = config.getInt("emoji.max-emojis-per-parse", 32);
// client optimization
client_optimization$entity_culling$enable = config.getBoolean("client-optimization.entity-culling.enable", false);
firstTime = false;
}
@@ -1152,6 +1157,10 @@ public class Config {
return instance.resource_pack$optimization$texture$zopfli_iterations;
}
public static boolean enableEntityCulling() {
return instance.client_optimization$entity_culling$enable;
}
public YamlDocument loadOrCreateYamlData(String fileName) {
Path path = this.plugin.dataFolderPath().resolve(fileName);
if (!Files.exists(path)) {

View File

@@ -2,14 +2,7 @@ package net.momirealms.craftengine.core.plugin.config.template;
import net.momirealms.craftengine.core.plugin.Manageable;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.config.template.argument.TemplateArgument;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.SNBTReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public interface TemplateManager extends Manageable {

View File

@@ -3,7 +3,6 @@ package net.momirealms.craftengine.core.plugin.config.template.argument;
import com.ezylang.evalex.Expression;
import com.ezylang.evalex.data.EvaluationValue;
import net.momirealms.craftengine.core.plugin.config.template.ArgumentString;
import net.momirealms.craftengine.core.plugin.config.template.TemplateManager;
import net.momirealms.craftengine.core.util.Key;
import java.util.Locale;

View File

@@ -40,7 +40,7 @@ public class MatchBlockCondition<CTX extends Context> implements Condition<CTX>
Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION);
if (optionalWorldPosition.isPresent()) {
World world = optionalWorldPosition.get().world();
ExistingBlock blockAt = world.getBlock(MiscUtils.fastFloor(this.x.getDouble(ctx)), MiscUtils.fastFloor(this.y.getDouble(ctx)), MiscUtils.fastFloor(this.z.getDouble(ctx)));
ExistingBlock blockAt = world.getBlock(MiscUtils.floor(this.x.getDouble(ctx)), MiscUtils.floor(this.y.getDouble(ctx)), MiscUtils.floor(this.z.getDouble(ctx)));
return MiscUtils.matchRegex(blockAt.id().asString(), this.ids, this.regexMatch);
}
return false;

View File

@@ -28,7 +28,7 @@ public class BreakBlockFunction<CTX extends Context> extends AbstractConditional
@Override
public void runInternal(CTX ctx) {
Optional<Player> optionalPlayer = ctx.getOptionalParameter(DirectContextParameters.PLAYER);
optionalPlayer.ifPresent(player -> player.breakBlock(MiscUtils.fastFloor(x.getDouble(ctx)), MiscUtils.fastFloor(y.getDouble(ctx)), MiscUtils.fastFloor(z.getDouble(ctx))));
optionalPlayer.ifPresent(player -> player.breakBlock(MiscUtils.floor(x.getDouble(ctx)), MiscUtils.floor(y.getDouble(ctx)), MiscUtils.floor(z.getDouble(ctx))));
}
@Override

View File

@@ -55,9 +55,9 @@ public class CycleBlockPropertyFunction<CTX extends Context> extends AbstractCon
Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION);
if (optionalWorldPosition.isEmpty()) return;
World world = optionalWorldPosition.get().world();
int x = MiscUtils.fastFloor(this.x.getDouble(ctx));
int y = MiscUtils.fastFloor(this.y.getDouble(ctx));
int z = MiscUtils.fastFloor(this.z.getDouble(ctx));
int x = MiscUtils.floor(this.x.getDouble(ctx));
int y = MiscUtils.floor(this.y.getDouble(ctx));
int z = MiscUtils.floor(this.z.getDouble(ctx));
BlockStateWrapper wrapper = updateBlockState(world.getBlock(x, y, z).blockState(), ctx);
world.setBlockState(x, y, z, wrapper, this.updateFlags.getInt(ctx));
}

View File

@@ -40,7 +40,7 @@ public class PlaceBlockFunction<CTX extends Context> extends AbstractConditional
Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION);
if (optionalWorldPosition.isPresent()) {
World world = optionalWorldPosition.get().world();
world.setBlockState(MiscUtils.fastFloor(this.x.getDouble(ctx)), MiscUtils.fastFloor(this.y.getDouble(ctx)), MiscUtils.fastFloor(this.z.getDouble(ctx)), this.lazyBlockState.get(), this.updateFlags.getInt(ctx));
world.setBlockState(MiscUtils.floor(this.x.getDouble(ctx)), MiscUtils.floor(this.y.getDouble(ctx)), MiscUtils.floor(this.z.getDouble(ctx)), this.lazyBlockState.get(), this.updateFlags.getInt(ctx));
}
}

View File

@@ -48,7 +48,7 @@ public class RunFunction<CTX extends Context> extends AbstractConditionalFunctio
for (Function<CTX> function : functions) {
function.run(ctx);
}
}, delay, pos.world().platformWorld(), MiscUtils.fastFloor(pos.x()) >> 4, MiscUtils.fastFloor(pos.z()) >> 4);
}, delay, pos.world().platformWorld(), MiscUtils.floor(pos.x()) >> 4, MiscUtils.floor(pos.z()) >> 4);
}
}
}

View File

@@ -45,9 +45,9 @@ public class TransformBlockFunction<CTX extends Context> extends AbstractConditi
Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION);
if (optionalWorldPosition.isPresent()) {
World world = optionalWorldPosition.get().world();
int x = MiscUtils.fastFloor(this.x.getDouble(ctx));
int y = MiscUtils.fastFloor(this.y.getDouble(ctx));
int z = MiscUtils.fastFloor(this.z.getDouble(ctx));
int x = MiscUtils.floor(this.x.getDouble(ctx));
int y = MiscUtils.floor(this.y.getDouble(ctx));
int z = MiscUtils.floor(this.z.getDouble(ctx));
BlockStateWrapper existingBlockState = world.getBlock(x, y, z).blockState().withProperties(this.properties);
CompoundTag newProperties = new CompoundTag();
for (String propertyName : existingBlockState.getPropertyNames()) {

View File

@@ -40,9 +40,9 @@ public class UpdateBlockPropertyFunction<CTX extends Context> extends AbstractCo
Optional<WorldPosition> optionalWorldPosition = ctx.getOptionalParameter(DirectContextParameters.POSITION);
if (optionalWorldPosition.isPresent()) {
World world = optionalWorldPosition.get().world();
int x = MiscUtils.fastFloor(this.x.getDouble(ctx));
int y = MiscUtils.fastFloor(this.y.getDouble(ctx));
int z = MiscUtils.fastFloor(this.z.getDouble(ctx));
int x = MiscUtils.floor(this.x.getDouble(ctx));
int y = MiscUtils.floor(this.y.getDouble(ctx));
int z = MiscUtils.floor(this.z.getDouble(ctx));
ExistingBlock blockAt = world.getBlock(x, y, z);
BlockStateWrapper wrapper = blockAt.blockState().withProperties(this.properties);
world.setBlockState(x, y, z, wrapper, this.updateFlags.getInt(ctx));

View File

@@ -20,9 +20,9 @@ public class EntityParameterProvider implements ChainParameterProvider<Entity> {
CONTEXT_FUNCTIONS.put(DirectContextParameters.YAW, Entity::xRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.PITCH, Entity::yRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.POSITION, Entity::position);
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.fastFloor(p.x()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.fastFloor(p.y()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.fastFloor(p.z()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.floor(p.x()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.floor(p.y()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.floor(p.z()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.NAME, Entity::name);
CONTEXT_FUNCTIONS.put(DirectContextParameters.UUID, Entity::uuid);
CONTEXT_FUNCTIONS.put(DirectContextParameters.WORLD, Entity::world);

View File

@@ -21,9 +21,9 @@ public class PlayerParameterProvider implements ChainParameterProvider<Player> {
CONTEXT_FUNCTIONS.put(DirectContextParameters.PITCH, Entity::xRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.YAW, Entity::yRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.POSITION, Entity::position);
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.fastFloor(p.x()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.fastFloor(p.y()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.fastFloor(p.z()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.floor(p.x()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.floor(p.y()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.floor(p.z()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.FOOD, Player::foodLevel);
CONTEXT_FUNCTIONS.put(DirectContextParameters.SATURATION, Player::saturation);
CONTEXT_FUNCTIONS.put(DirectContextParameters.NAME, Player::name);

View File

@@ -20,9 +20,9 @@ public class PositionParameterProvider implements ChainParameterProvider<WorldPo
CONTEXT_FUNCTIONS.put(DirectContextParameters.Z, WorldPosition::z);
CONTEXT_FUNCTIONS.put(DirectContextParameters.YAW, WorldPosition::xRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.PITCH, WorldPosition::yRot);
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.fastFloor(p.x()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.fastFloor(p.y()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.fastFloor(p.z()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_X, p -> MiscUtils.floor(p.x()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Y, p -> MiscUtils.floor(p.y()));
CONTEXT_FUNCTIONS.put(DirectContextParameters.BLOCK_Z, p -> MiscUtils.floor(p.z()));
}
@SuppressWarnings("unchecked")

View File

@@ -1,453 +1,362 @@
package net.momirealms.craftengine.core.plugin.entityculling;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.world.ChunkPos;
import net.momirealms.craftengine.core.world.MutableVec3d;
import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.craftengine.core.world.chunk.client.ClientChunk;
import net.momirealms.craftengine.core.world.collision.AABB;
import java.util.Arrays;
import java.util.BitSet;
public class EntityCulling {
// 面掩码常量
private static final int ON_MIN_X = 0x01;
private static final int ON_MAX_X = 0x02;
private static final int ON_MIN_Y = 0x04;
private static final int ON_MAX_Y = 0x08;
private static final int ON_MIN_Z = 0x10;
private static final int ON_MAX_Z = 0x20;
private final int reach;
public final class EntityCulling {
public static final int MAX_SAMPLES = 14;
private final Player player;
private final int maxDistance;
private final double aabbExpansion;
private final DataProvider provider;
private final OcclusionCache cache;
// 重用数据结构减少GC压力
private final BitSet skipList = new BitSet();
private final MutableVec3d[] targetPoints = new MutableVec3d[15];
private final MutableVec3d targetPos = new MutableVec3d(0, 0, 0);
private final int[] cameraPos = new int[3];
private final boolean[] dotselectors = new boolean[14];
private final int[] lastHitBlock = new int[3];
// 状态标志
private boolean allowRayChecks = false;
private boolean allowWallClipping = false;
private final boolean[] dotSelectors = new boolean[MAX_SAMPLES];
private final MutableVec3d[] targetPoints = new MutableVec3d[MAX_SAMPLES];
private final int[] lastHitBlock = new int[MAX_SAMPLES * 3];
private final boolean[] canCheckLastHitBlock = new boolean[MAX_SAMPLES];
private int hitBlockCount = 0;
private int lastVisitChunkX = Integer.MAX_VALUE;
private int lastVisitChunkZ = Integer.MAX_VALUE;
private ClientChunk lastVisitChunk = null;
public EntityCulling(int maxDistance, DataProvider provider) {
this(maxDistance, provider, new ArrayOcclusionCache(maxDistance), 0.5);
}
public EntityCulling(int maxDistance, DataProvider provider, OcclusionCache cache, double aabbExpansion) {
this.reach = maxDistance;
this.provider = provider;
this.cache = cache;
public EntityCulling(Player player, int maxDistance, double aabbExpansion) {
this.player = player;
this.maxDistance = maxDistance;
this.aabbExpansion = aabbExpansion;
// 预先初始化点对象
for(int i = 0; i < targetPoints.length; i++) {
targetPoints[i] = new MutableVec3d(0, 0, 0);
for (int i = 0; i < MAX_SAMPLES; i++) {
this.targetPoints[i] = new MutableVec3d(0,0,0);
}
}
public boolean isAABBVisible(Vec3d aabbMin, MutableVec3d aabbMax, MutableVec3d viewerPosition) {
try {
// 计算包围盒范围
int maxX = MiscUtils.fastFloor(aabbMax.x + aabbExpansion);
int maxY = MiscUtils.fastFloor(aabbMax.y + aabbExpansion);
int maxZ = MiscUtils.fastFloor(aabbMax.z + aabbExpansion);
int minX = MiscUtils.fastFloor(aabbMin.x - aabbExpansion);
int minY = MiscUtils.fastFloor(aabbMin.y - aabbExpansion);
int minZ = MiscUtils.fastFloor(aabbMin.z - aabbExpansion);
public boolean isVisible(AABB aabb, Vec3d cameraPos) {
// 情空标志位
Arrays.fill(this.canCheckLastHitBlock, false);
this.hitBlockCount = 0;
cameraPos[0] = MiscUtils.fastFloor(viewerPosition.x);
cameraPos[1] = MiscUtils.fastFloor(viewerPosition.y);
cameraPos[2] = MiscUtils.fastFloor(viewerPosition.z);
// 判断是否在包围盒内部
Relative relX = Relative.from(minX, maxX, cameraPos[0]);
Relative relY = Relative.from(minY, maxY, cameraPos[1]);
Relative relZ = Relative.from(minZ, maxZ, cameraPos[2]);
if(relX == Relative.INSIDE && relY == Relative.INSIDE && relZ == Relative.INSIDE) {
return true;
// 根据AABB获取能包裹此AABB的最小长方体
int minX = MiscUtils.floor(aabb.minX - this.aabbExpansion);
int minY = MiscUtils.floor(aabb.minY - this.aabbExpansion);
int minZ = MiscUtils.floor(aabb.minZ - this.aabbExpansion);
int maxX = MiscUtils.ceil(aabb.maxX + this.aabbExpansion);
int maxY = MiscUtils.ceil(aabb.maxY + this.aabbExpansion);
int maxZ = MiscUtils.ceil(aabb.maxZ + this.aabbExpansion);
double cameraX = cameraPos.x;
double cameraY = cameraPos.y;
double cameraZ = cameraPos.z;
Relative relX = Relative.from(minX, maxX, cameraX);
Relative relY = Relative.from(minY, maxY, cameraY);
Relative relZ = Relative.from(minZ, maxZ, cameraZ);
// 相机位于实体内部
if (relX == Relative.INSIDE && relY == Relative.INSIDE && relZ == Relative.INSIDE) {
return true;
}
// 如果设置了最大距离
if (this.maxDistance > 0) {
// 计算AABB到相机的最小距离
double distanceSq = 0.0;
// 计算XYZ轴方向的距离
distanceSq += distanceSq(minX, maxX, cameraX, relX);
distanceSq += distanceSq(minY, maxY, cameraY, relY);
distanceSq += distanceSq(minZ, maxZ, cameraZ, relZ);
// 检查距离是否超过最大值
double maxDistanceSq = this.maxDistance * this.maxDistance;
// 超过最大距离,剔除
if (distanceSq > maxDistanceSq) {
return false;
}
skipList.clear();
// 1. 快速检查缓存
int id = 0;
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
for (int z = minZ; z <= maxZ; z++) {
int cachedValue = getCacheValue(x, y, z);
if (cachedValue == 1) return true; // 缓存显示可见
if (cachedValue != 0) skipList.set(id); // 缓存显示不可见或遮挡
id++;
}
}
}
allowRayChecks = false;
id = 0;
// 2. 遍历体素进行光线投射检查
for (int x = minX; x <= maxX; x++) {
// 预计算X轴面的可见性和边缘数据
byte visibleOnFaceX = 0;
byte faceEdgeDataX = 0;
if (x == minX) { faceEdgeDataX |= ON_MIN_X; if (relX == Relative.POSITIVE) visibleOnFaceX |= ON_MIN_X; }
if (x == maxX) { faceEdgeDataX |= ON_MAX_X; if (relX == Relative.NEGATIVE) visibleOnFaceX |= ON_MAX_X; }
for (int y = minY; y <= maxY; y++) {
byte visibleOnFaceY = visibleOnFaceX;
byte faceEdgeDataY = faceEdgeDataX;
if (y == minY) { faceEdgeDataY |= ON_MIN_Y; if (relY == Relative.POSITIVE) visibleOnFaceY |= ON_MIN_Y; }
if (y == maxY) { faceEdgeDataY |= ON_MAX_Y; if (relY == Relative.NEGATIVE) visibleOnFaceY |= ON_MAX_Y; }
for (int z = minZ; z <= maxZ; z++) {
// 如果缓存已标记为不可见,跳过
if(skipList.get(id++)) continue;
byte visibleOnFace = visibleOnFaceY;
byte faceEdgeData = faceEdgeDataY;
if (z == minZ) { faceEdgeData |= ON_MIN_Z; if (relZ == Relative.POSITIVE) visibleOnFace |= ON_MIN_Z; }
if (z == maxZ) { faceEdgeData |= ON_MAX_Z; if (relZ == Relative.NEGATIVE) visibleOnFace |= ON_MAX_Z; }
if (visibleOnFace != 0) {
targetPos.set(x, y, z);
// 检查单个体素是否可见
if (isVoxelVisible(viewerPosition, targetPos, faceEdgeData, visibleOnFace)) {
return true;
}
}
}
}
}
return false;
} catch (Throwable t) {
t.printStackTrace();
return true; // 发生异常默认可见,防止渲染错误
}
}
// 接口定义
public interface DataProvider {
boolean prepareChunk(int chunkX, int chunkZ);
boolean isOpaqueFullCube(int x, int y, int z);
default void cleanup() {}
default void checkingPosition(MutableVec3d[] targetPoints, int size, MutableVec3d viewerPosition) {}
}
/**
* 检查单个体素是否对观察者可见
*/
private boolean isVoxelVisible(MutableVec3d viewerPosition, MutableVec3d position, byte faceData, byte visibleOnFace) {
int targetSize = 0;
Arrays.fill(dotselectors, false);
// 根据相对位置选择需要检测的关键点(角点和面中心点)
if((visibleOnFace & ON_MIN_X) != 0){
dotselectors[0] = true;
if((faceData & ~ON_MIN_X) != 0) { dotselectors[1] = dotselectors[4] = dotselectors[5] = true; }
dotselectors[8] = true;
}
if((visibleOnFace & ON_MIN_Y) != 0){
dotselectors[0] = true;
if((faceData & ~ON_MIN_Y) != 0) { dotselectors[3] = dotselectors[4] = dotselectors[7] = true; }
dotselectors[9] = true;
}
if((visibleOnFace & ON_MIN_Z) != 0){
dotselectors[0] = true;
if((faceData & ~ON_MIN_Z) != 0) { dotselectors[1] = dotselectors[4] = dotselectors[5] = true; }
dotselectors[10] = true;
}
if((visibleOnFace & ON_MAX_X) != 0){
dotselectors[4] = true;
if((faceData & ~ON_MAX_X) != 0) { dotselectors[5] = dotselectors[6] = dotselectors[7] = true; }
dotselectors[11] = true;
}
if((visibleOnFace & ON_MAX_Y) != 0){
dotselectors[1] = true;
if((faceData & ~ON_MAX_Y) != 0) { dotselectors[2] = dotselectors[5] = dotselectors[6] = true; }
dotselectors[12] = true;
}
if((visibleOnFace & ON_MAX_Z) != 0){
dotselectors[2] = true;
if((faceData & ~ON_MAX_Z) != 0) { dotselectors[3] = dotselectors[6] = dotselectors[7] = true; }
dotselectors[13] = true;
}
// 填充目标点使用偏移量防止Z-Fighting或精度问题
if (dotselectors[0]) targetPoints[targetSize++].add(position, 0.05, 0.05, 0.05);
if (dotselectors[1]) targetPoints[targetSize++].add(position, 0.05, 0.95, 0.05);
if (dotselectors[2]) targetPoints[targetSize++].add(position, 0.05, 0.95, 0.95);
if (dotselectors[3]) targetPoints[targetSize++].add(position, 0.05, 0.05, 0.95);
if (dotselectors[4]) targetPoints[targetSize++].add(position, 0.95, 0.05, 0.05);
if (dotselectors[5]) targetPoints[targetSize++].add(position, 0.95, 0.95, 0.05);
if (dotselectors[6]) targetPoints[targetSize++].add(position, 0.95, 0.95, 0.95);
if (dotselectors[7]) targetPoints[targetSize++].add(position, 0.95, 0.05, 0.95);
// 清空之前的缓存
Arrays.fill(this.dotSelectors, false);
if (relX == Relative.POSITIVE) {
this.dotSelectors[0] = this.dotSelectors[2] = this.dotSelectors[4] = this.dotSelectors[6] = this.dotSelectors[10] = true;
} else if (relX == Relative.NEGATIVE) {
this.dotSelectors[1] = this.dotSelectors[3] = this.dotSelectors[5] = this.dotSelectors[7] = this.dotSelectors[11] = true;
}
if (relY == Relative.POSITIVE) {
this.dotSelectors[0] = this.dotSelectors[1] = this.dotSelectors[2] = this.dotSelectors[3] = this.dotSelectors[12] = true;
} else if (relY == Relative.NEGATIVE) {
this.dotSelectors[4] = this.dotSelectors[5] = this.dotSelectors[6] = this.dotSelectors[7] = this.dotSelectors[13] = true;
}
if (relZ == Relative.POSITIVE) {
this.dotSelectors[0] = this.dotSelectors[1] = this.dotSelectors[4] = this.dotSelectors[5] = this.dotSelectors[8] = true;
} else if (relZ == Relative.NEGATIVE) {
this.dotSelectors[2] = this.dotSelectors[3] = this.dotSelectors[6] = this.dotSelectors[7] = this.dotSelectors[9] = true;
}
int size = 0;
if (this.dotSelectors[0]) targetPoints[size++].set(minX, minY, minZ);
if (this.dotSelectors[1]) targetPoints[size++].set(maxX, minY, minZ);
if (this.dotSelectors[2]) targetPoints[size++].set(minX, minY, maxZ);
if (this.dotSelectors[3]) targetPoints[size++].set(maxX, minY, maxZ);
if (this.dotSelectors[4]) targetPoints[size++].set(minX, maxY, minZ);
if (this.dotSelectors[5]) targetPoints[size++].set(maxX, maxY, minZ);
if (this.dotSelectors[6]) targetPoints[size++].set(minX, maxY, maxZ);
if (this.dotSelectors[7]) targetPoints[size++].set(maxX, maxY, maxZ);
// 面中心点
if (dotselectors[8]) targetPoints[targetSize++].add(position, 0.05, 0.5, 0.5);
if (dotselectors[9]) targetPoints[targetSize++].add(position, 0.5, 0.05, 0.5);
if (dotselectors[10]) targetPoints[targetSize++].add(position, 0.5, 0.5, 0.05);
if (dotselectors[11]) targetPoints[targetSize++].add(position, 0.95, 0.5, 0.5);
if (dotselectors[12]) targetPoints[targetSize++].add(position, 0.5, 0.95, 0.5);
if (dotselectors[13]) targetPoints[targetSize++].add(position, 0.5, 0.5, 0.95);
double averageX = (minX + maxX) / 2.0;
double averageY = (minY + maxY) / 2.0;
double averageZ = (minZ + maxZ) / 2.0;
if (this.dotSelectors[8]) targetPoints[size++].set(averageX, averageY, minZ);
if (this.dotSelectors[9]) targetPoints[size++].set(averageX, averageY, maxZ);
if (this.dotSelectors[10]) targetPoints[size++].set(minX, averageY, averageZ);
if (this.dotSelectors[11]) targetPoints[size++].set(maxX, averageY, averageZ);
if (this.dotSelectors[12]) targetPoints[size++].set(averageX, minY, averageZ);
if (this.dotSelectors[13]) targetPoints[size].set(averageX, maxY, averageZ);
return isVisible(viewerPosition, targetPoints, targetSize);
}
// 优化:使用基本数据类型代替对象分配
private boolean rayIntersection(int[] b, MutableVec3d rayOrigin, double dirX, double dirY, double dirZ) {
double invX = 1.0 / dirX;
double invY = 1.0 / dirY;
double invZ = 1.0 / dirZ;
double t1 = (b[0] - rayOrigin.x) * invX;
double t2 = (b[0] + 1 - rayOrigin.x) * invX;
double t3 = (b[1] - rayOrigin.y) * invY;
double t4 = (b[1] + 1 - rayOrigin.y) * invY;
double t5 = (b[2] - rayOrigin.z) * invZ;
double t6 = (b[2] + 1 - rayOrigin.z) * invZ;
double tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6));
double tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6));
// tmax > 0: 射线与AABB相交但AABB在身后
// tmin > tmax: 射线不相交
return tmax > 0 && tmin <= tmax;
return isVisible(cameraPos, this.targetPoints, size);
}
/**
* 基于网格的光线追踪 (DDA算法)
* 检测射线与轴对齐边界框(AABB)是否相交
* 使用slab方法进行射线-AABB相交检测
*/
private boolean isVisible(MutableVec3d start, MutableVec3d[] targets, int size) {
int startX = cameraPos[0];
int startY = cameraPos[1];
int startZ = cameraPos[2];
private boolean rayIntersection(int x, int y, int z, Vec3d rayOrigin, MutableVec3d rayDirection) {
// 计算射线方向的倒数,避免除法运算
// 这对于处理射线方向分量为0的情况很重要
MutableVec3d inverseRayDirection = new MutableVec3d(1, 1, 1).divide(rayDirection);
for (int v = 0; v < size; v++) {
MutableVec3d target = targets[v];
// 计算射线与边界框各对面slab的相交参数
// 对于每个轴,计算射线进入和退出该轴对应两个平面的时间
double tMinX = (x - rayOrigin.x) * inverseRayDirection.x;
double tMaxX = (x + 1 - rayOrigin.x) * inverseRayDirection.x;
double tMinY = (y - rayOrigin.y) * inverseRayDirection.y;
double tMaxY = (y + 1 - rayOrigin.y) * inverseRayDirection.y;
double tMinZ = (z - rayOrigin.z) * inverseRayDirection.z;
double tMaxZ = (z + 1 - rayOrigin.z) * inverseRayDirection.z;
double relX = start.x - target.x;
double relY = start.y - target.y;
double relZ = start.z - target.z;
// 计算射线进入边界框的最大时间(最近进入点)
// 需要取各轴进入时间的最大值,因为射线必须进入所有轴的范围内
double tEntry = Math.max(Math.max(Math.min(tMinX, tMaxX), Math.min(tMinY, tMaxY)), Math.min(tMinZ, tMaxZ));
// 优化避免在此处创建新的Vec3d对象进行归一化
if(allowRayChecks) {
double len = Math.sqrt(relX * relX + relY * relY + relZ * relZ);
// 传入归一化后的方向分量
if (rayIntersection(lastHitBlock, start, relX / len, relY / len, relZ / len)) {
continue;
// 计算射线退出边界框的最短时间(最早退出点)
// 需要取各轴退出时间的最小值,因为射线一旦退出任一轴的范围就离开了边界框
double tExit = Math.min(Math.min(Math.max(tMinX, tMaxX), Math.max(tMinY, tMaxY)), Math.max(tMinZ, tMaxZ));
// 如果最早退出时间大于0说明整个边界框在射线起点后面
// 这种情况我们视为不相交,因为通常我们只关心射线前方的相交
if (tExit > 0) {
return false;
}
// 如果进入时间大于退出时间,说明没有有效的相交区间
// 这发生在射线完全错过边界框的情况下
// 满足以下条件说明射线与边界框相交:
// 1. 进入时间 <= 退出时间(存在有效相交区间)
// 2. 退出时间 <= 0边界框至少有一部分在射线起点前方或包含起点
return tEntry <= tExit;
}
/**
* 使用3D DDA算法检测从起点到多个目标点的视线是否通畅
* 算法基于数字微分分析,遍历射线路径上的所有方块
*/
private boolean isVisible(Vec3d start, MutableVec3d[] targets, int targetCount) {
// 起点所在方块的整数坐标(世界坐标转换为方块坐标)
int startBlockX = MiscUtils.floor(start.x);
int startBlockY = MiscUtils.floor(start.y);
int startBlockZ = MiscUtils.floor(start.z);
// 遍历所有目标点进行视线检测
outer: for (int targetIndex = 0; targetIndex < targetCount; targetIndex++) {
MutableVec3d currentTarget = targets[targetIndex];
// 计算起点到目标的相对向量(世界坐标差)
double deltaX = start.x - currentTarget.x;
double deltaY = start.y - currentTarget.y;
double deltaZ = start.z - currentTarget.z;
// 检查之前命中的方块,大概率还是命中
for (int i = 0; i < MAX_SAMPLES; i++) {
if (this.canCheckLastHitBlock[i]) {
int offset = i * 3;
if (rayIntersection(this.lastHitBlock[offset], this.lastHitBlock[offset + 1], this.lastHitBlock[offset + 2], start, new MutableVec3d(deltaX, deltaY, deltaZ).normalize())) {
continue outer;
}
} else {
break;
}
}
double dimAbsX = Math.abs(relX);
double dimAbsY = Math.abs(relY);
double dimAbsZ = Math.abs(relZ);
double dimFracX = 1f / dimAbsX;
double dimFracY = 1f / dimAbsY;
double dimFracZ = 1f / dimAbsZ;
// 计算相对向量的绝对值,用于确定各方向上的距离
double absDeltaX = Math.abs(deltaX);
double absDeltaY = Math.abs(deltaY);
double absDeltaZ = Math.abs(deltaZ);
int intersectCount = 1;
int x_inc, y_inc, z_inc;
double t_next_y, t_next_x, t_next_z;
// 预计算每单位距离在各方块边界上的步进增量
// 这些值表示射线穿过一个方块所需的时间分数
double stepIncrementX = 1.0 / (absDeltaX + 1e-10); // 避免除0
double stepIncrementY = 1.0 / (absDeltaY + 1e-10);
double stepIncrementZ = 1.0 / (absDeltaZ + 1e-10);
// 初始化DDA步进参数
if (dimAbsX == 0f) {
x_inc = 0; t_next_x = dimFracX;
} else if (target.x > start.x) {
x_inc = 1;
intersectCount += MiscUtils.fastFloor(target.x) - startX;
t_next_x = (startX + 1 - start.x) * dimFracX;
// 射线将穿过的总方块数量(包括起点和终点)
int totalBlocksToCheck = 1;
// 各方块坐标的步进方向1: 正向, -1: 反向, 0: 静止)
int stepDirectionX, stepDirectionY, stepDirectionZ;
// 到下一个方块边界的时间参数(射线参数化表示)
double nextStepTimeX, nextStepTimeY, nextStepTimeZ;
// X方向步进参数计算
if (absDeltaX == 0.0) {
// X方向无变化射线平行于YZ平面
stepDirectionX = 0;
nextStepTimeX = stepIncrementX;
} else if (currentTarget.x > start.x) {
// 目标在起点右侧,向右步进
stepDirectionX = 1;
totalBlocksToCheck += MiscUtils.floor(currentTarget.x) - startBlockX;
nextStepTimeX = (startBlockX + 1 - start.x) * stepIncrementX;
} else {
x_inc = -1;
intersectCount += startX - MiscUtils.fastFloor(target.x);
t_next_x = (start.x - startX) * dimFracX;
// 目标在起点左侧,向左步进
stepDirectionX = -1;
totalBlocksToCheck += startBlockX - MiscUtils.floor(currentTarget.x);
nextStepTimeX = (start.x - startBlockX) * stepIncrementX;
}
if (dimAbsY == 0f) {
y_inc = 0; t_next_y = dimFracY;
} else if (target.y > start.y) {
y_inc = 1;
intersectCount += MiscUtils.fastFloor(target.y) - startY;
t_next_y = (startY + 1 - start.y) * dimFracY;
// Y方向步进参数计算
if (absDeltaY == 0.0) {
// Y方向无变化射线平行于XZ平面
stepDirectionY = 0;
nextStepTimeY = stepIncrementY;
} else if (currentTarget.y > start.y) {
// 目标在起点上方,向上步进
stepDirectionY = 1;
totalBlocksToCheck += MiscUtils.floor(currentTarget.y) - startBlockY;
nextStepTimeY = (startBlockY + 1 - start.y) * stepIncrementY;
} else {
y_inc = -1;
intersectCount += startY - MiscUtils.fastFloor(target.y);
t_next_y = (start.y - startY) * dimFracY;
// 目标在起点下方,向下步进
stepDirectionY = -1;
totalBlocksToCheck += startBlockY - MiscUtils.floor(currentTarget.y);
nextStepTimeY = (start.y - startBlockY) * stepIncrementY;
}
if (dimAbsZ == 0f) {
z_inc = 0; t_next_z = dimFracZ;
} else if (target.z > start.z) {
z_inc = 1;
intersectCount += MiscUtils.fastFloor(target.z) - startZ;
t_next_z = (startZ + 1 - start.z) * dimFracZ;
// Z方向步进参数计算
if (absDeltaZ == 0.0) {
// Z方向无变化射线平行于XY平面
stepDirectionZ = 0;
nextStepTimeZ = stepIncrementZ;
} else if (currentTarget.z > start.z) {
// 目标在起点前方,向前步进
stepDirectionZ = 1;
totalBlocksToCheck += MiscUtils.floor(currentTarget.z) - startBlockZ;
nextStepTimeZ = (startBlockZ + 1 - start.z) * stepIncrementZ;
} else {
z_inc = -1;
intersectCount += startZ - MiscUtils.fastFloor(target.z);
t_next_z = (start.z - startZ) * dimFracZ;
// 目标在起点后方,向后步进
stepDirectionZ = -1;
totalBlocksToCheck += startBlockZ - MiscUtils.floor(currentTarget.z);
nextStepTimeZ = (start.z - startBlockZ) * stepIncrementZ;
}
boolean finished = stepRay(startX, startY, startZ,
dimFracX, dimFracY, dimFracZ, intersectCount,
x_inc, y_inc, z_inc,
t_next_y, t_next_x, t_next_z);
provider.cleanup();
if (finished) {
cacheResult(targets[0], true);
// 执行DDA步进算法遍历射线路径上的所有方块
boolean isLineOfSightClear = stepRay(
startBlockX, startBlockY, startBlockZ,
stepIncrementX, stepIncrementY, stepIncrementZ, totalBlocksToCheck,
stepDirectionX, stepDirectionY, stepDirectionZ,
nextStepTimeY, nextStepTimeX, nextStepTimeZ);
// 如果当前目标点可见立即返回
if (isLineOfSightClear) {
return true;
} else {
allowRayChecks = true;
this.canCheckLastHitBlock[this.hitBlockCount++] = true;
}
}
cacheResult(targets[0], false);
return false;
}
private boolean stepRay(int currentX, int currentY, int currentZ,
double distInX, double distInY, double distInZ,
int n, int x_inc, int y_inc, int z_inc,
double t_next_y, double t_next_x, double t_next_z) {
allowWallClipping = true; // 初始允许穿墙直到移出起始方块
private boolean stepRay(int currentBlockX, int currentBlockY, int currentBlockZ,
double stepSizeX, double stepSizeY, double stepSizeZ,
int remainingSteps, int stepDirectionX, int stepDirectionY,
int stepDirectionZ, double nextStepTimeY, double nextStepTimeX,
double nextStepTimeZ) {
for (; n > 1; n--) {
// 检查缓存状态2=遮挡
int cVal = getCacheValue(currentX, currentY, currentZ);
if (cVal == 2 && !allowWallClipping) {
lastHitBlock[0] = currentX; lastHitBlock[1] = currentY; lastHitBlock[2] = currentZ;
return false;
// 遍历射线路径上的所有方块(跳过最后一个目标方块)
for (; remainingSteps > 1; remainingSteps--) {
// 检查当前方块是否遮挡视线
if (isOccluding(currentBlockX, currentBlockY, currentBlockZ)) {
this.lastHitBlock[this.hitBlockCount * 3] = currentBlockX;
this.lastHitBlock[this.hitBlockCount * 3 + 1] = currentBlockY;
this.lastHitBlock[this.hitBlockCount * 3 + 2] = currentBlockZ;
return false; // 视线被遮挡,立即返回
}
if (cVal == 0) {
// 未缓存查询Provider
int chunkX = currentX >> 4;
int chunkZ = currentZ >> 4;
if (!provider.prepareChunk(chunkX, chunkZ)) return false;
if (provider.isOpaqueFullCube(currentX, currentY, currentZ)) {
if (!allowWallClipping) {
cache.setLastHidden();
lastHitBlock[0] = currentX; lastHitBlock[1] = currentY; lastHitBlock[2] = currentZ;
return false;
}
} else {
allowWallClipping = false;
cache.setLastVisible();
}
} else if(cVal == 1) {
allowWallClipping = false;
}
// DDA算法选择下一个体素
if (t_next_y < t_next_x && t_next_y < t_next_z) {
currentY += y_inc;
t_next_y += distInY;
} else if (t_next_x < t_next_y && t_next_x < t_next_z) {
currentX += x_inc;
t_next_x += distInX;
// 基于时间参数选择下一个要遍历的方块方向
// 选择距离最近的方块边界作为下一步
if (nextStepTimeY < nextStepTimeX && nextStepTimeY < nextStepTimeZ) {
// Y方向边界最近垂直移动
currentBlockY += stepDirectionY;
nextStepTimeY += stepSizeY;
} else if (nextStepTimeX < nextStepTimeY && nextStepTimeX < nextStepTimeZ) {
// X方向边界最近水平移动
currentBlockX += stepDirectionX;
nextStepTimeX += stepSizeX;
} else {
currentZ += z_inc;
t_next_z += distInZ;
// Z方向边界最近深度移动
currentBlockZ += stepDirectionZ;
nextStepTimeZ += stepSizeZ;
}
}
// 成功遍历所有中间方块,视线通畅
return true;
}
// 缓存状态:-1=无效, 0=未检查, 1=可见, 2=遮挡
private int getCacheValue(int x, int y, int z) {
x -= cameraPos[0];
y -= cameraPos[1];
z -= cameraPos[2];
if (Math.abs(x) > reach - 2 || Math.abs(y) > reach - 2 || Math.abs(z) > reach - 2) {
return -1;
private double distanceSq(int min, int max, double camera, Relative rel) {
if (rel == Relative.NEGATIVE) {
double dx = camera - max;
return dx * dx;
} else if (rel == Relative.POSITIVE) {
double dx = min - camera;
return dx * dx;
}
return cache.getState(x + reach, y + reach, z + reach);
return 0d;
}
private void cacheResult(MutableVec3d vector, boolean result) {
int cx = MiscUtils.fastFloor(vector.x) - cameraPos[0] + reach;
int cy = MiscUtils.fastFloor(vector.y) - cameraPos[1] + reach;
int cz = MiscUtils.fastFloor(vector.z) - cameraPos[2] + reach;
if (result) cache.setVisible(cx, cy, cz);
else cache.setHidden(cx, cy, cz);
private boolean isOccluding(int x, int y, int z) {
int chunkX = x >> 4;
int chunkZ = z >> 4;
ClientChunk trackedChunk;
// 使用上次记录的值比每次走hash都更快
if (chunkX == this.lastVisitChunkX && chunkZ == this.lastVisitChunkZ) {
trackedChunk = this.lastVisitChunk;
} else {
trackedChunk = this.player.getTrackedChunk(ChunkPos.asLong(chunkX, chunkZ));
this.lastVisitChunk = trackedChunk;
this.lastVisitChunkX = chunkX;
this.lastVisitChunkZ = chunkZ;
}
if (trackedChunk == null) {
return false;
}
return trackedChunk.isOccluding(x, y, z);
}
public void resetCache() {
this.cache.resetCache();
public void removeLastVisitChunkIfMatches(int chunkX, int chunkZ) {
if (this.lastVisitChunk != null && this.lastVisitChunkX == chunkX && this.lastVisitChunkZ == chunkZ) {
this.lastVisitChunk = null;
}
}
private enum Relative {
INSIDE, POSITIVE, NEGATIVE;
public static Relative from(int min, int max, int pos) {
if (max > pos && min > pos) return POSITIVE;
else if (min < pos && max < pos) return NEGATIVE;
public static Relative from(int min, int max, double pos) {
if (min > pos) return POSITIVE;
else if (max < pos) return NEGATIVE;
return INSIDE;
}
}
public interface OcclusionCache {
void resetCache();
void setVisible(int x, int y, int z);
void setHidden(int x, int y, int z);
int getState(int x, int y, int z);
void setLastHidden();
void setLastVisible();
}
// 使用位运算压缩存储状态的缓存实现
public static class ArrayOcclusionCache implements OcclusionCache {
private final int reachX2;
private final byte[] cache;
private int entry, offset;
public ArrayOcclusionCache(int reach) {
this.reachX2 = reach * 2;
// 每一个位置占2位
this.cache = new byte[(reachX2 * reachX2 * reachX2) / 4 + 1];
}
@Override
public void resetCache() {
Arrays.fill(cache, (byte) 0);
}
private void calcIndex(int x, int y, int z) {
int positionKey = x + y * reachX2 + z * reachX2 * reachX2;
entry = positionKey / 4;
offset = (positionKey % 4) * 2;
}
@Override
public void setVisible(int x, int y, int z) {
calcIndex(x, y, z);
cache[entry] |= 1 << offset;
}
@Override
public void setHidden(int x, int y, int z) {
calcIndex(x, y, z);
cache[entry] |= 1 << (offset + 1);
}
@Override
public int getState(int x, int y, int z) {
calcIndex(x, y, z);
return (cache[entry] >> offset) & 3;
}
@Override
public void setLastVisible() {
cache[entry] |= 1 << offset;
}
@Override
public void setLastHidden() {
cache[entry] |= 1 << (offset + 1);
}
}
}
}

View File

@@ -0,0 +1,216 @@
package net.momirealms.craftengine.core.plugin.entityculling;
import java.util.Iterator;
import java.util.NoSuchElementException;
// Amanatides, J., & Woo, A. A Fast Voxel Traversal Algorithm for Ray Tracing. http://www.cse.yorku.ca/~amana/research/grid.pdf.
public final class VoxelIterator implements Iterator<int[]> {
private int x;
private int y;
private int z;
private int stepX;
private int stepY;
private int stepZ;
private double tMax;
private double tMaxX;
private double tMaxY;
private double tMaxZ;
private double tDeltaX;
private double tDeltaY;
private double tDeltaZ;
private int[] ref = new int[3]; // This implementation always returns ref or refSwap to avoid garbage. Can easily be changed if needed.
private int[] refSwap = new int[3];
private int[] next;
public VoxelIterator(double startX, double startY, double startZ, double endX, double endY, double endZ) {
initialize(startX, startY, startZ, endX, endY, endZ);
}
public VoxelIterator(int x, int y, int z, double startX, double startY, double startZ, double endX, double endY, double endZ) {
initialize(x, y, z, startX, startY, startZ, endX, endY, endZ);
}
public VoxelIterator(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
initialize(startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
initialize(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance, boolean normalized) {
if (normalized) {
initializeNormalized(startX, startY, startZ, directionX, directionY, directionZ, distance);
} else {
initialize(startX, startY, startZ, directionX, directionY, directionZ, distance);
}
}
public VoxelIterator(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance, boolean normalized) {
if (normalized) {
initializeNormalized(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
} else {
initialize(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
}
}
public VoxelIterator initialize(double startX, double startY, double startZ, double endX, double endY, double endZ) {
return initialize(floor(startX), floor(startY), floor(startZ), startX, startY, startZ, endX, endY, endZ);
}
public VoxelIterator initialize(int x, int y, int z, double startX, double startY, double startZ, double endX, double endY, double endZ) {
double directionX = endX - startX;
double directionY = endY - startY;
double directionZ = endZ - startZ;
double distance = Math.sqrt(directionX * directionX + directionY * directionY + directionZ * directionZ);
double fixedDistance = distance == 0. ? Double.NaN : distance;
directionX /= fixedDistance;
directionY /= fixedDistance;
directionZ /= fixedDistance;
return initializeNormalized(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator initialize(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
return initialize(floor(startX), floor(startY), floor(startZ), startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator initialize(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
double signum = Math.signum(distance);
directionX *= signum;
directionY *= signum;
directionZ *= signum;
double length = Math.sqrt(directionX * directionX + directionY * directionY + directionZ * directionZ);
if (length == 0.) {
length = Double.NaN;
}
directionX /= length;
directionY /= length;
directionZ /= length;
return initializeNormalized(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, Math.abs(distance));
}
public VoxelIterator initializeNormalized(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
return initializeNormalized(floor(startX), floor(startY), floor(startZ), startX, startY, startZ, directionX, directionY, directionZ, Math.abs(distance));
}
public VoxelIterator initializeNormalized(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
this.x = x;
this.y = y;
this.z = z;
tMax = distance;
stepX = directionX < 0. ? -1 : 1;
stepY = directionY < 0. ? -1 : 1;
stepZ = directionZ < 0. ? -1 : 1;
tMaxX = directionX == 0. ? Double.POSITIVE_INFINITY : (x + (stepX + 1) / 2 - startX) / directionX;
tMaxY = directionY == 0. ? Double.POSITIVE_INFINITY : (y + (stepY + 1) / 2 - startY) / directionY;
tMaxZ = directionZ == 0. ? Double.POSITIVE_INFINITY : (z + (stepZ + 1) / 2 - startZ) / directionZ;
tDeltaX = 1. / Math.abs(directionX);
tDeltaY = 1. / Math.abs(directionY);
tDeltaZ = 1. / Math.abs(directionZ);
next = ref;
ref[0] = x;
ref[1] = y;
ref[2] = z;
return this;
}
public int[] calculateNext() {
if (tMaxX < tMaxY) {
if (tMaxZ < tMaxX) {
if (tMaxZ <= tMax) {
z += stepZ;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxZ += tDeltaZ;
} else {
next = null;
}
} else {
if (tMaxX <= tMax) {
if (tMaxZ == tMaxX) {
z += stepZ;
tMaxZ += tDeltaZ;
}
x += stepX;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxX += tDeltaX;
} else {
next = null;
}
}
} else if (tMaxY < tMaxZ) {
if (tMaxY <= tMax) {
if (tMaxX == tMaxY) {
x += stepX;
tMaxX += tDeltaX;
}
y += stepY;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxY += tDeltaY;
} else {
next = null;
}
} else {
if (tMaxZ <= tMax) {
if (tMaxX == tMaxZ) {
x += stepX;
tMaxX += tDeltaX;
}
if (tMaxY == tMaxZ) {
y += stepY;
tMaxY += tDeltaY;
}
z += stepZ;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxZ += tDeltaZ;
} else {
next = null;
}
}
return next;
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public int[] next() {
int[] next = this.next;
if (next == null) {
throw new NoSuchElementException();
}
int[] temp = ref;
ref = refSwap;
refSwap = temp;
this.next = ref;
calculateNext();
return next;
}
private static int floor(double value) {
int i = (int) value;
return value < (double) i ? i - 1 : i;
}
}

View File

@@ -6,7 +6,8 @@ import net.kyori.adventure.text.Component;
import net.momirealms.craftengine.core.plugin.Plugin;
import net.momirealms.craftengine.core.util.IntIdentityList;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.world.chunk.ChunkStatus;
import net.momirealms.craftengine.core.world.World;
import net.momirealms.craftengine.core.world.chunk.client.ClientChunk;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
@@ -65,9 +66,7 @@ public interface NetWorkUser {
@ApiStatus.Internal
ConnectionState encoderState();
int clientSideSectionCount();
Key clientSideDimension();
World clientSideWorld();
Object serverPlayer();
@@ -89,9 +88,9 @@ public interface NetWorkUser {
boolean isChunkTracked(long chunkPos);
ChunkStatus getTrackedChunk(long chunkPos);
ClientChunk getTrackedChunk(long chunkPos);
void addTrackedChunk(long chunkPos, ChunkStatus chunkStatus);
void addTrackedChunk(long chunkPos, ClientChunk chunkStatus);
void clearTrackedChunks();

View File

@@ -38,7 +38,7 @@ public class Color {
}
public static Color fromVector3f(Vector3f vec) {
return new Color(0 << 24 /*不可省略*/ | MiscUtils.fastFloor(vec.x) << 16 | MiscUtils.fastFloor(vec.y) << 8 | MiscUtils.fastFloor(vec.z));
return new Color(0 << 24 /*不可省略*/ | MiscUtils.floor(vec.x) << 16 | MiscUtils.floor(vec.y) << 8 | MiscUtils.floor(vec.z));
}
public static int opaque(int color) {

View File

@@ -21,19 +21,19 @@ public class MiscUtils {
}
});
public static int fastFloor(double value) {
public static int floor(double value) {
int truncated = (int) value;
return value < (double) truncated ? truncated - 1 : truncated;
}
public static int fastFloor(float value) {
public static int floor(float value) {
int truncated = (int) value;
return value < (double) truncated ? truncated - 1 : truncated;
}
public static int lerpDiscrete(float delta, int start, int end) {
int i = end - start;
return start + fastFloor(delta * (float) (i - 1)) + (delta > 0.0F ? 1 : 0);
return start + floor(delta * (float) (i - 1)) + (delta > 0.0F ? 1 : 0);
}
public static int murmurHash3Mixer(int value) {
@@ -270,7 +270,7 @@ public class MiscUtils {
}
public static byte packDegrees(float degrees) {
return (byte) fastFloor(degrees * 256.0F / 360.0F);
return (byte) floor(degrees * 256.0F / 360.0F);
}
public static float unpackDegrees(byte degrees) {

View File

@@ -6,6 +6,7 @@ import net.momirealms.craftengine.core.plugin.locale.LocalizedException;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.craftengine.core.world.collision.AABB;
import org.jetbrains.annotations.Nullable;
import org.joml.Quaternionf;
import org.joml.Vector3f;
@@ -341,4 +342,52 @@ public final class ResourceConfigUtils {
}
TranslationManager.instance().log(e.node(), e.arguments());
}
public static AABB getAsAABB(Object o, String option) {
if (o == null) {
throw new LocalizedResourceConfigException("warning.config.type.aabb", "null", option);
}
if (o instanceof Number number) {
double min = -(number.doubleValue() / 2);
double max = number.doubleValue() / 2;
return new AABB(min, min, min, max, max, max);
} else {
double[] args;
if (o instanceof List<?> list) {
args = new double[list.size()];
for (int i = 0; i < args.length; i++) {
if (list.get(i) instanceof Number number) {
args[i] = number.doubleValue();
} else {
try {
args[i] = Double.parseDouble(list.get(i).toString());
} catch (NumberFormatException e) {
throw new LocalizedResourceConfigException("warning.config.type.aabb", o.toString(), option);
}
}
}
} else {
String[] split = o.toString().split(",");
args = new double[split.length];
for (int i = 0; i < args.length; i++) {
try {
args[i] = Double.parseDouble(split[i]);
} catch (NumberFormatException e) {
throw new LocalizedResourceConfigException("warning.config.type.aabb", o.toString(), option);
}
}
}
if (args.length == 1) {
return new AABB(-args[0]/2, -args[0]/2, -args[0]/2, args[0]/2, args[0]/2, args[0]/2);
} else if (args.length == 2) {
return new AABB(-args[0]/2, -args[1]/2, -args[0]/2, args[0]/2, args[1]/2, args[0]/2);
} else if (args.length == 3) {
return new AABB(-args[0]/2, -args[1]/2, -args[2]/2, args[0]/2, args[1]/2, args[2]/2);
} else if (args.length == 6) {
return new AABB(args[0], args[1], args[2], args[3], args[4], args[5]);
} else {
throw new LocalizedResourceConfigException("warning.config.type.aabb", o.toString(), option);
}
}
}
}

View File

@@ -23,7 +23,7 @@ public class BlockPos extends Vec3i {
}
public static BlockPos fromVec3d(Vec3d vec) {
return new BlockPos(MiscUtils.fastFloor(vec.x), MiscUtils.fastFloor(vec.y), MiscUtils.fastFloor(vec.z));
return new BlockPos(MiscUtils.floor(vec.x), MiscUtils.floor(vec.y), MiscUtils.floor(vec.z));
}
public static BlockPos of(long packedPos) {

View File

@@ -0,0 +1,13 @@
package net.momirealms.craftengine.core.world;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.world.collision.AABB;
public interface Cullable {
AABB aabb();
void show(Player player);
void hide(Player player);
}

View File

@@ -23,9 +23,9 @@ public class EntityHitResult {
}
private BlockPos getBlockPos() {
int x = MiscUtils.fastFloor(this.position.x);
int y = MiscUtils.fastFloor(this.position.y);
int z = MiscUtils.fastFloor(this.position.z);
int x = MiscUtils.floor(this.position.x);
int y = MiscUtils.floor(this.position.y);
int z = MiscUtils.floor(this.position.z);
if (this.direction == Direction.UP) {
if (this.position.y % 1 == 0) {
y--;

View File

@@ -14,9 +14,9 @@ public class MutableVec3d implements Position {
}
public MutableVec3d toCenter() {
this.x = MiscUtils.fastFloor(x) + 0.5;
this.y = MiscUtils.fastFloor(y) + 0.5;
this.z = MiscUtils.fastFloor(z) + 0.5;
this.x = MiscUtils.floor(x) + 0.5;
this.y = MiscUtils.floor(y) + 0.5;
this.z = MiscUtils.floor(z) + 0.5;
return this;
}

View File

@@ -17,4 +17,39 @@ public class SectionPos extends Vec3i {
public static int sectionRelative(int rel) {
return rel & 15;
}
public static SectionPos of(long packed) {
return new SectionPos((int) (packed >> 42), (int) (packed << 44 >> 44), (int) (packed << 22 >> 42));
}
public long asLong() {
return ((long) this.x & 4194303L) << 42 | (long) this.y & 1048575L | ((long) this.z & 4194303L) << 20;
}
public ChunkPos asChunkPos() {
return new ChunkPos(this.x, this.z);
}
public static short packSectionRelativePos(BlockPos pos) {
return (short) ((pos.x & 15) << 8 | (pos.z & 15) << 4 | pos.y & 15);
}
public static BlockPos unpackSectionRelativePos(short encoded) {
int x = (encoded >> 8) & 15;
int z = (encoded >> 4) & 15;
int y = encoded & 15;
return new BlockPos(x, y, z);
}
public final int minBlockX() {
return this.x << 4;
}
public final int minBlockY() {
return this.y << 4;
}
public final int minBlockZ() {
return this.z << 4;
}
}

View File

@@ -15,7 +15,7 @@ public class Vec3d implements Position {
}
public Vec3d toCenter() {
return new Vec3d(MiscUtils.fastFloor(x) + 0.5, MiscUtils.fastFloor(y) + 0.5, MiscUtils.fastFloor(z) + 0.5);
return new Vec3d(MiscUtils.floor(x) + 0.5, MiscUtils.floor(y) + 0.5, MiscUtils.floor(z) + 0.5);
}
public Vec3d add(Vec3d vec) {

View File

@@ -4,9 +4,9 @@ import net.momirealms.craftengine.core.util.Direction;
public class Vec3i implements Comparable<Vec3i> {
public static final Vec3i ZERO = new Vec3i(0, 0, 0);
protected int x;
protected int y;
protected int z;
public final int x;
public final int y;
public final int z;
public Vec3i(int x, int y, int z) {
this.x = x;
@@ -30,21 +30,6 @@ public class Vec3i implements Comparable<Vec3i> {
return x == 0 && y == 0 && z == 0 ? this : new Vec3i(this.x() + x, this.y() + y, this.z() + z);
}
protected Vec3i setX(int x) {
this.x = x;
return this;
}
protected Vec3i setY(int y) {
this.y = y;
return this;
}
protected Vec3i setZ(int z) {
this.z = z;
return this;
}
@Override
public boolean equals(Object object) {
return this == object || object instanceof Vec3i vec3i && this.x == vec3i.x && this.y == vec3i.y && this.z == vec3i.z;

View File

@@ -77,15 +77,24 @@ public class ArrayPalette<T> implements Palette<T> {
@Override
public boolean hasAny(Predicate<T> predicate) {
for(int i = 0; i < this.size; ++i) {
for (int i = 0; i < this.size; ++i) {
if (predicate.test(this.array[i])) {
return true;
}
}
return false;
}
@Override
public boolean allMatch(Predicate<T> predicate) {
for (int i = 0; i < this.size; ++i) {
if (!predicate.test(this.array[i])) {
return false;
}
}
return true;
}
@Override
public T get(int id) {
if (id >= 0 && id < this.size) {

View File

@@ -67,7 +67,7 @@ public class BiMapPalette<T> implements Palette<T> {
@Override
public boolean hasAny(Predicate<T> predicate) {
for(int i = 0; i < this.getSize(); ++i) {
for (int i = 0; i < this.getSize(); ++i) {
if (predicate.test(this.map.get(i))) {
return true;
}
@@ -75,6 +75,16 @@ public class BiMapPalette<T> implements Palette<T> {
return false;
}
@Override
public boolean allMatch(Predicate<T> predicate) {
for (int i = 0; i < this.getSize(); ++i) {
if (!predicate.test(this.map.get(i))) {
return false;
}
}
return true;
}
@Override
public T get(int id) {
T object = this.map.get(id);

View File

@@ -11,8 +11,10 @@ import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityEl
import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityElementConfig;
import net.momirealms.craftengine.core.block.entity.tick.*;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.logger.Debugger;
import net.momirealms.craftengine.core.world.*;
import net.momirealms.craftengine.core.world.chunk.client.VirtualCullableObject;
import net.momirealms.craftengine.core.world.chunk.serialization.DefaultBlockEntityRendererSerializer;
import net.momirealms.craftengine.core.world.chunk.serialization.DefaultBlockEntitySerializer;
import net.momirealms.sparrow.nbt.ListTag;
@@ -91,8 +93,12 @@ public class CEChunk {
public void spawnBlockEntities(Player player) {
try {
this.renderLock.readLock().lock();
for (ConstantBlockEntityRenderer renderer : this.constantBlockEntityRenderers.values()) {
renderer.show(player);
if (Config.enableEntityCulling()) {
player.addTrackedBlockEntities(this.constantBlockEntityRenderers);
} else {
for (ConstantBlockEntityRenderer renderer : this.constantBlockEntityRenderers.values()) {
renderer.show(player);
}
}
for (DynamicBlockEntityRenderer renderer : this.dynamicBlockEntityRenderers.values()) {
renderer.show(player);
@@ -105,8 +111,12 @@ public class CEChunk {
public void despawnBlockEntities(Player player) {
try {
this.renderLock.readLock().lock();
for (ConstantBlockEntityRenderer renderer : this.constantBlockEntityRenderers.values()) {
renderer.hide(player);
if (Config.enableEntityCulling()) {
player.removeTrackedBlockEntities(this.constantBlockEntityRenderers.keySet());
} else {
for (ConstantBlockEntityRenderer renderer : this.constantBlockEntityRenderers.values()) {
renderer.hide(player);
}
}
for (DynamicBlockEntityRenderer renderer : this.dynamicBlockEntityRenderers.values()) {
renderer.hide(player);
@@ -129,7 +139,7 @@ public class CEChunk {
BlockEntityElementConfig<? extends BlockEntityElement>[] renderers = state.constantRenderers();
if (renderers != null && renderers.length > 0) {
BlockEntityElement[] elements = new BlockEntityElement[renderers.length];
ConstantBlockEntityRenderer renderer = new ConstantBlockEntityRenderer(elements);
ConstantBlockEntityRenderer renderer = new ConstantBlockEntityRenderer(elements, state.estimatedBoundingBox().move(pos));
World wrappedWorld = this.world.world();
List<Player> trackedBy = getTrackedBy();
boolean hasTrackedBy = trackedBy != null && !trackedBy.isEmpty();
@@ -146,8 +156,20 @@ public class CEChunk {
if (element != null) {
elements[0] = element;
if (hasTrackedBy) {
for (Player player : trackedBy) {
element.transform(player);
// 如果启用实体剔除,那么只对已经渲染的进行变换
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
VirtualCullableObject trackedBlockEntity = player.getTrackedBlockEntity(pos);
if (trackedBlockEntity == null || trackedBlockEntity.isShown()) {
element.transform(player);
}
}
}
// 否则直接变换
else {
for (Player player : trackedBy) {
element.transform(player);
}
}
}
break outer;
@@ -156,9 +178,18 @@ public class CEChunk {
BlockEntityElement element = config.create(wrappedWorld, pos);
elements[0] = element;
if (hasTrackedBy) {
for (Player player : trackedBy) {
previousElement.hide(player);
element.show(player);
// 如果启用实体剔除,那么只添加记录
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
player.addTrackedBlockEntity(pos, renderer);
}
}
// 否则直接显示
else {
for (Player player : trackedBy) {
previousElement.hide(player);
element.show(player);
}
}
}
}
@@ -173,8 +204,20 @@ public class CEChunk {
previousElements[j] = null;
elements[i] = newElement;
if (hasTrackedBy) {
for (Player player : trackedBy) {
newElement.transform(player);
// 如果启用实体剔除,那么只对已经渲染的进行变换
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
VirtualCullableObject trackedBlockEntity = player.getTrackedBlockEntity(pos);
if (trackedBlockEntity == null || trackedBlockEntity.isShown()) {
newElement.transform(player);
}
}
}
// 否则直接变换
else {
for (Player player : trackedBy) {
newElement.transform(player);
}
}
}
continue outer;
@@ -184,8 +227,14 @@ public class CEChunk {
BlockEntityElement newElement = config.create(wrappedWorld, pos);
elements[i] = newElement;
if (hasTrackedBy) {
for (Player player : trackedBy) {
newElement.show(player);
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
player.addTrackedBlockEntity(pos, renderer);
}
} else {
for (Player player : trackedBy) {
newElement.show(player);
}
}
}
}
@@ -205,8 +254,14 @@ public class CEChunk {
elements[i] = renderers[i].create(wrappedWorld, pos);
}
if (hasTrackedBy) {
for (Player player : trackedBy) {
renderer.show(player);
if (Config.enableEntityCulling()) {
for (Player player : trackedBy) {
player.addTrackedBlockEntity(pos, renderer);
}
} else {
for (Player player : trackedBy) {
renderer.show(player);
}
}
}
}
@@ -233,8 +288,14 @@ public class CEChunk {
ConstantBlockEntityRenderer removed = this.constantBlockEntityRenderers.remove(pos);
if (removed != null) {
if (hide) {
for (Player player : getTrackedBy()) {
removed.hide(player);
if (Config.enableEntityCulling()) {
for (Player player : getTrackedBy()) {
player.removeTrackedBlockEntities(List.of(pos));
}
} else {
for (Player player : getTrackedBy()) {
removed.hide(player);
}
}
}
}
@@ -439,7 +500,7 @@ public class CEChunk {
return Collections.unmodifiableCollection(this.blockEntities.values());
}
public List<BlockPos> constantBlockEntityRenderers() {
public List<BlockPos> constantBlockEntityRendererPositions() {
try {
this.renderLock.readLock().lock();
return new ArrayList<>(this.constantBlockEntityRenderers.keySet());

View File

@@ -1,7 +0,0 @@
package net.momirealms.craftengine.core.world.chunk;
public class ChunkStatus {
public ChunkStatus() {
}
}

View File

@@ -29,6 +29,11 @@ public class IdListPalette<T> implements Palette<T> {
return true;
}
@Override
public boolean allMatch(Predicate<T> predicate) {
return true;
}
@Override
public T get(int id) {
T object = this.idList.get(id);

View File

@@ -13,6 +13,8 @@ public interface Palette<T> {
boolean hasAny(Predicate<T> predicate);
boolean allMatch(Predicate<T> predicate);
T get(int id);
int getSize();

View File

@@ -45,6 +45,15 @@ public class SingularPalette<T> implements Palette<T> {
}
}
@Override
public boolean allMatch(Predicate<T> predicate) {
if (this.entry == null) {
throw new IllegalStateException("Use of an uninitialized palette");
} else {
return predicate.test(this.entry);
}
}
@Override
public T get(int id) {
if (this.entry != null && id == 0) {

View File

@@ -0,0 +1,53 @@
package net.momirealms.craftengine.core.world.chunk.client;
import net.momirealms.craftengine.core.world.SectionPos;
import net.momirealms.craftengine.core.world.WorldHeight;
import org.jetbrains.annotations.Nullable;
public class ClientChunk {
@Nullable
public final ClientSection[] sections;
private final WorldHeight worldHeight;
public ClientChunk(ClientSection[] sections, WorldHeight worldHeight) {
this.sections = sections;
this.worldHeight = worldHeight;
}
@Nullable
public ClientSection[] sections() {
return sections;
}
public boolean isOccluding(int x, int y, int z) {
if (this.sections == null) return false;
int index = sectionIndex(SectionPos.blockToSectionCoord(y));
ClientSection section = this.sections[index];
if (section == null) return false;
return section.isOccluding((y & 15) << 8 | (z & 15) << 4 | x & 15);
}
public void setOccluding(int x, int y, int z, boolean occluding) {
if (this.sections == null) return;
int index = sectionIndex(SectionPos.blockToSectionCoord(y));
ClientSection section = this.sections[index];
if (section == null) return;
section.setOccluding((y & 15) << 8 | (z & 15) << 4 | x & 15, occluding);
}
public int sectionIndex(int sectionId) {
return sectionId - this.worldHeight.getMinSection();
}
@Nullable
public ClientSection sectionByIndex(int sectionIndex) {
if (this.sections == null) return null;
return this.sections[sectionIndex];
}
@Nullable
public ClientSection sectionById(int sectionId) {
if (this.sections == null) return null;
return this.sections[sectionIndex(sectionId)];
}
}

View File

@@ -0,0 +1,34 @@
package net.momirealms.craftengine.core.world.chunk.client;
public class ClientSection {
private ClientSectionOcclusionStorage storage;
public ClientSection(ClientSectionOcclusionStorage storage) {
this.storage = storage;
}
public boolean isOccluding(int x, int y, int z) {
return isOccluding((y << 4 | z) << 4 | x);
}
public boolean isOccluding(int index) {
return this.storage.isOccluding(index);
}
public void setOccluding(int x, int y, int z, boolean value) {
this.setOccluding((y << 4 | z) << 4 | x, value);
}
public void setOccluding(int index, boolean value) {
boolean wasOccluding = this.storage.isOccluding(index);
if (wasOccluding != value) {
if (this.storage instanceof PackedOcclusionStorage arrayStorage) {
arrayStorage.set(index, value);
} else {
PackedOcclusionStorage newStorage = new PackedOcclusionStorage(wasOccluding);
newStorage.set(index, value);
this.storage = newStorage;
}
}
}
}

View File

@@ -0,0 +1,6 @@
package net.momirealms.craftengine.core.world.chunk.client;
public interface ClientSectionOcclusionStorage {
boolean isOccluding(int index);
}

View File

@@ -0,0 +1,37 @@
package net.momirealms.craftengine.core.world.chunk.client;
import java.util.Arrays;
public class PackedOcclusionStorage implements ClientSectionOcclusionStorage {
private static final int SIZE = 4096;
private static final int LONGS = SIZE / 64;
private final long[] data;
public PackedOcclusionStorage() {
this.data = new long[LONGS];
}
public PackedOcclusionStorage(boolean defaultValue) {
this.data = new long[LONGS];
if (defaultValue) {
Arrays.fill(this.data, -1L); // 所有位设为1
}
}
@Override
public boolean isOccluding(int index) {
int arrayIndex = index >>> 6; // index / 64
int bitIndex = index & 0x3F; // index % 64
return (this.data[arrayIndex] & (1L << bitIndex)) != 0;
}
public void set(int index, boolean occlusion) {
int arrayIndex = index >>> 6; // index / 64
int bitIndex = index & 0x3F; // index % 64
if (occlusion) {
this.data[arrayIndex] |= (1L << bitIndex);
} else {
this.data[arrayIndex] &= ~(1L << bitIndex);
}
}
}

View File

@@ -0,0 +1,14 @@
package net.momirealms.craftengine.core.world.chunk.client;
public class SingularOcclusionStorage implements ClientSectionOcclusionStorage {
private final boolean isOccluding;
public SingularOcclusionStorage(boolean isOccluding) {
this.isOccluding = isOccluding;
}
@Override
public boolean isOccluding(int index) {
return this.isOccluding;
}
}

View File

@@ -0,0 +1,32 @@
package net.momirealms.craftengine.core.world.chunk.client;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.world.Cullable;
public class VirtualCullableObject {
public final Cullable cullable;
private boolean isShown;
public VirtualCullableObject(Cullable cullable) {
this.cullable = cullable;
this.isShown = false;
}
public Cullable cullable() {
return cullable;
}
public boolean isShown() {
return isShown;
}
public void setShown(Player player, boolean shown) {
if (this.isShown == shown) return;
this.isShown = shown;
if (shown) {
this.cullable.show(player);
} else {
this.cullable.hide(player);
}
}
}

View File

@@ -32,7 +32,7 @@ public final class DefaultChunkSerializer {
if (!blockEntities.isEmpty()) {
chunkNbt.put("block_entities", blockEntities);
}
ListTag blockEntityRenders = DefaultBlockEntityRendererSerializer.serialize(chunk.constantBlockEntityRenderers());
ListTag blockEntityRenders = DefaultBlockEntityRendererSerializer.serialize(chunk.constantBlockEntityRendererPositions());
if (!blockEntityRenders.isEmpty()) {
chunkNbt.put("block_entity_renderers", blockEntityRenders);
}

View File

@@ -35,6 +35,17 @@ public class AABB {
this.maxZ = Math.max(pos1.z, pos2.z);
}
public AABB move(BlockPos pos) {
return new AABB(
this.minX + pos.x + 0.5,
this.minY + pos.y + 0.5,
this.minZ + pos.z + 0.5,
this.maxX + pos.x + 0.5,
this.maxY + pos.y + 0.5,
this.maxZ + pos.z + 0.5
);
}
public AABB(BlockPos pos) {
this(pos.x(), pos.y(), pos.z(), pos.x() + 1, pos.y() + 1, pos.z() + 1);
}