9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2026-01-04 15:41:38 +00:00

Merge branch 'Xiao-MoMi:dev' into dev

This commit is contained in:
jhqwqmc
2025-11-30 02:57:09 +08:00
committed by GitHub
37 changed files with 867 additions and 517 deletions

View File

@@ -45,6 +45,14 @@ public class ItemBlockEntityElementConfig implements BlockEntityElementConfig<It
return new ItemBlockEntityElement(this, pos, previous.entityId1, previous.entityId2, !previous.config.position.equals(this.position));
}
@Override
public ItemBlockEntityElement createExact(World world, BlockPos pos, ItemBlockEntityElement previous) {
if (!previous.config.equals(this)) {
return null;
}
return new ItemBlockEntityElement(this, pos, previous.entityId1, previous.entityId2, false);
}
@Override
public Class<ItemBlockEntityElement> elementClass() {
return ItemBlockEntityElement.class;
@@ -62,6 +70,12 @@ public class ItemBlockEntityElementConfig implements BlockEntityElementConfig<It
return this.lazyMetadataPacket.apply(player);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ItemBlockEntityElementConfig that)) return false;
return this.position.equals(that.position);
}
public static class Factory implements BlockEntityElementConfigFactory {
@SuppressWarnings("unchecked")

View File

@@ -1,5 +1,6 @@
package net.momirealms.craftengine.bukkit.block.entity.renderer.element;
import com.google.common.base.Objects;
import net.momirealms.craftengine.bukkit.entity.data.ItemDisplayEntityData;
import net.momirealms.craftengine.bukkit.item.BukkitItemManager;
import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityElement;
@@ -61,14 +62,14 @@ public class ItemDisplayBlockEntityElementConfig implements BlockEntityElementCo
this.shadowStrength = shadowStrength;
this.lazyMetadataPacket = player -> {
List<Object> 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<ItemDisplayBlockEntityElement> 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")

View File

@@ -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<Object> 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<TextDisplayBlockEntityElement> 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")

View File

@@ -42,6 +42,7 @@ public class BukkitCommandManager extends AbstractCommandManager<CommandSender>
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),

View File

@@ -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<CommandSender> {
public SetEntityViewDistanceScaleCommand(CraftEngineCommandManager<CommandSender> commandManager, CraftEngine plugin) {
super(commandManager, plugin);
}
@Override
public Command.Builder<? extends CommandSender> assembleCommand(org.incendo.cloud.CommandManager<CommandSender> manager, Command.Builder<CommandSender> 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";
}
}

View File

@@ -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);

View File

@@ -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<BlockPos, VirtualCullableObject> 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);
}
}

View File

@@ -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

View File

@@ -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
resource-pack: false
block: false
entity-culling: false

View File

@@ -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

View File

@@ -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:

View File

@@ -74,6 +74,7 @@ command.send_resource_pack.success.multiple: "<white>Send resource packs to <arg
command.locale.set.failure: "<red>Invalid locale format: <arg:0></red>"
command.locale.set.success: "<white>Updated selected locale to <arg:0> for <arg:1></white>"
command.locale.unset.success: "<white>Cleared selected locale for <arg:0></white>"
command.entity_view_distance_scale.set.success: "<white>Updated entity view distance scale to <arg:0> for <arg:1></white>"
warning.network.resource_pack.unverified_uuid: "<yellow>Player <arg:0> is attempting to request a resource pack using a UUID (<arg:1>) that is not authenticated by the server.</yellow>"
warning.config.pack.duplicated_files: "<red>Duplicated files Found. Please resolve them through config.yml 'resource-pack.duplicated-files-handler' section.</red>"
warning.config.yaml.duplicated_key: "<red>Issue found in file <arg:0> - Found duplicated key '<arg:1>' at line <arg:2>, this might cause unexpected results.</red>"

View File

