diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/entity/renderer/element/ItemBlockEntityElementConfig.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/entity/renderer/element/ItemBlockEntityElementConfig.java index 96873d757..b68c2af5d 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/entity/renderer/element/ItemBlockEntityElementConfig.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/entity/renderer/element/ItemBlockEntityElementConfig.java @@ -45,6 +45,14 @@ public class ItemBlockEntityElementConfig implements BlockEntityElementConfig elementClass() { return ItemBlockEntityElement.class; @@ -62,6 +70,12 @@ public class ItemBlockEntityElementConfig implements BlockEntityElementConfig { List dataValues = new ArrayList<>(); - ItemDisplayEntityData.DisplayedItem.addEntityDataIfNotDefaultValue(item.apply(player).getLiteralObject(), dataValues); - ItemDisplayEntityData.Scale.addEntityDataIfNotDefaultValue(this.scale, dataValues); - ItemDisplayEntityData.RotationLeft.addEntityDataIfNotDefaultValue(this.rotation, dataValues); - ItemDisplayEntityData.BillboardConstraints.addEntityDataIfNotDefaultValue(this.billboard.id(), dataValues); - ItemDisplayEntityData.Translation.addEntityDataIfNotDefaultValue(this.translation, dataValues); - ItemDisplayEntityData.DisplayType.addEntityDataIfNotDefaultValue(this.displayContext.id(), dataValues); - ItemDisplayEntityData.ShadowRadius.addEntityDataIfNotDefaultValue(this.shadowRadius, dataValues); - ItemDisplayEntityData.ShadowStrength.addEntityDataIfNotDefaultValue(this.shadowStrength, dataValues); + ItemDisplayEntityData.DisplayedItem.addEntityData(item.apply(player).getLiteralObject(), dataValues); + ItemDisplayEntityData.Scale.addEntityData(this.scale, dataValues); + ItemDisplayEntityData.RotationLeft.addEntityData(this.rotation, dataValues); + ItemDisplayEntityData.BillboardConstraints.addEntityData(this.billboard.id(), dataValues); + ItemDisplayEntityData.Translation.addEntityData(this.translation, dataValues); + ItemDisplayEntityData.DisplayType.addEntityData(this.displayContext.id(), dataValues); + ItemDisplayEntityData.ShadowRadius.addEntityData(this.shadowRadius, dataValues); + ItemDisplayEntityData.ShadowStrength.addEntityData(this.shadowStrength, dataValues); return dataValues; }; } @@ -80,14 +81,6 @@ public class ItemDisplayBlockEntityElementConfig implements BlockEntityElementCo @Override public ItemDisplayBlockEntityElement create(World world, BlockPos pos, ItemDisplayBlockEntityElement previous) { - Quaternionf previousRotation = previous.config.rotation; - if (previousRotation.x != 0 || previousRotation.y != 0 || previousRotation.z != 0 || previousRotation.w != 1) { - return null; - } - Vector3f translation = previous.config.translation; - if (translation.x != 0 || translation.y != 0 || translation.z != 0) { - return null; - } return new ItemDisplayBlockEntityElement(this, pos, previous.entityId, previous.config.yRot != this.yRot || previous.config.xRot != this.xRot || @@ -95,6 +88,14 @@ public class ItemDisplayBlockEntityElementConfig implements BlockEntityElementCo ); } + @Override + public ItemDisplayBlockEntityElement createExact(World world, BlockPos pos, ItemDisplayBlockEntityElement previous) { + if (!previous.config.equals(this)) { + return null; + } + return new ItemDisplayBlockEntityElement(this, pos, previous.entityId, false); + } + @Override public Class elementClass() { return ItemDisplayBlockEntityElement.class; @@ -148,6 +149,16 @@ public class ItemDisplayBlockEntityElementConfig implements BlockEntityElementCo return this.lazyMetadataPacket.apply(player); } + @Override + public boolean equals(Object o) { + if (!(o instanceof ItemDisplayBlockEntityElementConfig that)) return false; + return Float.compare(xRot, that.xRot) == 0 && + Float.compare(yRot, that.yRot) == 0 && + Objects.equal(position, that.position) && + Objects.equal(translation, that.translation) && + Objects.equal(rotation, that.rotation); + } + public static class Factory implements BlockEntityElementConfigFactory { @SuppressWarnings("unchecked") diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/entity/renderer/element/TextDisplayBlockEntityElementConfig.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/entity/renderer/element/TextDisplayBlockEntityElementConfig.java index 43b6af680..12012724d 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/entity/renderer/element/TextDisplayBlockEntityElementConfig.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/block/entity/renderer/element/TextDisplayBlockEntityElementConfig.java @@ -1,5 +1,6 @@ package net.momirealms.craftengine.bukkit.block.entity.renderer.element; +import com.google.common.base.Objects; import net.kyori.adventure.text.Component; import net.momirealms.craftengine.bukkit.entity.data.TextDisplayEntityData; import net.momirealms.craftengine.bukkit.util.ComponentUtils; @@ -52,11 +53,11 @@ public class TextDisplayBlockEntityElementConfig implements BlockEntityElementCo this.billboard = billboard; this.lazyMetadataPacket = player -> { List dataValues = new ArrayList<>(); - TextDisplayEntityData.Text.addEntityDataIfNotDefaultValue(ComponentUtils.adventureToMinecraft(text(player)), dataValues); - TextDisplayEntityData.Scale.addEntityDataIfNotDefaultValue(this.scale, dataValues); - TextDisplayEntityData.RotationLeft.addEntityDataIfNotDefaultValue(this.rotation, dataValues); - TextDisplayEntityData.BillboardConstraints.addEntityDataIfNotDefaultValue(this.billboard.id(), dataValues); - TextDisplayEntityData.Translation.addEntityDataIfNotDefaultValue(this.translation, dataValues); + TextDisplayEntityData.Text.addEntityData(ComponentUtils.adventureToMinecraft(text(player)), dataValues); + TextDisplayEntityData.Scale.addEntityData(this.scale, dataValues); + TextDisplayEntityData.RotationLeft.addEntityData(this.rotation, dataValues); + TextDisplayEntityData.BillboardConstraints.addEntityData(this.billboard.id(), dataValues); + TextDisplayEntityData.Translation.addEntityData(this.translation, dataValues); return dataValues; }; } @@ -68,14 +69,6 @@ public class TextDisplayBlockEntityElementConfig implements BlockEntityElementCo @Override public TextDisplayBlockEntityElement create(World world, BlockPos pos, TextDisplayBlockEntityElement previous) { - Quaternionf previousRotation = previous.config.rotation; - if (previousRotation.x != 0 || previousRotation.y != 0 || previousRotation.z != 0 || previousRotation.w != 1) { - return null; - } - Vector3f translation = previous.config.translation; - if (translation.x != 0 || translation.y != 0 || translation.z != 0) { - return null; - } return new TextDisplayBlockEntityElement(this, pos, previous.entityId, previous.config.yRot != this.yRot || previous.config.xRot != this.xRot || @@ -83,6 +76,14 @@ public class TextDisplayBlockEntityElementConfig implements BlockEntityElementCo ); } + @Override + public TextDisplayBlockEntityElement createExact(World world, BlockPos pos, TextDisplayBlockEntityElement previous) { + if (!previous.config.equals(this)) { + return null; + } + return new TextDisplayBlockEntityElement(this, pos, previous.entityId, false); + } + @Override public Class elementClass() { return TextDisplayBlockEntityElement.class; @@ -124,6 +125,16 @@ public class TextDisplayBlockEntityElementConfig implements BlockEntityElementCo return this.lazyMetadataPacket.apply(player); } + @Override + public boolean equals(Object o) { + if (!(o instanceof TextDisplayBlockEntityElementConfig that)) return false; + return Float.compare(xRot, that.xRot) == 0 && + Float.compare(yRot, that.yRot) == 0 && + Objects.equal(position, that.position) && + Objects.equal(translation, that.translation) && + Objects.equal(rotation, that.rotation); + } + public static class Factory implements BlockEntityElementConfigFactory { @SuppressWarnings("unchecked") diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/BukkitCommandManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/BukkitCommandManager.java index 6ce68c38d..00dbb694b 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/BukkitCommandManager.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/BukkitCommandManager.java @@ -42,6 +42,7 @@ public class BukkitCommandManager extends AbstractCommandManager new SearchUsageAdminCommand(this, plugin), new TestCommand(this, plugin), new SetLocaleCommand(this, plugin), + new SetEntityViewDistanceScaleCommand(this, plugin), new UnsetLocaleCommand(this, plugin), new DebugGetBlockStateRegistryIdCommand(this, plugin), new DebugGetBlockInternalIdCommand(this, plugin), diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/SetEntityViewDistanceScaleCommand.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/SetEntityViewDistanceScaleCommand.java new file mode 100644 index 000000000..17d5ca2e3 --- /dev/null +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/command/feature/SetEntityViewDistanceScaleCommand.java @@ -0,0 +1,42 @@ +package net.momirealms.craftengine.bukkit.plugin.command.feature; + +import net.kyori.adventure.text.Component; +import net.momirealms.craftengine.bukkit.api.BukkitAdaptors; +import net.momirealms.craftengine.bukkit.plugin.command.BukkitCommandFeature; +import net.momirealms.craftengine.bukkit.plugin.user.BukkitServerPlayer; +import net.momirealms.craftengine.core.plugin.CraftEngine; +import net.momirealms.craftengine.core.plugin.command.CraftEngineCommandManager; +import net.momirealms.craftengine.core.plugin.command.FlagKeys; +import net.momirealms.craftengine.core.plugin.locale.MessageConstants; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.incendo.cloud.Command; +import org.incendo.cloud.bukkit.parser.PlayerParser; +import org.incendo.cloud.parser.standard.DoubleParser; + +public class SetEntityViewDistanceScaleCommand extends BukkitCommandFeature { + + public SetEntityViewDistanceScaleCommand(CraftEngineCommandManager commandManager, CraftEngine plugin) { + super(commandManager, plugin); + } + + @Override + public Command.Builder assembleCommand(org.incendo.cloud.CommandManager manager, Command.Builder builder) { + return builder + .flag(FlagKeys.SILENT_FLAG) + .required("player", PlayerParser.playerParser()) + .required("scale", DoubleParser.doubleParser(0.125, 8)) + .handler(context -> { + Player player = context.get("player"); + double scale = context.get("scale"); + BukkitServerPlayer serverPlayer = BukkitAdaptors.adapt(player); + serverPlayer.setEntityCullingViewDistanceScale(scale); + handleFeedback(context, MessageConstants.COMMAND_ENTITY_VIEW_DISTANCE_SCALE_SET_SUCCESS, Component.text(scale), Component.text(player.getName())); + }); + } + + @Override + public String getFeatureID() { + return "set_entity_view_distance_scale"; + } +} diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java index 3549a46fc..7dd9dfc88 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/network/BukkitNetworkManager.java @@ -2046,7 +2046,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes boolean hasGlobalPalette = false; // 创建客户端侧世界(只在开启实体情况下创建) - ClientSection[] clientSections = Config.enableEntityCulling() ? new ClientSection[count] : null; + ClientSection[] clientSections = Config.entityCullingRayTracing() ? new ClientSection[count] : null; for (int i = 0; i < count; i++) { MCSection mcSection = new MCSection(user.clientBlockList(), this.blockList, this.biomeList); @@ -2225,7 +2225,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes // 获取客户端侧区域 ClientSection clientSection = null; - if (Config.enableEntityCulling()) { + if (Config.entityCullingRayTracing()) { SectionPos sectionPos = SectionPos.of(sPos); ClientChunk trackedChunk = user.getTrackedChunk(sectionPos.asChunkPos().longKey); clientSection = trackedChunk.sectionById(sectionPos.y); @@ -2271,7 +2271,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes FriendlyByteBuf buf = event.getBuffer(); BlockPos pos = buf.readBlockPos(); int before = buf.readVarInt(); - if (Config.enableEntityCulling()) { + if (Config.entityCullingRayTracing()) { 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)); @@ -2451,7 +2451,7 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes BlockPos blockPos = buf.readBlockPos(); int state = buf.readInt(); // 移除不透明设置 - if (Config.enableEntityCulling()) { + if (Config.entityCullingRayTracing()) { ClientChunk trackedChunk = user.getTrackedChunk(ChunkPos.asLong(blockPos.x >> 4, blockPos.z >> 4)); if (trackedChunk != null) { trackedChunk.setOccluding(blockPos.x, blockPos.y, blockPos.z, false); diff --git a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/user/BukkitServerPlayer.java b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/user/BukkitServerPlayer.java index a08fea4dc..fc12b179f 100644 --- a/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/user/BukkitServerPlayer.java +++ b/bukkit/src/main/java/net/momirealms/craftengine/bukkit/plugin/user/BukkitServerPlayer.java @@ -32,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.CullingData; import net.momirealms.craftengine.core.plugin.entityculling.EntityCulling; import net.momirealms.craftengine.core.plugin.locale.TranslationManager; import net.momirealms.craftengine.core.plugin.network.ConnectionState; @@ -51,6 +52,7 @@ import org.bukkit.block.Block; import org.bukkit.damage.DamageSource; import org.bukkit.damage.DamageType; import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.inventory.EquipmentSlot; @@ -71,6 +73,7 @@ import java.util.concurrent.ConcurrentHashMap; public class BukkitServerPlayer extends Player { public static final Key SELECTED_LOCALE_KEY = Key.of("craftengine:locale"); + public static final Key ENTITY_CULLING_VIEW_DISTANCE_SCALE = Key.of("craftengine:entity_culling_view_distance_scale"); private final BukkitCraftEngine plugin; // connection state @@ -114,8 +117,6 @@ public class BukkitServerPlayer extends Player { private IntIdentityList blockList = new IntIdentityList(BlockStateUtils.vanillaBlockStateCount()); // cache if player can break blocks private boolean clientSideCanBreak = true; - // prevent AFK players from consuming too much CPU resource on predicting - private Location previousEyeLocation; // a cooldown for better breaking experience private int lastSuccessfulBreak; // player's game tick @@ -142,8 +143,11 @@ public class BukkitServerPlayer extends Player { private int lastStopMiningTick; // 跟踪到的方块实体渲染器 private final Map trackedBlockEntityRenderers = new ConcurrentHashMap<>(); - private final EntityCulling culling; + private Vec3d firstPersonCameraVec3; + private Vec3d thirdPersonCameraVec3; + // 玩家眼睛所在位置 + private Location eyeLocation; public BukkitServerPlayer(BukkitCraftEngine plugin, @Nullable Channel channel) { this.channel = channel; @@ -157,7 +161,7 @@ public class BukkitServerPlayer extends Player { } } } - this.culling = new EntityCulling(this, 64, 0.5); + this.culling = new EntityCulling(this); } public void setPlayer(org.bukkit.entity.Player player) { @@ -169,6 +173,8 @@ public class BukkitServerPlayer extends Player { this.isNameVerified = true; byte[] bytes = player.getPersistentDataContainer().get(KeyUtils.toNamespacedKey(CooldownData.COOLDOWN_KEY), PersistentDataType.BYTE_ARRAY); String locale = player.getPersistentDataContainer().get(KeyUtils.toNamespacedKey(SELECTED_LOCALE_KEY), PersistentDataType.STRING); + Double scale = player.getPersistentDataContainer().get(KeyUtils.toNamespacedKey(ENTITY_CULLING_VIEW_DISTANCE_SCALE), PersistentDataType.DOUBLE); + this.culling.setDistanceScale(Optional.ofNullable(scale).orElse(1.0)); this.selectedLocale = TranslationManager.parseLocale(locale); this.trackedChunks = ConcurrentLong2ReferenceChainedHashTable.createWithCapacity(512, 0.5f); this.entityTypeView = new ConcurrentHashMap<>(256); @@ -509,10 +515,11 @@ public class BukkitServerPlayer extends Player { @Override public void tick() { // not fully online - if (serverPlayer() == null) return; + Object serverPlayer = serverPlayer(); + if (serverPlayer == null) return; + org.bukkit.entity.Player bukkitPlayer = platformPlayer(); if (VersionHelper.isFolia()) { try { - Object serverPlayer = serverPlayer(); Object gameMode = FastNMS.INSTANCE.field$ServerPlayer$gameMode(serverPlayer); this.gameTicks = (int) CoreReflections.field$ServerPlayerGameMode$gameTicks.get(gameMode); } catch (ReflectiveOperationException e) { @@ -524,12 +531,30 @@ public class BukkitServerPlayer extends Player { if (this.gameTicks % 20 == 0) { this.updateGUI(); } + + // 更新眼睛位置 + { + Location unsureEyeLocation = bukkitPlayer.getEyeLocation(); + Entity vehicle = bukkitPlayer.getVehicle(); + if (vehicle != null) { + Object mountPos = FastNMS.INSTANCE.method$Entity$getPassengerRidingPosition(FastNMS.INSTANCE.method$CraftEntity$getHandle(vehicle), serverPlayer); + unsureEyeLocation.set(FastNMS.INSTANCE.field$Vec3$x(mountPos), FastNMS.INSTANCE.field$Vec3$y(mountPos) + bukkitPlayer.getEyeHeight(), FastNMS.INSTANCE.field$Vec3$z(mountPos)); + } + if (Config.predictBreaking() && !this.isDestroyingCustomBlock && !unsureEyeLocation.equals(this.eyeLocation)) { + // if it's not destroying blocks, we do predict + if ((gameTicks() + entityID()) % Config.predictBreakingInterval() == 0) { + this.predictNextBlockToMine(); + } + } + this.eyeLocation = unsureEyeLocation; + } + if (hasSwingHand()) { if (this.isDestroyingBlock) { this.tickBlockDestroy(); } else if (this.lastStopMiningPos != null && this.gameTicks - this.lastStopMiningTick <= 5) { double range = getCachedInteractionRange(); - RayTraceResult result = platformPlayer().rayTraceBlocks(range, FluidCollisionMode.NEVER); + RayTraceResult result = rayTrace(this.eyeLocation, range, FluidCollisionMode.NEVER); if (result != null) { Block hitBlock = result.getHitBlock(); if (hitBlock != null) { @@ -552,25 +577,60 @@ public class BukkitServerPlayer extends Player { this.isHackedBreak = false; } } - if (Config.predictBreaking() && !this.isDestroyingCustomBlock) { - // if it's not destroying blocks, we do predict - if ((gameTicks() + entityID()) % Config.predictBreakingInterval() == 0) { - Location eyeLocation = platformPlayer().getEyeLocation(); - if (eyeLocation.equals(this.previousEyeLocation)) { - return; + + if (Config.entityCullingRayTracing()) { + org.bukkit.entity.Player player = platformPlayer(); + Location eyeLocation = this.eyeLocation.clone(); + this.firstPersonCameraVec3 = LocationUtils.toVec3d(eyeLocation); + int distance = 4; + if (VersionHelper.isOrAbove1_21_6()) { + Entity vehicle = player.getVehicle(); + if (vehicle != null && vehicle.getType() == EntityType.HAPPY_GHAST) { + distance = 8; } - this.previousEyeLocation = eyeLocation; - this.predictNextBlockToMine(); } + this.thirdPersonCameraVec3 = LocationUtils.toVec3d(eyeLocation.subtract(eyeLocation.getDirection().multiply(distance))); } - 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); + } + + @Override + public void entityCullingTick() { + this.culling.restoreTokenOnTick(); + boolean useRayTracing = Config.entityCullingRayTracing(); + for (VirtualCullableObject cullableObject : this.trackedBlockEntityRenderers.values()) { + CullingData cullingData = cullableObject.cullable.cullingData(); + if (cullingData != null) { + boolean firstPersonVisible = this.culling.isVisible(cullingData, this.firstPersonCameraVec3, useRayTracing); + // 之前可见 + if (cullableObject.isShown) { + boolean thirdPersonVisible = this.culling.isVisible(cullingData, this.thirdPersonCameraVec3, useRayTracing); + if (!firstPersonVisible && !thirdPersonVisible) { + cullableObject.setShown(this, false); + } + } + // 之前不可见 + else { + // 但是第一人称可见了 + if (firstPersonVisible) { + // 下次再说 + if (Config.enableEntityCullingRateLimiting() && !this.culling.takeToken()) { + continue; + } + cullableObject.setShown(this, true); + continue; + } + if (this.culling.isVisible(cullingData, this.thirdPersonCameraVec3, useRayTracing)) { + // 下次再说 + if (Config.enableEntityCullingRateLimiting() && !this.culling.takeToken()) { + continue; + } + cullableObject.setShown(this, true); + } + // 仍然不可见 + } + } else { + cullableObject.setShown(this, true); } - long nano2 = System.nanoTime(); - //CraftEngine.instance().logger().info("EntityCulling took " + (nano2 - nano1) / 1_000_000d + "ms"); } } @@ -590,17 +650,12 @@ public class BukkitServerPlayer extends Player { public boolean canInteractWithBlock(BlockPos pos, double distance) { double d = this.getCachedInteractionRange() + distance; - return (new AABB(pos)).distanceToSqr(this.getEyePosition()) < d * d; + return (new AABB(pos)).distanceToSqr(this.getEyePos()) < d * d; } public boolean canInteractPoint(Vec3d pos, double distance) { double d = this.getCachedInteractionRange() + distance; - return Vec3d.distanceToSqr(this.getEyePosition(), pos) < d * d; - } - - public final Vec3d getEyePosition() { - Location eyeLocation = this.platformPlayer().getEyeLocation(); - return new Vec3d(eyeLocation.getX(), eyeLocation.getY(), eyeLocation.getZ()); + return Vec3d.distanceToSqr(this.getEyePos(), pos) < d * d; } @Override @@ -620,7 +675,7 @@ public class BukkitServerPlayer extends Player { private void predictNextBlockToMine() { double range = getCachedInteractionRange() + Config.extendedInteractionRange(); - RayTraceResult result = platformPlayer().rayTraceBlocks(range, FluidCollisionMode.NEVER); + RayTraceResult result = rayTrace(this.eyeLocation, range, FluidCollisionMode.NEVER); if (result == null) { if (!this.clientSideCanBreak) { setClientSideCanBreakBlock(true); @@ -760,7 +815,7 @@ public class BukkitServerPlayer extends Player { try { org.bukkit.entity.Player player = platformPlayer(); double range = getCachedInteractionRange(); - RayTraceResult result = player.rayTraceBlocks(range, FluidCollisionMode.NEVER); + RayTraceResult result = rayTrace(this.eyeLocation, range, FluidCollisionMode.NEVER); if (result == null) return; Block hitBlock = result.getHitBlock(); if (hitBlock == null) return; @@ -1200,6 +1255,9 @@ public class BukkitServerPlayer extends Player { @Override public void removeTrackedChunk(long chunkPos) { this.trackedChunks.remove(chunkPos); + if (Config.entityCullingRayTracing()) { + this.culling.removeLastVisitChunkIfMatches((int) chunkPos, (int) (chunkPos >> 32)); + } } @Override @@ -1276,6 +1334,13 @@ public class BukkitServerPlayer extends Player { } } + @Override + public void setEntityCullingViewDistanceScale(double value) { + value = Math.min(Math.max(0.125, value), 8); + this.culling.setDistanceScale(value); + platformPlayer().getPersistentDataContainer().set(KeyUtils.toNamespacedKey(ENTITY_CULLING_VIEW_DISTANCE_SCALE), PersistentDataType.DOUBLE, value); + } + @Override public void giveExperiencePoints(int xpPoints) { platformPlayer().giveExp(xpPoints); @@ -1340,4 +1405,36 @@ public class BukkitServerPlayer extends Player { public void clearTrackedBlockEntities() { this.trackedBlockEntityRenderers.clear(); } + + @Override + public WorldPosition eyePosition() { + return LocationUtils.toWorldPosition(this.getEyeLocation()); + } + + public Location getEyeLocation() { + org.bukkit.entity.Player player = platformPlayer(); + Location eyeLocation = player.getEyeLocation(); + Entity vehicle = player.getVehicle(); + if (vehicle != null) { + Object mountPos = FastNMS.INSTANCE.method$Entity$getPassengerRidingPosition(FastNMS.INSTANCE.method$CraftEntity$getHandle(vehicle), serverPlayer()); + eyeLocation.set(FastNMS.INSTANCE.field$Vec3$x(mountPos), FastNMS.INSTANCE.field$Vec3$y(mountPos) + player.getEyeHeight(), FastNMS.INSTANCE.field$Vec3$z(mountPos)); + } + return eyeLocation; + } + + public Vec3d getEyePos() { + org.bukkit.entity.Player player = platformPlayer(); + Entity vehicle = player.getVehicle(); + if (vehicle != null) { + Object mountPos = FastNMS.INSTANCE.method$Entity$getPassengerRidingPosition(FastNMS.INSTANCE.method$CraftEntity$getHandle(vehicle), serverPlayer()); + return new Vec3d(FastNMS.INSTANCE.field$Vec3$x(mountPos), FastNMS.INSTANCE.field$Vec3$y(mountPos) + player.getEyeHeight(), FastNMS.INSTANCE.field$Vec3$z(mountPos)); + } else { + Location location = player.getLocation(); + return new Vec3d(location.getX(), location.getY() + player.getEyeHeight(), location.getZ()); + } + } + + private RayTraceResult rayTrace(Location start, double range, FluidCollisionMode mode) { + return start.getWorld().rayTraceBlocks(start, start.getDirection(), range, mode); + } } diff --git a/common-files/src/main/resources/commands.yml b/common-files/src/main/resources/commands.yml index a2bfdbeef..ffd2eadee 100644 --- a/common-files/src/main/resources/commands.yml +++ b/common-files/src/main/resources/commands.yml @@ -129,6 +129,12 @@ unset_locale: usage: - /ce feature locale unset +set_entity_view_distance_scale: + enable: true + permission: ce.command.admin.set_entity_view_distance_scale + usage: + - /ce feature entity-view-distance-scale set + # Debug commands debug_set_block: enable: true diff --git a/common-files/src/main/resources/config.yml b/common-files/src/main/resources/config.yml index 0d6efafb4..6c3d4bf47 100644 --- a/common-files/src/main/resources/config.yml +++ b/common-files/src/main/resources/config.yml @@ -552,10 +552,23 @@ chunk-system: remove: [] convert: {} +# [Premium Exclusive] client-optimization: - # Using server-side ray tracing algorithms to hide certain entities and reduce client-side rendering pressure. + # Requires a restart to fully apply. entity-culling: - enable: false + enable: true + # Using server-side ray tracing algorithms to hide block entities/furniture and reduce client-side rendering pressure. + ray-tracing: true + # Cull entities based on distance + view-distance: 64 # -1 = no limit + # Determining the number of threads to execute these raytrace operations + threads: 1 + # Limit the maximum number of entities with visibility changes per tick for one player + # This helps mitigate client-side performance impacts and server-side bandwidth spikes caused by a large number of entities appearing. + rate-limiting: + enable: true + bucket-size: 1000 + restore-per-tick: 25 # Enables or disables debug mode debug: @@ -563,4 +576,6 @@ debug: packet: false furniture: false item: false - resource-pack: false \ No newline at end of file + resource-pack: false + block: false + entity-culling: false \ No newline at end of file diff --git a/common-files/src/main/resources/resources/default/configuration/blocks/sofa.yml b/common-files/src/main/resources/resources/default/configuration/blocks/sofa.yml index 5aba431e7..120c02811 100644 --- a/common-files/src/main/resources/resources/default/configuration/blocks/sofa.yml +++ b/common-files/src/main/resources/resources/default/configuration/blocks/sofa.yml @@ -93,7 +93,7 @@ items: state: barrier entity-renderer: item: default:sofa - yaw: 90 + rotation: 90 facing=north,shape=straight: state: barrier entity-renderer: @@ -102,12 +102,12 @@ items: state: barrier entity-renderer: item: default:sofa - yaw: 180 + rotation: 180 facing=west,shape=straight: state: barrier entity-renderer: item: default:sofa - yaw: 270 + rotation: 270 facing=east,shape=inner_left: state: barrier entity-renderer: @@ -116,22 +116,22 @@ items: state: barrier entity-renderer: item: default:sofa_inner - yaw: 270 + rotation: 270 facing=south,shape=inner_left: state: barrier entity-renderer: item: default:sofa_inner - yaw: 90 + rotation: 90 facing=west,shape=inner_left: state: barrier entity-renderer: item: default:sofa_inner - yaw: 180 + rotation: 180 facing=east,shape=inner_right: state: barrier entity-renderer: item: default:sofa_inner - yaw: 90 + rotation: 90 facing=north,shape=inner_right: state: barrier entity-renderer: @@ -140,12 +140,12 @@ items: state: barrier entity-renderer: item: default:sofa_inner - yaw: 180 + rotation: 180 facing=west,shape=inner_right: state: barrier entity-renderer: item: default:sofa_inner - yaw: 270 + rotation: 270 variants: facing=east,shape=inner_left: appearance: facing=east,shape=inner_left diff --git a/common-files/src/main/resources/resources/default/configuration/templates/block_states.yml b/common-files/src/main/resources/resources/default/configuration/templates/block_states.yml index 3550304a2..c254f682e 100644 --- a/common-files/src/main/resources/resources/default/configuration/templates/block_states.yml +++ b/common-files/src/main/resources/resources/default/configuration/templates/block_states.yml @@ -1834,354 +1834,354 @@ templates: state: ${base_block}[east=false,north=false,south=false,waterlogged=false,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 east=true,north=false,south=false,waterlogged=false,west=false: state: ${base_block}[east=true,north=false,south=false,waterlogged=false,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=false,north=true,south=false,waterlogged=false,west=false: state: ${base_block}[east=false,north=true,south=false,waterlogged=false,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 180 + rotation: 180 east=false,north=false,south=true,waterlogged=false,west=false: state: ${base_block}[east=false,north=false,south=true,waterlogged=false,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 east=false,north=false,south=false,waterlogged=false,west=true: state: ${base_block}[east=false,north=false,south=false,waterlogged=false,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 east=true,north=true,south=false,waterlogged=false,west=false: state: ${base_block}[east=true,north=true,south=false,waterlogged=false,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 180 + rotation: 180 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=true,north=false,south=true,waterlogged=false,west=false: state: ${base_block}[east=true,north=false,south=true,waterlogged=false,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=true,north=false,south=false,waterlogged=false,west=true: state: ${base_block}[east=true,north=false,south=false,waterlogged=false,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=false,north=true,south=true,waterlogged=false,west=false: state: ${base_block}[east=false,north=true,south=true,waterlogged=false,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 180 + rotation: 180 east=false,north=true,south=false,waterlogged=false,west=true: state: ${base_block}[east=false,north=true,south=false,waterlogged=false,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 180 + rotation: 180 east=false,north=false,south=true,waterlogged=false,west=true: state: ${base_block}[east=false,north=false,south=true,waterlogged=false,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 east=true,north=true,south=true,waterlogged=false,west=false: state: ${base_block}[east=true,north=true,south=true,waterlogged=false,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 180 + rotation: 180 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=true,north=true,south=false,waterlogged=false,west=true: state: ${base_block}[east=true,north=true,south=false,waterlogged=false,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 180 + rotation: 180 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=true,north=false,south=true,waterlogged=false,west=true: state: ${base_block}[east=true,north=false,south=true,waterlogged=false,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=false,north=true,south=true,waterlogged=false,west=true: state: ${base_block}[east=false,north=true,south=true,waterlogged=false,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 180 + rotation: 180 east=true,north=true,south=true,waterlogged=false,west=true: state: ${base_block}[east=true,north=true,south=true,waterlogged=false,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 180 + rotation: 180 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=false,north=false,south=false,waterlogged=true,west=false: state: ${base_block}[east=false,north=false,south=false,waterlogged=true,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 east=true,north=false,south=false,waterlogged=true,west=false: state: ${base_block}[east=true,north=false,south=false,waterlogged=true,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=false,north=true,south=false,waterlogged=true,west=false: state: ${base_block}[east=false,north=true,south=false,waterlogged=true,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 180 + rotation: 180 east=false,north=false,south=true,waterlogged=true,west=false: state: ${base_block}[east=false,north=false,south=true,waterlogged=true,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 east=false,north=false,south=false,waterlogged=true,west=true: state: ${base_block}[east=false,north=false,south=false,waterlogged=true,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 east=true,north=true,south=false,waterlogged=true,west=false: state: ${base_block}[east=true,north=true,south=false,waterlogged=true,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 180 + rotation: 180 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=true,north=false,south=true,waterlogged=true,west=false: state: ${base_block}[east=true,north=false,south=true,waterlogged=true,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=true,north=false,south=false,waterlogged=true,west=true: state: ${base_block}[east=true,north=false,south=false,waterlogged=true,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=false,north=true,south=true,waterlogged=true,west=false: state: ${base_block}[east=false,north=true,south=true,waterlogged=true,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 180 + rotation: 180 east=false,north=true,south=false,waterlogged=true,west=true: state: ${base_block}[east=false,north=true,south=false,waterlogged=true,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 180 + rotation: 180 east=false,north=false,south=true,waterlogged=true,west=true: state: ${base_block}[east=false,north=false,south=true,waterlogged=true,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 east=true,north=true,south=true,waterlogged=true,west=false: state: ${base_block}[east=true,north=true,south=true,waterlogged=true,west=false] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 180 + rotation: 180 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=true,north=true,south=false,waterlogged=true,west=true: state: ${base_block}[east=true,north=true,south=false,waterlogged=true,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 180 + rotation: 180 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=true,north=false,south=true,waterlogged=true,west=true: state: ${base_block}[east=true,north=false,south=true,waterlogged=true,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 270 + rotation: 270 east=false,north=true,south=true,waterlogged=true,west=true: state: ${base_block}[east=false,north=true,south=true,waterlogged=true,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 180 + rotation: 180 east=true,north=true,south=true,waterlogged=true,west=true: state: ${base_block}[east=true,north=true,south=true,waterlogged=true,west=true] entity-renderer: - item: ${fence_post_item} - yaw: 180 + rotation: 180 scale: 1.0003 translation: 0,0.0001,0 - item: ${fence_side_item} - yaw: 0 + rotation: 0 - item: ${fence_side_item} - yaw: 90 + rotation: 90 - item: ${fence_side_item} - yaw: 180 + rotation: 180 - item: ${fence_side_item} - yaw: 270 + rotation: 270 variants: waterlogged=true: settings: diff --git a/common-files/src/main/resources/translations/en.yml b/common-files/src/main/resources/translations/en.yml index f38c9d06b..de5be911f 100644 --- a/common-files/src/main/resources/translations/en.yml +++ b/common-files/src/main/resources/translations/en.yml @@ -74,6 +74,7 @@ command.send_resource_pack.success.multiple: "Send resource packs to Invalid locale format: " command.locale.set.success: "Updated selected locale to for " command.locale.unset.success: "Cleared selected locale for " +command.entity_view_distance_scale.set.success: "Updated entity view distance scale to for " warning.network.resource_pack.unverified_uuid: "Player is attempting to request a resource pack using a UUID () that is not authenticated by the server." warning.config.pack.duplicated_files: "Duplicated files Found. Please resolve them through config.yml 'resource-pack.duplicated-files-handler' section." warning.config.yaml.duplicated_key: "Issue found in file - Found duplicated key '' at line , this might cause unexpected results." diff --git a/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockManager.java b/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockManager.java index 1b0b09185..5b0dbf02b 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockManager.java +++ b/core/src/main/java/net/momirealms/craftengine/core/block/AbstractBlockManager.java @@ -31,6 +31,7 @@ import net.momirealms.craftengine.core.plugin.context.Context; import net.momirealms.craftengine.core.plugin.context.event.EventFunctions; import net.momirealms.craftengine.core.plugin.context.event.EventTrigger; import net.momirealms.craftengine.core.plugin.context.function.Function; +import net.momirealms.craftengine.core.plugin.entityculling.CullingData; import net.momirealms.craftengine.core.plugin.locale.LocalizedException; import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException; import net.momirealms.craftengine.core.plugin.logger.Debugger; @@ -38,6 +39,7 @@ import net.momirealms.craftengine.core.registry.BuiltInRegistries; import net.momirealms.craftengine.core.registry.Holder; import net.momirealms.craftengine.core.registry.WritableRegistry; import net.momirealms.craftengine.core.util.*; +import net.momirealms.craftengine.core.world.collision.AABB; import net.momirealms.sparrow.nbt.CompoundTag; import org.incendo.cloud.suggestion.Suggestion; import org.jetbrains.annotations.NotNull; @@ -52,6 +54,7 @@ import java.util.concurrent.ExecutionException; public abstract class AbstractBlockManager extends AbstractModelGenerator implements BlockManager { private static final JsonElement EMPTY_VARIANT_MODEL = MiscUtils.init(new JsonObject(), o -> o.addProperty("model", "minecraft:block/empty")); + private static final AABB DEFAULT_BLOCK_ENTITY_AABB = new AABB(-.5, -.5, -.5, .5, .5, .5); protected final BlockParser blockParser; protected final BlockStateMappingParser blockStateMappingParser; // 根据id获取自定义方块 @@ -603,7 +606,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem BlockStateAppearance blockStateAppearance = new BlockStateAppearance( visualBlockState, parseBlockEntityRender(appearanceSection.get("entity-renderer")), - ResourceConfigUtils.getAsAABB(appearanceSection.getOrDefault("aabb", 1), "aabb") + parseCullingData(appearanceSection.get("entity-culling")) ); appearances.put(appearanceName, blockStateAppearance); if (anyAppearance == null) { @@ -643,7 +646,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem } for (ImmutableBlockState possibleState : possibleStates) { possibleState.setVisualBlockState(appearance.blockState()); - possibleState.setEstimatedBoundingBox(appearance.estimateAABB()); + possibleState.setCullingData(appearance.cullingData()); appearance.blockEntityRenderer().ifPresent(possibleState::setConstantRenderers); } } @@ -667,7 +670,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem if (visualState == null) { visualState = anyAppearance.blockState(); state.setVisualBlockState(visualState); - state.setEstimatedBoundingBox(anyAppearance.estimateAABB()); + state.setCullingData(anyAppearance.cullingData()); anyAppearance.blockEntityRenderer().ifPresent(state::setConstantRenderers); } int appearanceId = visualState.registryId(); @@ -707,6 +710,22 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem }, () -> GsonHelper.get().toJson(section))); } + private CullingData parseCullingData(Object arguments) { + if (arguments instanceof Boolean b && !b) { + return null; + } + if (!(arguments instanceof Map)) { + return new CullingData(DEFAULT_BLOCK_ENTITY_AABB, Config.entityCullingViewDistance(), 0.5, true); + } + Map argumentsMap = ResourceConfigUtils.getAsMap(arguments, "entity-culling"); + return new CullingData( + ResourceConfigUtils.getAsAABB(argumentsMap.getOrDefault("aabb", 1), "aabb"), + ResourceConfigUtils.getAsInt(argumentsMap.getOrDefault("view-distance", Config.entityCullingViewDistance()), "view-distance"), + ResourceConfigUtils.getAsDouble(argumentsMap.getOrDefault("aabb-expansion", 0.5), "aabb-expansion"), + ResourceConfigUtils.getAsBoolean(argumentsMap.getOrDefault("ray-tracing", true), "ray-tracing") + ); + } + @SuppressWarnings("unchecked") private Optional[]> parseBlockEntityRender(Object arguments) { if (arguments == null) return Optional.empty(); diff --git a/core/src/main/java/net/momirealms/craftengine/core/block/BlockStateAppearance.java b/core/src/main/java/net/momirealms/craftengine/core/block/BlockStateAppearance.java index d2b8d0189..d3e7f8c85 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/block/BlockStateAppearance.java +++ b/core/src/main/java/net/momirealms/craftengine/core/block/BlockStateAppearance.java @@ -2,11 +2,12 @@ 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 net.momirealms.craftengine.core.plugin.entityculling.CullingData; +import org.jetbrains.annotations.Nullable; import java.util.Optional; public record BlockStateAppearance(BlockStateWrapper blockState, Optional[]> blockEntityRenderer, - AABB estimateAABB) { + @Nullable CullingData cullingData) { } diff --git a/core/src/main/java/net/momirealms/craftengine/core/block/ImmutableBlockState.java b/core/src/main/java/net/momirealms/craftengine/core/block/ImmutableBlockState.java index 9a4010ef8..a112628f5 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/block/ImmutableBlockState.java +++ b/core/src/main/java/net/momirealms/craftengine/core/block/ImmutableBlockState.java @@ -13,11 +13,11 @@ import net.momirealms.craftengine.core.item.Item; import net.momirealms.craftengine.core.loot.LootTable; import net.momirealms.craftengine.core.plugin.context.ContextHolder; import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextParameters; +import net.momirealms.craftengine.core.plugin.entityculling.CullingData; 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; @@ -43,7 +43,8 @@ public final class ImmutableBlockState { private BlockEntityType blockEntityType; @Nullable private BlockEntityElementConfig[] renderers; - private AABB estimatedBoundingBox; + @Nullable + private CullingData cullingData; ImmutableBlockState( Holder.Reference owner, @@ -89,12 +90,13 @@ public final class ImmutableBlockState { this.renderers = renderers; } - public void setEstimatedBoundingBox(AABB aabb) { - this.estimatedBoundingBox = aabb; + @Nullable + public CullingData cullingData() { + return cullingData; } - public AABB estimatedBoundingBox() { - return estimatedBoundingBox; + public void setCullingData(@Nullable CullingData cullingData) { + this.cullingData = cullingData; } public boolean hasBlockEntity() { diff --git a/core/src/main/java/net/momirealms/craftengine/core/block/entity/render/ConstantBlockEntityRenderer.java b/core/src/main/java/net/momirealms/craftengine/core/block/entity/render/ConstantBlockEntityRenderer.java index 4551ed667..461331e96 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/block/entity/render/ConstantBlockEntityRenderer.java +++ b/core/src/main/java/net/momirealms/craftengine/core/block/entity/render/ConstantBlockEntityRenderer.java @@ -2,18 +2,19 @@ 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.plugin.entityculling.CullingData; import net.momirealms.craftengine.core.world.Cullable; -import net.momirealms.craftengine.core.world.collision.AABB; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; @ApiStatus.Experimental public class ConstantBlockEntityRenderer implements Cullable { private final BlockEntityElement[] elements; - public final AABB aabb; + public final CullingData cullingData; - public ConstantBlockEntityRenderer(BlockEntityElement[] elements, AABB aabb) { + public ConstantBlockEntityRenderer(BlockEntityElement[] elements, @Nullable CullingData cullingData) { this.elements = elements; - this.aabb = aabb; + this.cullingData = cullingData; } @Override @@ -55,7 +56,11 @@ public class ConstantBlockEntityRenderer implements Cullable { } @Override - public AABB aabb() { - return this.aabb; + public CullingData cullingData() { + return this.cullingData; + } + + public boolean canCull() { + return this.cullingData != null; } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/block/entity/render/element/BlockEntityElementConfig.java b/core/src/main/java/net/momirealms/craftengine/core/block/entity/render/element/BlockEntityElementConfig.java index 0d3c320ad..99346e5bc 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/block/entity/render/element/BlockEntityElementConfig.java +++ b/core/src/main/java/net/momirealms/craftengine/core/block/entity/render/element/BlockEntityElementConfig.java @@ -11,5 +11,9 @@ public interface BlockEntityElementConfig { return null; } + default E createExact(World world, BlockPos pos, E previous) { + return null; + } + Class elementClass(); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/block/entity/tick/TickingBlockEntityImpl.java b/core/src/main/java/net/momirealms/craftengine/core/block/entity/tick/TickingBlockEntityImpl.java index 12d09ee8c..85d4a1aba 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/block/entity/tick/TickingBlockEntityImpl.java +++ b/core/src/main/java/net/momirealms/craftengine/core/block/entity/tick/TickingBlockEntityImpl.java @@ -32,7 +32,7 @@ public class TickingBlockEntityImpl implements TickingBlo // 不是合法方块 if (!this.blockEntity.isValidBlockState(state)) { this.chunk.removeBlockEntity(pos); - Debugger.BLOCK_ENTITY.warn(() -> "Invalid block entity(" + this.blockEntity.getClass().getSimpleName() + ") with state " + state + " found at world " + this.chunk.world().name() + " " + pos, null); + Debugger.BLOCK.warn(() -> "Invalid block entity(" + this.blockEntity.getClass().getSimpleName() + ") with state " + state + " found at world " + this.chunk.world().name() + " " + pos, null); return; } try { diff --git a/core/src/main/java/net/momirealms/craftengine/core/entity/player/Player.java b/core/src/main/java/net/momirealms/craftengine/core/entity/player/Player.java index 36e3fcb76..847fe6d1c 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/entity/player/Player.java +++ b/core/src/main/java/net/momirealms/craftengine/core/entity/player/Player.java @@ -38,6 +38,8 @@ public abstract class Player extends AbstractEntity implements NetWorkUser { public abstract void setClientSideWorld(World world); + public abstract void entityCullingTick(); + public abstract float getDestroyProgress(Object blockState, BlockPos pos); public abstract void setClientSideCanBreakBlock(boolean canBreak); @@ -189,6 +191,8 @@ public abstract class Player extends AbstractEntity implements NetWorkUser { public abstract void setSelectedLocale(@Nullable Locale locale); + public abstract void setEntityCullingViewDistanceScale(double value); + public abstract void giveExperiencePoints(int xpPoints); public abstract void giveExperienceLevels(int levels); @@ -214,4 +218,6 @@ public abstract class Player extends AbstractEntity implements NetWorkUser { @Override public void remove() { } + + public abstract WorldPosition eyePosition(); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java index 2c32adc8b..3075ea81e 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/CraftEngine.java @@ -26,6 +26,8 @@ import net.momirealms.craftengine.core.plugin.dependency.Dependencies; import net.momirealms.craftengine.core.plugin.dependency.Dependency; import net.momirealms.craftengine.core.plugin.dependency.DependencyManager; import net.momirealms.craftengine.core.plugin.dependency.DependencyManagerImpl; +import net.momirealms.craftengine.core.plugin.entityculling.EntityCullingManager; +import net.momirealms.craftengine.core.plugin.entityculling.EntityCullingManagerImpl; import net.momirealms.craftengine.core.plugin.gui.GuiManager; import net.momirealms.craftengine.core.plugin.gui.category.ItemBrowserManager; import net.momirealms.craftengine.core.plugin.gui.category.ItemBrowserManagerImpl; @@ -79,6 +81,7 @@ public abstract class CraftEngine implements Plugin { protected GlobalVariableManager globalVariableManager; protected ProjectileManager projectileManager; protected SeatManager seatManager; + protected EntityCullingManager entityCullingManager; private final PluginTaskRegistry beforeEnableTaskRegistry = new PluginTaskRegistry(); private final PluginTaskRegistry afterEnableTaskRegistry = new PluginTaskRegistry(); @@ -118,6 +121,8 @@ public abstract class CraftEngine implements Plugin { this.globalVariableManager = new GlobalVariableManager(); // 初始化物品浏览器 this.itemBrowserManager = new ItemBrowserManagerImpl(this); + // 初始化实体剔除器 + this.entityCullingManager = new EntityCullingManagerImpl(); } public void setUpConfigAndLocale() { @@ -158,6 +163,7 @@ public abstract class CraftEngine implements Plugin { this.advancementManager.reload(); this.projectileManager.reload(); this.seatManager.reload(); + this.entityCullingManager.reload(); } private void runDelayTasks(boolean reloadRecipe) { @@ -349,6 +355,7 @@ public abstract class CraftEngine implements Plugin { if (this.translationManager != null) this.translationManager.disable(); if (this.globalVariableManager != null) this.globalVariableManager.disable(); if (this.projectileManager != null) this.projectileManager.disable(); + if (this.entityCullingManager != null) this.entityCullingManager.disable(); if (this.scheduler != null) this.scheduler.shutdownScheduler(); if (this.scheduler != null) this.scheduler.shutdownExecutor(); if (this.commandManager != null) this.commandManager.unregisterFeatures(); diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java index 3720a4268..df09a8b08 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/config/Config.java @@ -54,6 +54,8 @@ public class Config { protected boolean debug$item; protected boolean debug$furniture; protected boolean debug$resource_pack; + protected boolean debug$block; + protected boolean debug$entity_culling; protected boolean resource_pack$remove_tinted_leaves_particle; protected boolean resource_pack$generate_mod_assets; @@ -204,6 +206,12 @@ public class Config { protected int emoji$max_emojis_per_parse; protected boolean client_optimization$entity_culling$enable; + protected int client_optimization$entity_culling$view_distance; + protected int client_optimization$entity_culling$threads; + protected boolean client_optimization$entity_culling$ray_tracing; + protected boolean client_optimization$entity_culling$rate_limiting$enable; + protected int client_optimization$entity_culling$rate_limiting$bucket_size; + protected int client_optimization$entity_culling$rate_limiting$restore_per_tick; public Config(CraftEngine plugin) { this.plugin = plugin; @@ -307,6 +315,8 @@ public class Config { debug$item = config.getBoolean("debug.item", false); debug$furniture = config.getBoolean("debug.furniture", false); debug$resource_pack = config.getBoolean("debug.resource-pack", false); + debug$block = config.getBoolean("debug.block", false); + debug$entity_culling = config.getBoolean("debug.entity-culling", false); // resource pack resource_pack$path = resolvePath(config.getString("resource-pack.path", "./generated/resource_pack.zip")); @@ -565,7 +575,15 @@ public class Config { 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); + if (firstTime) { + client_optimization$entity_culling$enable = VersionHelper.PREMIUM && config.getBoolean("client-optimization.entity-culling.enable", false); + } + client_optimization$entity_culling$view_distance = config.getInt("client-optimization.entity-culling.view-distance", 64); + client_optimization$entity_culling$threads = config.getInt("client-optimization.entity-culling.threads", 1); + client_optimization$entity_culling$ray_tracing = client_optimization$entity_culling$enable && config.getBoolean("client-optimization.entity-culling.ray-tracing", true); + client_optimization$entity_culling$rate_limiting$enable = config.getBoolean("client-optimization.entity-culling.rate-limiting.enable", true); + client_optimization$entity_culling$rate_limiting$bucket_size = config.getInt("client-optimization.entity-culling.rate-limiting.bucket-size", 300); + client_optimization$entity_culling$rate_limiting$restore_per_tick = config.getInt("client-optimization.entity-culling.rate-limiting.restore-per-tick", 5); firstTime = false; } @@ -604,12 +622,12 @@ public class Config { return instance.debug$item; } - public static boolean debugBlockEntity() { - return false; + public static boolean debugBlock() { + return instance.debug$block; } - public static boolean debugBlock() { - return false; + public static boolean debugEntityCulling() { + return instance.debug$entity_culling; } public static boolean debugFurniture() { @@ -1161,6 +1179,30 @@ public class Config { return instance.client_optimization$entity_culling$enable; } + public static int entityCullingViewDistance() { + return instance.client_optimization$entity_culling$view_distance; + } + + public static int entityCullingThreads() { + return instance.client_optimization$entity_culling$threads; + } + + public static boolean enableEntityCullingRateLimiting() { + return instance.client_optimization$entity_culling$rate_limiting$enable; + } + + public static int entityCullingRateLimitingBucketSize() { + return instance.client_optimization$entity_culling$rate_limiting$bucket_size; + } + + public static int entityCullingRateLimitingRestorePerTick() { + return instance.client_optimization$entity_culling$rate_limiting$restore_per_tick; + } + + public static boolean entityCullingRayTracing() { + return instance.client_optimization$entity_culling$ray_tracing; + } + public YamlDocument loadOrCreateYamlData(String fileName) { Path path = this.plugin.dataFolderPath().resolve(fileName); if (!Files.exists(path)) { diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/CullingData.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/CullingData.java new file mode 100644 index 000000000..67c7131f0 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/CullingData.java @@ -0,0 +1,33 @@ +package net.momirealms.craftengine.core.plugin.entityculling; + +import net.momirealms.craftengine.core.world.collision.AABB; + +public final class CullingData { + public final AABB aabb; + public final int maxDistance; + public final double aabbExpansion; + public final boolean rayTracing; + + public CullingData(AABB aabb, int maxDistance, double aabbExpansion, boolean rayTracing) { + this.aabb = aabb; + this.maxDistance = maxDistance; + this.aabbExpansion = aabbExpansion; + this.rayTracing = rayTracing; + } + + public AABB aabb() { + return this.aabb; + } + + public int maxDistance() { + return this.maxDistance; + } + + public double aabbExpansion() { + return this.aabbExpansion; + } + + public boolean rayTracing() { + return this.rayTracing; + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCulling.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCulling.java index 2b16695bf..5ced80c59 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCulling.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCulling.java @@ -1,6 +1,7 @@ package net.momirealms.craftengine.core.plugin.entityculling; import net.momirealms.craftengine.core.entity.player.Player; +import net.momirealms.craftengine.core.plugin.config.Config; import net.momirealms.craftengine.core.util.MiscUtils; import net.momirealms.craftengine.core.world.ChunkPos; import net.momirealms.craftengine.core.world.MutableVec3d; @@ -13,38 +14,58 @@ import java.util.Arrays; 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 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 final int[] lastHitBlock = new int[3]; + private boolean canCheckLastHitBlock = false; private int hitBlockCount = 0; private int lastVisitChunkX = Integer.MAX_VALUE; private int lastVisitChunkZ = Integer.MAX_VALUE; private ClientChunk lastVisitChunk = null; + private int currentTokens = Config.entityCullingRateLimitingBucketSize(); + private double distanceScale = 1d; - public EntityCulling(Player player, int maxDistance, double aabbExpansion) { + public EntityCulling(Player player) { this.player = player; - this.maxDistance = maxDistance; - this.aabbExpansion = aabbExpansion; for (int i = 0; i < MAX_SAMPLES; i++) { this.targetPoints[i] = new MutableVec3d(0,0,0); } } - public boolean isVisible(AABB aabb, Vec3d cameraPos) { + public void setDistanceScale(double distanceScale) { + this.distanceScale = distanceScale; + } + + public double distanceScale() { + return distanceScale; + } + + public void restoreTokenOnTick() { + this.currentTokens = Math.min(Config.entityCullingRateLimitingBucketSize(), this.currentTokens + Config.entityCullingRateLimitingRestorePerTick()); + } + + public boolean takeToken() { + if (this.currentTokens > 0) { + this.currentTokens--; + return true; + } + return false; + } + + public boolean isVisible(CullingData cullable, Vec3d cameraPos, boolean rayTracing) { // 情空标志位 - Arrays.fill(this.canCheckLastHitBlock, false); + this.canCheckLastHitBlock = false; this.hitBlockCount = 0; + AABB aabb = cullable.aabb; + double aabbExpansion = cullable.aabbExpansion; // 根据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); + int minX = MiscUtils.floor(aabb.minX - aabbExpansion); + int minY = MiscUtils.floor(aabb.minY - aabbExpansion); + int minZ = MiscUtils.floor(aabb.minZ - aabbExpansion); + int maxX = MiscUtils.ceil(aabb.maxX + aabbExpansion); + int maxY = MiscUtils.ceil(aabb.maxY + aabbExpansion); + int maxZ = MiscUtils.ceil(aabb.maxZ + aabbExpansion); double cameraX = cameraPos.x; double cameraY = cameraPos.y; @@ -60,7 +81,8 @@ public final class EntityCulling { } // 如果设置了最大距离 - if (this.maxDistance > 0) { + double maxDistance = cullable.maxDistance * this.distanceScale; + if (maxDistance > 0) { // 计算AABB到相机的最小距离 double distanceSq = 0.0; // 计算XYZ轴方向的距离 @@ -68,13 +90,17 @@ public final class EntityCulling { distanceSq += distanceSq(minY, maxY, cameraY, relY); distanceSq += distanceSq(minZ, maxZ, cameraZ, relZ); // 检查距离是否超过最大值 - double maxDistanceSq = this.maxDistance * this.maxDistance; + double maxDistanceSq = maxDistance * maxDistance; // 超过最大距离,剔除 if (distanceSq > maxDistanceSq) { return false; } } + if (!rayTracing || !cullable.rayTracing) { + return true; + } + // 清空之前的缓存 Arrays.fill(this.dotSelectors, false); if (relX == Relative.POSITIVE) { @@ -94,24 +120,24 @@ public final class EntityCulling { } 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 (this.dotSelectors[0]) targetPoints[size++].set(minX + 0.05, minY + 0.05, minZ + 0.05); + if (this.dotSelectors[1]) targetPoints[size++].set(maxX - 0.05, minY + 0.05, minZ + 0.05); + if (this.dotSelectors[2]) targetPoints[size++].set(minX + 0.05, minY + 0.05, maxZ - 0.05); + if (this.dotSelectors[3]) targetPoints[size++].set(maxX - 0.05, minY + 0.05, maxZ - 0.05); + if (this.dotSelectors[4]) targetPoints[size++].set(minX + 0.05, maxY - 0.05, minZ + 0.05); + if (this.dotSelectors[5]) targetPoints[size++].set(maxX - 0.05, maxY - 0.05, minZ + 0.05); + if (this.dotSelectors[6]) targetPoints[size++].set(minX + 0.05, maxY - 0.05, maxZ - 0.05); + if (this.dotSelectors[7]) targetPoints[size++].set(maxX - 0.05, maxY - 0.05, maxZ - 0.05); // 面中心点 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); + if (this.dotSelectors[8]) targetPoints[size++].set(averageX, averageY, minZ + 0.05); + if (this.dotSelectors[9]) targetPoints[size++].set(averageX, averageY, maxZ - 0.05); + if (this.dotSelectors[10]) targetPoints[size++].set(minX + 0.05, averageY, averageZ); + if (this.dotSelectors[11]) targetPoints[size++].set(maxX - 0.05, averageY, averageZ); + if (this.dotSelectors[12]) targetPoints[size++].set(averageX, minY + 0.05, averageZ); + if (this.dotSelectors[13]) targetPoints[size].set(averageX, maxY - 0.05, averageZ); return isVisible(cameraPos, this.targetPoints, size); } @@ -168,7 +194,7 @@ public final class EntityCulling { int startBlockZ = MiscUtils.floor(start.z); // 遍历所有目标点进行视线检测 - outer: for (int targetIndex = 0; targetIndex < targetCount; targetIndex++) { + for (int targetIndex = 0; targetIndex < targetCount; targetIndex++) { MutableVec3d currentTarget = targets[targetIndex]; // 计算起点到目标的相对向量(世界坐标差) @@ -177,14 +203,9 @@ public final class EntityCulling { 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; + if (this.canCheckLastHitBlock) { + if (rayIntersection(this.lastHitBlock[0], this.lastHitBlock[1], this.lastHitBlock[2], start, new MutableVec3d(deltaX, deltaY, deltaZ).normalize())) { + continue; } } @@ -195,9 +216,9 @@ public final class EntityCulling { // 预计算每单位距离在各方块边界上的步进增量 // 这些值表示射线穿过一个方块所需的时间分数 - double stepIncrementX = 1.0 / (absDeltaX + 1e-10); // 避免除0 - double stepIncrementY = 1.0 / (absDeltaY + 1e-10); - double stepIncrementZ = 1.0 / (absDeltaZ + 1e-10); + double stepIncrementX = 1.0 / absDeltaX; + double stepIncrementY = 1.0 / absDeltaY; + double stepIncrementZ = 1.0 / absDeltaZ; // 射线将穿过的总方块数量(包括起点和终点) int totalBlocksToCheck = 1; @@ -270,21 +291,25 @@ public final class EntityCulling { if (isLineOfSightClear) { return true; } else { - this.canCheckLastHitBlock[this.hitBlockCount++] = true; + this.canCheckLastHitBlock = true; } } return false; } - private boolean stepRay(int currentBlockX, int currentBlockY, int currentBlockZ, + private boolean stepRay(int startingX, int startingY, int startingZ, double stepSizeX, double stepSizeY, double stepSizeZ, - int remainingSteps, int stepDirectionX, int stepDirectionY, - int stepDirectionZ, double nextStepTimeY, double nextStepTimeX, - double nextStepTimeZ) { + int remainingSteps, + int stepDirectionX, int stepDirectionY, int stepDirectionZ, + double nextStepTimeY, double nextStepTimeX, double nextStepTimeZ) { - // 遍历射线路径上的所有方块(跳过最后一个目标方块) - for (; remainingSteps > 1; remainingSteps--) { + int currentBlockX = startingX; + int currentBlockY = startingY; + int currentBlockZ = startingZ; + + // 遍历射线路径上的所有方块 + for (; remainingSteps > 0; remainingSteps--) { // 检查当前方块是否遮挡视线 if (isOccluding(currentBlockX, currentBlockY, currentBlockZ)) { @@ -315,6 +340,22 @@ public final class EntityCulling { return true; } + private int getCacheIndex(int x, int y, int z, int startX, int startY, int startZ) { + int deltaX = startX + 16 - x; + if (deltaX < 0 || deltaX >= 32) { + return -1; + } + int deltaY = startY + 16 - y; + if (deltaY < 0 || deltaY >= 32) { + return -1; + } + int deltaZ = startZ + 16 - z; + if (deltaZ < 0 || deltaZ >= 32) { + return -1; + } + return deltaX + 32 * deltaY + 32 * 32 * deltaZ; + } + private double distanceSq(int min, int max, double camera, Relative rel) { if (rel == Relative.NEGATIVE) { double dx = camera - max; diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCullingManager.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCullingManager.java new file mode 100644 index 000000000..123cfbdee --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCullingManager.java @@ -0,0 +1,6 @@ +package net.momirealms.craftengine.core.plugin.entityculling; + +import net.momirealms.craftengine.core.plugin.Manageable; + +public interface EntityCullingManager extends Manageable { +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCullingManagerImpl.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCullingManagerImpl.java new file mode 100644 index 000000000..e01602e81 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCullingManagerImpl.java @@ -0,0 +1,32 @@ +package net.momirealms.craftengine.core.plugin.entityculling; + +import net.momirealms.craftengine.core.plugin.config.Config; + +import java.util.ArrayList; +import java.util.List; + +public class EntityCullingManagerImpl implements EntityCullingManager { + private final List threads = new ArrayList<>(); + + @Override + public void load() { + if (Config.enableEntityCulling()) { + int threads = Math.min(64, Math.max(Config.entityCullingThreads(), 1)); + for (int i = 0; i < threads; i++) { + EntityCullingThread thread = new EntityCullingThread(i, threads); + this.threads.add(thread); + thread.start(); + } + } + } + + @Override + public void unload() { + if (!this.threads.isEmpty()) { + for (EntityCullingThread thread : this.threads) { + thread.stop(); + } + this.threads.clear(); + } + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCullingThread.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCullingThread.java new file mode 100644 index 000000000..7eb6f1e69 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCullingThread.java @@ -0,0 +1,67 @@ +package net.momirealms.craftengine.core.plugin.entityculling; + +import net.momirealms.craftengine.core.entity.player.Player; +import net.momirealms.craftengine.core.plugin.CraftEngine; +import net.momirealms.craftengine.core.plugin.config.Config; +import net.momirealms.craftengine.core.plugin.logger.Debugger; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +public class EntityCullingThread { + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private final AtomicBoolean isRunning = new AtomicBoolean(false); + private final int id; + private final int threads; + private int timer; + + public EntityCullingThread(int id, int threads) { + this.id = id; + this.threads = threads; + } + + public void start() { + // 错开线程启动时间,避免所有线程同时执行 + long initialDelay = this.id * (50L / this.threads); + this.scheduler.scheduleAtFixedRate(this::scheduleTask, initialDelay, 50, TimeUnit.MILLISECONDS); + } + + private void scheduleTask() { + // 使用CAS操作,更安全 + if (!this.isRunning.compareAndSet(false, true)) { + return; + } + + this.scheduler.execute(() -> { + try { + int processed = 0; + long startTime = System.nanoTime(); + + for (Player player : CraftEngine.instance().networkManager().onlineUsers()) { + // 使用绝对值确保非负,使用 threads 而不是 threads-1 确保均匀分布 + if (Math.abs(player.uuid().hashCode()) % this.threads == this.id) { + player.entityCullingTick(); + processed++; + } + } + + long duration = System.nanoTime() - startTime; + if (Config.debugEntityCulling() && this.timer++ % 20 == 0) { + String value = String.format("EntityCullingThread-%d processed %d players in %sms", + this.id, processed, String.format("%.2f", duration / 1_000_000.0)); + Debugger.ENTITY_CULLING.debug(() -> value); + } + } catch (Exception e) { + CraftEngine.instance().logger().severe("Failed to run entity culling tick: " + e.getMessage()); + } finally { + this.isRunning.set(false); + } + }); + } + + public void stop() { + this.scheduler.shutdown(); + } +} \ No newline at end of file diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/VoxelIterator.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/VoxelIterator.java deleted file mode 100644 index a77160fa1..000000000 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/VoxelIterator.java +++ /dev/null @@ -1,216 +0,0 @@ -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 { - 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; - } -} diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/MessageConstants.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/MessageConstants.java index b488bde08..81174c5d0 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/MessageConstants.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/locale/MessageConstants.java @@ -41,4 +41,5 @@ public interface MessageConstants { TranslatableComponent.Builder COMMAND_ITEM_CLEAR_FAILED_MULTIPLE = Component.translatable().key("command.item.clear.failed.multiple"); TranslatableComponent.Builder COMMAND_ITEM_CLEAR_TEST_SINGLE = Component.translatable().key("command.item.clear.test.single"); TranslatableComponent.Builder COMMAND_ITEM_CLEAR_TEST_MULTIPLE = Component.translatable().key("command.item.clear.test.multiple"); + TranslatableComponent.Builder COMMAND_ENTITY_VIEW_DISTANCE_SCALE_SET_SUCCESS = Component.translatable().key("command.entity_view_distance_scale.set.success"); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/logger/Debugger.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/logger/Debugger.java index 49dcb3ef7..3dd1cf3f2 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/logger/Debugger.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/logger/Debugger.java @@ -12,7 +12,7 @@ public enum Debugger { RESOURCE_PACK(Config::debugResourcePack), ITEM(Config::debugItem), BLOCK(Config::debugBlock), - BLOCK_ENTITY(Config::debugBlockEntity); + ENTITY_CULLING(Config::debugEntityCulling); private final Supplier condition; diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/scheduler/AbstractJavaScheduler.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/scheduler/AbstractJavaScheduler.java index c728459ac..8e2ab4818 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/scheduler/AbstractJavaScheduler.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/scheduler/AbstractJavaScheduler.java @@ -6,6 +6,7 @@ import java.lang.Thread.UncaughtExceptionHandler; import java.util.Arrays; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -46,6 +47,13 @@ public abstract class AbstractJavaScheduler implements SchedulerAdapter { return new AsyncTask(future); } + @Override + public SchedulerTask asyncRepeating(Consumer task, long delay, long interval, TimeUnit unit) { + LazyAsyncTask asyncTask = new LazyAsyncTask(); + asyncTask.future = this.scheduler.scheduleAtFixedRate(() -> this.worker.execute(() -> task.accept(asyncTask)), delay, interval, unit); + return asyncTask; + } + @Override public void shutdownScheduler() { this.scheduler.shutdown(); diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/scheduler/LazyAsyncTask.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/scheduler/LazyAsyncTask.java new file mode 100644 index 000000000..84ad86199 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/scheduler/LazyAsyncTask.java @@ -0,0 +1,21 @@ +package net.momirealms.craftengine.core.plugin.scheduler; + +import java.util.concurrent.ScheduledFuture; + +public class LazyAsyncTask implements SchedulerTask { + + public ScheduledFuture future; + + @Override + public void cancel() { + if (future != null) { + future.cancel(false); + } + } + + @Override + public boolean cancelled() { + if (future == null) return false; + return future.isCancelled(); + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/plugin/scheduler/SchedulerAdapter.java b/core/src/main/java/net/momirealms/craftengine/core/plugin/scheduler/SchedulerAdapter.java index fd7af51e3..786ea491c 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/plugin/scheduler/SchedulerAdapter.java +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/scheduler/SchedulerAdapter.java @@ -2,6 +2,7 @@ package net.momirealms.craftengine.core.plugin.scheduler; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; public interface SchedulerAdapter { @@ -25,6 +26,8 @@ public interface SchedulerAdapter { SchedulerTask asyncRepeating(Runnable task, long delay, long interval, TimeUnit unit); + SchedulerTask asyncRepeating(Consumer task, long delay, long interval, TimeUnit unit); + void shutdownScheduler(); void shutdownExecutor(); diff --git a/core/src/main/java/net/momirealms/craftengine/core/world/Cullable.java b/core/src/main/java/net/momirealms/craftengine/core/world/Cullable.java index 700442fb3..404594503 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/world/Cullable.java +++ b/core/src/main/java/net/momirealms/craftengine/core/world/Cullable.java @@ -1,13 +1,15 @@ package net.momirealms.craftengine.core.world; import net.momirealms.craftengine.core.entity.player.Player; -import net.momirealms.craftengine.core.world.collision.AABB; +import net.momirealms.craftengine.core.plugin.entityculling.CullingData; +import org.jetbrains.annotations.Nullable; public interface Cullable { - AABB aabb(); - void show(Player player); void hide(Player player); + + @Nullable + CullingData cullingData(); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/world/chunk/CEChunk.java b/core/src/main/java/net/momirealms/craftengine/core/world/chunk/CEChunk.java index 4ca2fe5df..50eda3140 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/world/chunk/CEChunk.java +++ b/core/src/main/java/net/momirealms/craftengine/core/world/chunk/CEChunk.java @@ -12,6 +12,7 @@ import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityEl 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.entityculling.CullingData; import net.momirealms.craftengine.core.plugin.logger.Debugger; import net.momirealms.craftengine.core.world.*; import net.momirealms.craftengine.core.world.chunk.client.VirtualCullableObject; @@ -139,7 +140,12 @@ public class CEChunk { BlockEntityElementConfig[] renderers = state.constantRenderers(); if (renderers != null && renderers.length > 0) { BlockEntityElement[] elements = new BlockEntityElement[renderers.length]; - ConstantBlockEntityRenderer renderer = new ConstantBlockEntityRenderer(elements, state.estimatedBoundingBox().move(pos)); + ConstantBlockEntityRenderer renderer = new ConstantBlockEntityRenderer( + elements, + Optional.ofNullable(state.cullingData()) + .map(data -> new CullingData(data.aabb.move(pos), data.maxDistance, data.aabbExpansion, data.rayTracing)) + .orElse(null) + ); World wrappedWorld = this.world.world(); List trackedBy = getTrackedBy(); boolean hasTrackedBy = trackedBy != null && !trackedBy.isEmpty(); @@ -147,6 +153,12 @@ public class CEChunk { if (previous != null) { // 由于entity-render的体量基本都很小,所以考虑一个特殊情况,即前后都是1个renderer,对此情况进行简化和优化 BlockEntityElement[] previousElements = previous.elements().clone(); + + /* + * + * 1 对 1,命中率最高 + * + */ if (previousElements.length == 1 && renderers.length == 1) { BlockEntityElement previousElement = previousElements[0]; BlockEntityElementConfig config = renderers[0]; @@ -156,18 +168,19 @@ public class CEChunk { if (element != null) { elements[0] = element; if (hasTrackedBy) { - // 如果启用实体剔除,那么只对已经渲染的进行变换 - if (Config.enableEntityCulling()) { - for (Player player : trackedBy) { + for (Player player : trackedBy) { + // 如果启用剔除,则暂时保留原先可见度,因为大概率可见度不发生变化 + if (Config.enableEntityCulling()) { VirtualCullableObject trackedBlockEntity = player.getTrackedBlockEntity(pos); - if (trackedBlockEntity == null || trackedBlockEntity.isShown()) { + if (trackedBlockEntity == null || trackedBlockEntity.isShown) { element.transform(player); } - } - } - // 否则直接变换 - else { - for (Player player : trackedBy) { + if (trackedBlockEntity != null) { + trackedBlockEntity.setCullable(renderer); + } else { + player.addTrackedBlockEntity(pos, renderer); + } + } else { element.transform(player); } } @@ -178,44 +191,56 @@ public class CEChunk { BlockEntityElement element = config.create(wrappedWorld, pos); elements[0] = element; if (hasTrackedBy) { - // 如果启用实体剔除,那么只添加记录 - if (Config.enableEntityCulling()) { - for (Player player : trackedBy) { - player.addTrackedBlockEntity(pos, renderer); - } - } - // 否则直接显示 - else { - for (Player player : trackedBy) { + for (Player player : trackedBy) { + if (Config.enableEntityCulling()) { + VirtualCullableObject trackedBlockEntity = player.getTrackedBlockEntity(pos); + if (trackedBlockEntity != null) { + if (trackedBlockEntity.isShown) { + trackedBlockEntity.setShown(player, false); + } + trackedBlockEntity.setCullable(renderer); + } else { + player.addTrackedBlockEntity(pos, renderer); + } + } else { previousElement.hide(player); element.show(player); } } } } - } else { + } + /* + * + * 1 对 多, 多 对 多 + * + */ + else { + + VirtualCullableObject[] previousObjects = hasTrackedBy ? new VirtualCullableObject[trackedBy.size()] : null; + if (hasTrackedBy) { + for (int j = 0; j < previousObjects.length; j++) { + previousObjects[j] = trackedBy.get(j).getTrackedBlockEntity(pos); + } + } + outer: for (int i = 0; i < elements.length; i++) { BlockEntityElementConfig config = renderers[i]; + /* + * 严格可变换部分 + */ for (int j = 0; j < previousElements.length; j++) { BlockEntityElement previousElement = previousElements[j]; if (previousElement != null && config.elementClass().isInstance(previousElement)) { - BlockEntityElement newElement = ((BlockEntityElementConfig) config).create(wrappedWorld, pos, previousElement); + BlockEntityElement newElement = ((BlockEntityElementConfig) config).createExact(wrappedWorld, pos, previousElement); if (newElement != null) { previousElements[j] = null; elements[i] = newElement; if (hasTrackedBy) { - // 如果启用实体剔除,那么只对已经渲染的进行变换 - 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) { + for (int k = 0; k < trackedBy.size(); k++) { + Player player = trackedBy.get(k); + VirtualCullableObject cullableObject = previousObjects[k]; + if (cullableObject == null || cullableObject.isShown) { newElement.transform(player); } } @@ -224,21 +249,48 @@ public class CEChunk { } } } + /* + * 可变换部分 + */ + for (int j = 0; j < previousElements.length; j++) { + BlockEntityElement previousElement = previousElements[j]; + if (previousElement != null && config.elementClass().isInstance(previousElement)) { + BlockEntityElement newElement = ((BlockEntityElementConfig) config).create(wrappedWorld, pos, previousElement); + if (newElement != null) { + previousElements[j] = null; + elements[i] = newElement; + if (hasTrackedBy) { + for (int k = 0; k < trackedBy.size(); k++) { + Player player = trackedBy.get(k); + VirtualCullableObject cullableObject = previousObjects[k]; + if (cullableObject == null || cullableObject.isShown) { + newElement.transform(player); + } + } + } + continue outer; + } + } + } + /* + * 不可变换的直接生成 + */ BlockEntityElement newElement = config.create(wrappedWorld, pos); elements[i] = newElement; if (hasTrackedBy) { - if (Config.enableEntityCulling()) { - for (Player player : trackedBy) { - player.addTrackedBlockEntity(pos, renderer); - } - } else { - for (Player player : trackedBy) { + for (int k = 0; k < trackedBy.size(); k++) { + Player player = trackedBy.get(k); + VirtualCullableObject cullableObject = previousObjects[k]; + if (cullableObject == null || cullableObject.isShown) { newElement.show(player); } } } } if (hasTrackedBy) { + /* + * 未能完成变化的,需要直接删除 + */ for (int i = 0; i < previousElements.length; i++) { BlockEntityElement previousElement = previousElements[i]; if (previousElement != null) { @@ -247,19 +299,31 @@ public class CEChunk { } } } + // 添加 track + for (int i = 0; i < previousObjects.length; i++) { + VirtualCullableObject previousObject = previousObjects[i]; + if (previousObject != null) { + previousObject.setCullable(renderer); + } else { + trackedBy.get(i).addTrackedBlockEntity(pos, renderer); + } + } } } } else { + /* + * + * 全新方块实体 + * + */ for (int i = 0; i < elements.length; i++) { elements[i] = renderers[i].create(wrappedWorld, pos); } if (hasTrackedBy) { - if (Config.enableEntityCulling()) { - for (Player player : trackedBy) { + for (Player player : trackedBy) { + if (Config.enableEntityCulling()) { player.addTrackedBlockEntity(pos, renderer); - } - } else { - for (Player player : trackedBy) { + } else { renderer.show(player); } } @@ -456,7 +520,7 @@ public class CEChunk { BlockPos pos = blockEntity.pos(); ImmutableBlockState blockState = this.getBlockState(pos); if (!blockState.hasBlockEntity()) { - Debugger.BLOCK_ENTITY.debug(() -> "Failed to add invalid block entity " + blockEntity.saveAsTag() + " at " + pos); + Debugger.BLOCK.debug(() -> "Failed to add invalid block entity " + blockEntity.saveAsTag() + " at " + pos); return; } // 设置方块实体所在世界 diff --git a/core/src/main/java/net/momirealms/craftengine/core/world/chunk/client/VirtualCullableObject.java b/core/src/main/java/net/momirealms/craftengine/core/world/chunk/client/VirtualCullableObject.java index 80b6f9ef2..fef5e547d 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/world/chunk/client/VirtualCullableObject.java +++ b/core/src/main/java/net/momirealms/craftengine/core/world/chunk/client/VirtualCullableObject.java @@ -4,14 +4,18 @@ 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 Cullable cullable; + public boolean isShown; public VirtualCullableObject(Cullable cullable) { this.cullable = cullable; this.isShown = false; } + public void setCullable(Cullable cullable) { + this.cullable = cullable; + } + public Cullable cullable() { return cullable; } diff --git a/core/src/main/java/net/momirealms/craftengine/core/world/chunk/serialization/DefaultBlockEntitySerializer.java b/core/src/main/java/net/momirealms/craftengine/core/world/chunk/serialization/DefaultBlockEntitySerializer.java index 5c2a49712..3c331d193 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/world/chunk/serialization/DefaultBlockEntitySerializer.java +++ b/core/src/main/java/net/momirealms/craftengine/core/world/chunk/serialization/DefaultBlockEntitySerializer.java @@ -38,7 +38,7 @@ public final class DefaultBlockEntitySerializer { Key id = Key.of(data.getString("id")); BlockEntityType type = BuiltInRegistries.BLOCK_ENTITY_TYPE.getValue(id); if (type == null) { - Debugger.BLOCK_ENTITY.debug(() -> "Unknown block entity type: " + id); + Debugger.BLOCK.debug(() -> "Unknown block entity type: " + id); } else { BlockPos pos = BlockEntity.readPosAndVerify(data, chunk.chunkPos()); ImmutableBlockState blockState = chunk.getBlockState(pos); diff --git a/gradle.properties b/gradle.properties index 102014893..46db05f12 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,9 +1,9 @@ org.gradle.jvmargs=-Xmx1G # Project settings -project_version=0.0.65.13.1 -config_version=58 -lang_version=40 +project_version=0.0.65.15 +config_version=60 +lang_version=41 project_group=net.momirealms latest_supported_version=1.21.10 @@ -48,7 +48,7 @@ byte_buddy_version=1.18.1 ahocorasick_version=0.6.3 snake_yaml_version=2.5 anti_grief_version=1.0.5 -nms_helper_version=1.0.137 +nms_helper_version=1.0.138 evalex_version=3.5.0 reactive_streams_version=1.0.4 amazon_awssdk_version=2.38.7