@@ -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<String, Object> 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<BlockEntityElementConfig<? extends BlockEntityElement>[]> parseBlockEntityRender(Object arguments) {
if (arguments == null) return Optional.empty();

View File

@@ -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<BlockEntityElementConfig<? extends BlockEntityElement>[]> blockEntityRenderer,
AABB estimateAABB) {
@Nullable CullingData cullingData) {
}

View File

@@ -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<? extends BlockEntity> blockEntityType;
@Nullable
private BlockEntityElementConfig<? extends BlockEntityElement>[] renderers;
private AABB estimatedBoundingBox;
@Nullable
private CullingData cullingData;
ImmutableBlockState(
Holder.Reference<CustomBlock> 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() {

View File

@@ -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;
}
}

View File

@@ -11,5 +11,9 @@ public interface BlockEntityElementConfig<E extends BlockEntityElement> {
return null;
}
default E createExact(World world, BlockPos pos, E previous) {
return null;
}
Class<E> elementClass();
}

View File

@@ -32,7 +32,7 @@ public class TickingBlockEntityImpl<T extends BlockEntity> 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 {

View File

@@ -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();
}

View File

@@ -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();

View File

@@ -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)) {

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -0,0 +1,6 @@
package net.momirealms.craftengine.core.plugin.entityculling;
import net.momirealms.craftengine.core.plugin.Manageable;
public interface EntityCullingManager extends Manageable {
}

View File

@@ -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<EntityCullingThread> 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();
}
}
}

View File

@@ -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();
}
}

View File

@@ -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<int[]> {
private int x;
private int y;
private int z;
private int stepX;
private int stepY;
private int stepZ;
private double tMax;
private double tMaxX;
private double tMaxY;
private double tMaxZ;
private double tDeltaX;
private double tDeltaY;
private double tDeltaZ;
private int[] ref = new int[3]; // This implementation always returns ref or refSwap to avoid garbage. Can easily be changed if needed.
private int[] refSwap = new int[3];
private int[] next;
public VoxelIterator(double startX, double startY, double startZ, double endX, double endY, double endZ) {
initialize(startX, startY, startZ, endX, endY, endZ);
}
public VoxelIterator(int x, int y, int z, double startX, double startY, double startZ, double endX, double endY, double endZ) {
initialize(x, y, z, startX, startY, startZ, endX, endY, endZ);
}
public VoxelIterator(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
initialize(startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
initialize(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance, boolean normalized) {
if (normalized) {
initializeNormalized(startX, startY, startZ, directionX, directionY, directionZ, distance);
} else {
initialize(startX, startY, startZ, directionX, directionY, directionZ, distance);
}
}
public VoxelIterator(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance, boolean normalized) {
if (normalized) {
initializeNormalized(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
} else {
initialize(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
}
}
public VoxelIterator initialize(double startX, double startY, double startZ, double endX, double endY, double endZ) {
return initialize(floor(startX), floor(startY), floor(startZ), startX, startY, startZ, endX, endY, endZ);
}
public VoxelIterator initialize(int x, int y, int z, double startX, double startY, double startZ, double endX, double endY, double endZ) {
double directionX = endX - startX;
double directionY = endY - startY;
double directionZ = endZ - startZ;
double distance = Math.sqrt(directionX * directionX + directionY * directionY + directionZ * directionZ);
double fixedDistance = distance == 0. ? Double.NaN : distance;
directionX /= fixedDistance;
directionY /= fixedDistance;
directionZ /= fixedDistance;
return initializeNormalized(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator initialize(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
return initialize(floor(startX), floor(startY), floor(startZ), startX, startY, startZ, directionX, directionY, directionZ, distance);
}
public VoxelIterator initialize(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
double signum = Math.signum(distance);
directionX *= signum;
directionY *= signum;
directionZ *= signum;
double length = Math.sqrt(directionX * directionX + directionY * directionY + directionZ * directionZ);
if (length == 0.) {
length = Double.NaN;
}
directionX /= length;
directionY /= length;
directionZ /= length;
return initializeNormalized(x, y, z, startX, startY, startZ, directionX, directionY, directionZ, Math.abs(distance));
}
public VoxelIterator initializeNormalized(double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
return initializeNormalized(floor(startX), floor(startY), floor(startZ), startX, startY, startZ, directionX, directionY, directionZ, Math.abs(distance));
}
public VoxelIterator initializeNormalized(int x, int y, int z, double startX, double startY, double startZ, double directionX, double directionY, double directionZ, double distance) {
this.x = x;
this.y = y;
this.z = z;
tMax = distance;
stepX = directionX < 0. ? -1 : 1;
stepY = directionY < 0. ? -1 : 1;
stepZ = directionZ < 0. ? -1 : 1;
tMaxX = directionX == 0. ? Double.POSITIVE_INFINITY : (x + (stepX + 1) / 2 - startX) / directionX;
tMaxY = directionY == 0. ? Double.POSITIVE_INFINITY : (y + (stepY + 1) / 2 - startY) / directionY;
tMaxZ = directionZ == 0. ? Double.POSITIVE_INFINITY : (z + (stepZ + 1) / 2 - startZ) / directionZ;
tDeltaX = 1. / Math.abs(directionX);
tDeltaY = 1. / Math.abs(directionY);
tDeltaZ = 1. / Math.abs(directionZ);
next = ref;
ref[0] = x;
ref[1] = y;
ref[2] = z;
return this;
}
public int[] calculateNext() {
if (tMaxX < tMaxY) {
if (tMaxZ < tMaxX) {
if (tMaxZ <= tMax) {
z += stepZ;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxZ += tDeltaZ;
} else {
next = null;
}
} else {
if (tMaxX <= tMax) {
if (tMaxZ == tMaxX) {
z += stepZ;
tMaxZ += tDeltaZ;
}
x += stepX;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxX += tDeltaX;
} else {
next = null;
}
}
} else if (tMaxY < tMaxZ) {
if (tMaxY <= tMax) {
if (tMaxX == tMaxY) {
x += stepX;
tMaxX += tDeltaX;
}
y += stepY;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxY += tDeltaY;
} else {
next = null;
}
} else {
if (tMaxZ <= tMax) {
if (tMaxX == tMaxZ) {
x += stepX;
tMaxX += tDeltaX;
}
if (tMaxY == tMaxZ) {
y += stepY;
tMaxY += tDeltaY;
}
z += stepZ;
// next = new int[] { x, y, z };
ref[0] = x;
ref[1] = y;
ref[2] = z;
tMaxZ += tDeltaZ;
} else {
next = null;
}
}
return next;
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public int[] next() {
int[] next = this.next;
if (next == null) {
throw new NoSuchElementException();
}
int[] temp = ref;
ref = refSwap;
refSwap = temp;
this.next = ref;
calculateNext();
return next;
}
private static int floor(double value) {
int i = (int) value;
return value < (double) i ? i - 1 : i;
}
}

View File

@@ -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");
}

View File

@@ -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<Boolean> condition;

View File

@@ -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<T> implements SchedulerAdapter<T> {
return new AsyncTask(future);
}
@Override
public SchedulerTask asyncRepeating(Consumer<SchedulerTask> 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();

View File

@@ -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();
}
}

View File

@@ -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<W> {
@@ -25,6 +26,8 @@ public interface SchedulerAdapter<W> {
SchedulerTask asyncRepeating(Runnable task, long delay, long interval, TimeUnit unit);
SchedulerTask asyncRepeating(Consumer<SchedulerTask> task, long delay, long interval, TimeUnit unit);
void shutdownScheduler();
void shutdownExecutor();

View File

@@ -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();
}

View File

@@ -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<? extends BlockEntityElement>[] 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<Player> 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<? extends BlockEntityElement> 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<? extends BlockEntityElement> 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;
}
// 设置方块实体所在世界

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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