diff --git a/common-files/src/main/resources/config.yml b/common-files/src/main/resources/config.yml index 44c64fa80..c5c00829e 100644 --- a/common-files/src/main/resources/config.yml +++ b/common-files/src/main/resources/config.yml @@ -193,15 +193,17 @@ resource-pack: ip: "localhost" port: 8163 protocol: "http" + # Blocks all requests from non-Minecraft clients. deny-non-minecraft-request: true + # Generates a single-use, time-limited download link for each player. one-time-token: true - rate-limitation-per-ip: - enable: true - max-requests: 10 - reset-interval: 30 - token-bucket: - enable: true - qps: 1000 + rate-limiting: + # Maximum bandwidth per second to prevent server instability for other players during resource pack downloads + max-bandwidth-per-second: 5_000_000 # 5MB/s + # Minimum guaranteed download speed per player to ensure acceptable download performance during concurrent downloads + min-download-speed-per-player: 50_000 # 50KB/s + # Prevent a single IP from sending too many resource pack download requests in a short time period + qps-per-ip: 5/60 # 5 requests per 60 seconds item: # [Premium Exclusive] @@ -550,6 +552,12 @@ chunk-system: remove: [] convert: {} +#client-optimization: +# # Using server-side ray tracing algorithms to hide certain entities and reduce client-side rendering pressure. +# entity-culling: +# enable: false +# whitelist-entities: [] + # Enables or disables debug mode debug: common: false 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 df459b4d2..08f10e736 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 @@ -83,15 +83,18 @@ templates: # any leaves block default:block_state/leaves: template: default:block_state/__leaves__ - arguments::auto_state: leaves + arguments: + auto_state: leaves # tintable leaves block default:block_state/tintable_leaves: template: default:block_state/__leaves__ - arguments::auto_state: tintable_leaves + arguments: + auto_state: tintable_leaves # non-tintable leaves block default:block_state/non_tintable_leaves: template: default:block_state/__leaves__ - arguments::auto_state: non_tintable_leaves + arguments: + auto_state: non_tintable_leaves # trapdoor block default:block_state/trapdoor: properties: diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHost.java b/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHost.java index c0958173d..1c71218ae 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHost.java +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHost.java @@ -1,6 +1,5 @@ package net.momirealms.craftengine.core.pack.host.impl; -import com.google.common.util.concurrent.RateLimiter; import io.github.bucket4j.Bandwidth; import net.momirealms.craftengine.core.pack.host.ResourcePackDownloadData; import net.momirealms.craftengine.core.pack.host.ResourcePackHost; @@ -10,7 +9,6 @@ import net.momirealms.craftengine.core.plugin.CraftEngine; import net.momirealms.craftengine.core.plugin.config.Config; import net.momirealms.craftengine.core.plugin.locale.LocalizedException; import net.momirealms.craftengine.core.util.Key; -import net.momirealms.craftengine.core.util.MiscUtils; import net.momirealms.craftengine.core.util.ResourceConfigUtils; import java.nio.file.Path; @@ -19,7 +17,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; public class SelfHost implements ResourcePackHost { public static final Factory FACTORY = new Factory(); @@ -81,30 +78,28 @@ public class SelfHost implements ResourcePackHost { boolean oneTimeToken = ResourceConfigUtils.getAsBoolean(arguments.getOrDefault("one-time-token", true), "one-time-token"); String protocol = arguments.getOrDefault("protocol", "http").toString(); boolean denyNonMinecraftRequest = ResourceConfigUtils.getAsBoolean(arguments.getOrDefault("deny-non-minecraft-request", true), "deny-non-minecraft-request"); - Map rateLimitPerIp = MiscUtils.castToMap(arguments.get("rate-limitation-per-ip"), true); - boolean enabledLimitPerIp = false; + + Bandwidth limit = null; - out: - if (rateLimitPerIp != null) { - enabledLimitPerIp = ResourceConfigUtils.getAsBoolean(rateLimitPerIp.getOrDefault("enable", false), "enable"); - if (!enabledLimitPerIp) break out; - int maxRequests = Math.max(ResourceConfigUtils.getAsInt(rateLimitPerIp.getOrDefault("max-requests", 5), "max-requests"), 1); - int resetInterval = Math.max(ResourceConfigUtils.getAsInt(rateLimitPerIp.getOrDefault("reset-interval", 20), "reset-interval"), 1); - limit = Bandwidth.builder() - .capacity(maxRequests) - .refillGreedy(maxRequests, Duration.ofSeconds(resetInterval)) - .build(); + Map rateLimitingSection = ResourceConfigUtils.getAsMapOrNull(arguments.get("rate-limiting"), "rate-limiting"); + long maxBandwidthUsage = 0L; + long minDownloadSpeed = 50_000L; + if (rateLimitingSection != null) { + if (rateLimitingSection.containsKey("qps-per-ip")) { + String qps = rateLimitingSection.get("qps-per-ip").toString(); + String[] split = qps.split("/", 2); + if (split.length == 1) split = new String[]{split[0], "1"}; + int maxRequests = ResourceConfigUtils.getAsInt(split[0], "qps-per-ip"); + int resetInterval = ResourceConfigUtils.getAsInt(split[1], "qps-per-ip"); + limit = Bandwidth.builder() + .capacity(maxRequests) + .refillGreedy(maxRequests, Duration.ofSeconds(resetInterval)) + .build(); + } + maxBandwidthUsage = ResourceConfigUtils.getAsLong(rateLimitingSection.getOrDefault("max-bandwidth-per-second", 0), "max-bandwidth"); + minDownloadSpeed = ResourceConfigUtils.getAsLong(rateLimitingSection.getOrDefault("min-download-speed-per-player", 50_000), "min-download-speed-per-player"); } - Map tokenBucket = MiscUtils.castToMap(arguments.get("token-bucket"), true); - boolean enabledTokenBucket = false; - RateLimiter globalLimiter = null; - out: - if (tokenBucket != null) { - enabledTokenBucket = ResourceConfigUtils.getAsBoolean(tokenBucket.getOrDefault("enable", false), "enable"); - if (!enabledTokenBucket) break out; - globalLimiter = RateLimiter.create(ResourceConfigUtils.getAsDouble(tokenBucket.getOrDefault("qps", 1000), "qps")); - } - selfHostHttpServer.updateProperties(ip, port, url, denyNonMinecraftRequest, protocol, limit, enabledLimitPerIp, enabledTokenBucket, globalLimiter, oneTimeToken); + selfHostHttpServer.updateProperties(ip, port, url, denyNonMinecraftRequest, protocol, limit, oneTimeToken, maxBandwidthUsage, minDownloadSpeed); return INSTANCE; } } diff --git a/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHostHttpServer.java b/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHostHttpServer.java index 7004f90fa..df91aeb70 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHostHttpServer.java +++ b/core/src/main/java/net/momirealms/craftengine/core/pack/host/impl/SelfHostHttpServer.java @@ -3,21 +3,27 @@ package net.momirealms.craftengine.core.pack.host.impl; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Scheduler; -import com.google.common.util.concurrent.RateLimiter; import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bucket; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.Unpooled; import io.netty.channel.*; +import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.DefaultChannelGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.*; +import io.netty.handler.stream.ChunkedStream; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.handler.traffic.GlobalChannelTrafficShapingHandler; import io.netty.util.CharsetUtil; +import io.netty.util.concurrent.GlobalEventExecutor; import net.momirealms.craftengine.core.pack.host.ResourcePackDownloadData; import net.momirealms.craftengine.core.plugin.CraftEngine; import org.jetbrains.annotations.Nullable; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.InetSocketAddress; import java.net.URLEncoder; @@ -28,6 +34,8 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -35,14 +43,14 @@ import java.util.concurrent.atomic.AtomicLong; public class SelfHostHttpServer { private static SelfHostHttpServer instance; private final Cache oneTimePackUrls = Caffeine.newBuilder() - .maximumSize(256) + .maximumSize(1024) .scheduler(Scheduler.systemScheduler()) .expireAfterWrite(1, TimeUnit.MINUTES) .build(); private final Cache ipRateLimiters = Caffeine.newBuilder() - .maximumSize(256) + .maximumSize(1024) .scheduler(Scheduler.systemScheduler()) - .expireAfterAccess(10, TimeUnit.MINUTES) + .expireAfterAccess(5, TimeUnit.MINUTES) .build(); private final AtomicLong totalRequests = new AtomicLong(); @@ -53,9 +61,7 @@ public class SelfHostHttpServer { .refillGreedy(1, Duration.ofSeconds(1)) .initialTokens(1) .build(); - private RateLimiter globalLimiter = RateLimiter.create(1); - private boolean enabledLimitPerIp = false; - private boolean enabledGlobalLimit = false; + private String ip = "localhost"; private int port = -1; private String protocol = "http"; @@ -63,6 +69,12 @@ public class SelfHostHttpServer { private boolean denyNonMinecraft = true; private boolean useToken; + private long globalUploadRateLimit = 0; + private long minDownloadSpeed = 50_000; + private GlobalChannelTrafficShapingHandler trafficShapingHandler; + private ScheduledExecutorService virtualTrafficExecutor; + private final ChannelGroup activeDownloadChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); + private byte[] resourcePackBytes; private String packHash; private UUID packUUID; @@ -84,20 +96,24 @@ public class SelfHostHttpServer { boolean denyNonMinecraft, String protocol, Bandwidth limitPerIp, - boolean enabledLimitPerIp, - boolean enabledGlobalLimit, - RateLimiter globalLimiter, - boolean token) { + boolean token, + long globalUploadRateLimit, + long minDownloadSpeed) { this.ip = ip; this.url = url; this.denyNonMinecraft = denyNonMinecraft; this.protocol = protocol; this.limitPerIp = limitPerIp; - this.enabledLimitPerIp = enabledLimitPerIp; - this.enabledGlobalLimit = enabledGlobalLimit; - this.globalLimiter = globalLimiter; this.useToken = token; - + if (this.globalUploadRateLimit != globalUploadRateLimit || this.minDownloadSpeed != minDownloadSpeed) { + this.globalUploadRateLimit = globalUploadRateLimit; + this.minDownloadSpeed = minDownloadSpeed; + if (this.trafficShapingHandler != null) { + long initSize = globalUploadRateLimit <= 0 ? 0 : Math.max(minDownloadSpeed, globalUploadRateLimit); + this.trafficShapingHandler.setWriteLimit(initSize); + this.trafficShapingHandler.setWriteChannelLimit(initSize); + } + } if (port <= 0 || port > 65535) { throw new IllegalArgumentException("Invalid port: " + port); } @@ -119,7 +135,17 @@ public class SelfHostHttpServer { private void initializeServer() { bossGroup = new NioEventLoopGroup(1); workerGroup = new NioEventLoopGroup(); - + virtualTrafficExecutor = Executors.newScheduledThreadPool(1, Thread.ofVirtual().factory()); + long initSize = globalUploadRateLimit <= 0 ? 0 : Math.max(minDownloadSpeed, globalUploadRateLimit); + trafficShapingHandler = new GlobalChannelTrafficShapingHandler( + virtualTrafficExecutor, + initSize, + 0, // 全局读取不限 + initSize, // 默认单通道和总体一致 + 0, // 单通道读取不限 + 100, // checkInterval (ms) + 10_000 // maxTime (ms) + ); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) @@ -127,7 +153,9 @@ public class SelfHostHttpServer { @Override protected void initChannel(SocketChannel ch) { ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast("trafficShaping", trafficShapingHandler); pipeline.addLast(new HttpServerCodec()); + pipeline.addLast(new ChunkedWriteHandler()); pipeline.addLast(new HttpObjectAggregator(1048576)); pipeline.addLast(new RequestHandler()); } @@ -143,6 +171,17 @@ public class SelfHostHttpServer { @ChannelHandler.Sharable private class RequestHandler extends SimpleChannelInboundHandler { + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + super.channelInactive(ctx); + // 有人走了,其他人的速度上限提高 + if (activeDownloadChannels.contains(ctx.channel())) { + activeDownloadChannels.remove(ctx.channel()); + rebalanceBandwidth(); + } + } + @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) { totalRequests.incrementAndGet(); @@ -151,13 +190,7 @@ public class SelfHostHttpServer { String clientIp = ((InetSocketAddress) ctx.channel().remoteAddress()) .getAddress().getHostAddress(); - if (enabledGlobalLimit && !globalLimiter.tryAcquire()) { - sendError(ctx, HttpResponseStatus.TOO_MANY_REQUESTS, "Rate limit exceeded"); - blockedRequests.incrementAndGet(); - return; - } - - if (enabledLimitPerIp && !checkIpRateLimit(clientIp)) { + if (!checkIpRateLimit(clientIp)) { sendError(ctx, HttpResponseStatus.TOO_MANY_REQUESTS, "Rate limit exceeded"); blockedRequests.incrementAndGet(); return; @@ -180,6 +213,7 @@ public class SelfHostHttpServer { } private void handleDownload(ChannelHandlerContext ctx, FullHttpRequest request, QueryStringDecoder queryDecoder) { + // 使用一次性token if (useToken) { String token = queryDecoder.parameters().getOrDefault("token", java.util.Collections.emptyList()).stream().findFirst().orElse(null); if (!validateToken(token)) { @@ -189,6 +223,7 @@ public class SelfHostHttpServer { } } + // 不是Minecraft客户端 if (denyNonMinecraft) { String userAgent = request.headers().get(HttpHeaderNames.USER_AGENT); if (userAgent == null || !userAgent.startsWith("Minecraft Java/")) { @@ -198,22 +233,47 @@ public class SelfHostHttpServer { } } + // 没有资源包 if (resourcePackBytes == null) { sendError(ctx, HttpResponseStatus.NOT_FOUND, "Resource pack missing"); blockedRequests.incrementAndGet(); return; } - FullHttpResponse response = new DefaultFullHttpResponse( - HttpVersion.HTTP_1_1, - HttpResponseStatus.OK, - Unpooled.wrappedBuffer(resourcePackBytes) - ); - response.headers() - .set(HttpHeaderNames.CONTENT_TYPE, "application/zip") - .set(HttpHeaderNames.CONTENT_LENGTH, resourcePackBytes.length); + // 新人来了,所有人的速度上限降低 + if (!activeDownloadChannels.contains(ctx.channel())) { + activeDownloadChannels.add(ctx.channel()); + rebalanceBandwidth(); + } - ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + // 告诉客户端资源包大小 + long fileLength = resourcePackBytes.length; + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + HttpUtil.setContentLength(response, fileLength); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/zip"); + boolean keepAlive = HttpUtil.isKeepAlive(request); + if (keepAlive) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + ctx.write(response); + + // 发送分段资源包 + ChunkedStream chunkedStream = new ChunkedStream(new ByteArrayInputStream(resourcePackBytes), 8192); + HttpChunkedInput httpChunkedInput = new HttpChunkedInput(chunkedStream); + ChannelFuture sendFileFuture = ctx.writeAndFlush(httpChunkedInput); + if (!keepAlive) { + sendFileFuture.addListener(ChannelFutureListener.CLOSE); + } + + // 监听下载完成(成功或失败),以便在下载结束后(如果不关闭连接)也能移除计数 + // 注意:如果是 Keep-Alive,连接不会断,但下载结束了。 + // 为了精确控制,可以在这里监听 operationComplete + sendFileFuture.addListener((ChannelFutureListener) future -> { + if (activeDownloadChannels.contains(ctx.channel())) { + activeDownloadChannels.remove(ctx.channel()); + rebalanceBandwidth(); + } + }); } private void handleMetrics(ChannelHandlerContext ctx) { @@ -267,6 +327,28 @@ public class SelfHostHttpServer { } } + private synchronized void rebalanceBandwidth() { + if (globalUploadRateLimit == 0) { + trafficShapingHandler.setWriteChannelLimit(0); + return; + } + + int activeCount = activeDownloadChannels.size(); + if (activeCount == 0) { + trafficShapingHandler.setWriteChannelLimit(globalUploadRateLimit); + return; + } + + // 计算平均带宽:全局总量 / 当前人数 + long fairRate = globalUploadRateLimit / activeCount; + + // 确保不低于最小保障速率(可选,防止除法导致过小) + fairRate = Math.max(fairRate, this.minDownloadSpeed); + + // 更新 Handler 配置 + trafficShapingHandler.setWriteChannelLimit(fairRate); + } + @Nullable public ResourcePackDownloadData generateOneTimeUrl() { if (this.resourcePackBytes == null) return null; @@ -285,6 +367,17 @@ public class SelfHostHttpServer { } public void disable() { + // 释放流量整形资源 + if (trafficShapingHandler != null) { + trafficShapingHandler.release(); + trafficShapingHandler = null; + } + // 关闭专用线程池 + if (virtualTrafficExecutor != null) { + virtualTrafficExecutor.shutdown(); + virtualTrafficExecutor = null; + } + activeDownloadChannels.close(); if (serverChannel != null) { serverChannel.close().awaitUninterruptibly(); bossGroup.shutdownGracefully(); 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 new file mode 100644 index 000000000..72c6ecdc0 --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/plugin/entityculling/EntityCulling.java @@ -0,0 +1,453 @@ +package net.momirealms.craftengine.core.plugin.entityculling; + +import net.momirealms.craftengine.core.util.MiscUtils; +import net.momirealms.craftengine.core.world.MutableVec3d; +import net.momirealms.craftengine.core.world.Vec3d; + +import java.util.Arrays; +import java.util.BitSet; + +public class EntityCulling { + + // 面掩码常量 + private static final int ON_MIN_X = 0x01; + private static final int ON_MAX_X = 0x02; + private static final int ON_MIN_Y = 0x04; + private static final int ON_MAX_Y = 0x08; + private static final int ON_MIN_Z = 0x10; + private static final int ON_MAX_Z = 0x20; + + private final int reach; + private final double aabbExpansion; + private final DataProvider provider; + private final OcclusionCache cache; + + // 重用数据结构,减少GC压力 + private final BitSet skipList = new BitSet(); + private final MutableVec3d[] targetPoints = new MutableVec3d[15]; + private final MutableVec3d targetPos = new MutableVec3d(0, 0, 0); + private final int[] cameraPos = new int[3]; + private final boolean[] dotselectors = new boolean[14]; + private final int[] lastHitBlock = new int[3]; + + // 状态标志 + private boolean allowRayChecks = false; + private boolean allowWallClipping = false; + + public EntityCulling(int maxDistance, DataProvider provider) { + this(maxDistance, provider, new ArrayOcclusionCache(maxDistance), 0.5); + } + + public EntityCulling(int maxDistance, DataProvider provider, OcclusionCache cache, double aabbExpansion) { + this.reach = maxDistance; + this.provider = provider; + this.cache = cache; + this.aabbExpansion = aabbExpansion; + // 预先初始化点对象 + for(int i = 0; i < targetPoints.length; i++) { + targetPoints[i] = new MutableVec3d(0, 0, 0); + } + } + + public boolean isAABBVisible(Vec3d aabbMin, MutableVec3d aabbMax, MutableVec3d viewerPosition) { + try { + // 计算包围盒范围 + int maxX = MiscUtils.fastFloor(aabbMax.x + aabbExpansion); + int maxY = MiscUtils.fastFloor(aabbMax.y + aabbExpansion); + int maxZ = MiscUtils.fastFloor(aabbMax.z + aabbExpansion); + int minX = MiscUtils.fastFloor(aabbMin.x - aabbExpansion); + int minY = MiscUtils.fastFloor(aabbMin.y - aabbExpansion); + int minZ = MiscUtils.fastFloor(aabbMin.z - aabbExpansion); + + cameraPos[0] = MiscUtils.fastFloor(viewerPosition.x); + cameraPos[1] = MiscUtils.fastFloor(viewerPosition.y); + cameraPos[2] = MiscUtils.fastFloor(viewerPosition.z); + + // 判断是否在包围盒内部 + Relative relX = Relative.from(minX, maxX, cameraPos[0]); + Relative relY = Relative.from(minY, maxY, cameraPos[1]); + Relative relZ = Relative.from(minZ, maxZ, cameraPos[2]); + + if(relX == Relative.INSIDE && relY == Relative.INSIDE && relZ == Relative.INSIDE) { + return true; + } + + skipList.clear(); + + // 1. 快速检查缓存 + int id = 0; + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + for (int z = minZ; z <= maxZ; z++) { + int cachedValue = getCacheValue(x, y, z); + if (cachedValue == 1) return true; // 缓存显示可见 + if (cachedValue != 0) skipList.set(id); // 缓存显示不可见或遮挡 + id++; + } + } + } + + allowRayChecks = false; + id = 0; + + // 2. 遍历体素进行光线投射检查 + for (int x = minX; x <= maxX; x++) { + // 预计算X轴面的可见性和边缘数据 + byte visibleOnFaceX = 0; + byte faceEdgeDataX = 0; + if (x == minX) { faceEdgeDataX |= ON_MIN_X; if (relX == Relative.POSITIVE) visibleOnFaceX |= ON_MIN_X; } + if (x == maxX) { faceEdgeDataX |= ON_MAX_X; if (relX == Relative.NEGATIVE) visibleOnFaceX |= ON_MAX_X; } + + for (int y = minY; y <= maxY; y++) { + byte visibleOnFaceY = visibleOnFaceX; + byte faceEdgeDataY = faceEdgeDataX; + if (y == minY) { faceEdgeDataY |= ON_MIN_Y; if (relY == Relative.POSITIVE) visibleOnFaceY |= ON_MIN_Y; } + if (y == maxY) { faceEdgeDataY |= ON_MAX_Y; if (relY == Relative.NEGATIVE) visibleOnFaceY |= ON_MAX_Y; } + + for (int z = minZ; z <= maxZ; z++) { + // 如果缓存已标记为不可见,跳过 + if(skipList.get(id++)) continue; + + byte visibleOnFace = visibleOnFaceY; + byte faceEdgeData = faceEdgeDataY; + if (z == minZ) { faceEdgeData |= ON_MIN_Z; if (relZ == Relative.POSITIVE) visibleOnFace |= ON_MIN_Z; } + if (z == maxZ) { faceEdgeData |= ON_MAX_Z; if (relZ == Relative.NEGATIVE) visibleOnFace |= ON_MAX_Z; } + + if (visibleOnFace != 0) { + targetPos.set(x, y, z); + // 检查单个体素是否可见 + if (isVoxelVisible(viewerPosition, targetPos, faceEdgeData, visibleOnFace)) { + return true; + } + } + } + } + } + return false; + } catch (Throwable t) { + t.printStackTrace(); + return true; // 发生异常默认可见,防止渲染错误 + } + } + + // 接口定义 + public interface DataProvider { + boolean prepareChunk(int chunkX, int chunkZ); + boolean isOpaqueFullCube(int x, int y, int z); + default void cleanup() {} + default void checkingPosition(MutableVec3d[] targetPoints, int size, MutableVec3d viewerPosition) {} + } + + /** + * 检查单个体素是否对观察者可见 + */ + private boolean isVoxelVisible(MutableVec3d viewerPosition, MutableVec3d position, byte faceData, byte visibleOnFace) { + int targetSize = 0; + Arrays.fill(dotselectors, false); + + // 根据相对位置选择需要检测的关键点(角点和面中心点) + if((visibleOnFace & ON_MIN_X) != 0){ + dotselectors[0] = true; + if((faceData & ~ON_MIN_X) != 0) { dotselectors[1] = dotselectors[4] = dotselectors[5] = true; } + dotselectors[8] = true; + } + if((visibleOnFace & ON_MIN_Y) != 0){ + dotselectors[0] = true; + if((faceData & ~ON_MIN_Y) != 0) { dotselectors[3] = dotselectors[4] = dotselectors[7] = true; } + dotselectors[9] = true; + } + if((visibleOnFace & ON_MIN_Z) != 0){ + dotselectors[0] = true; + if((faceData & ~ON_MIN_Z) != 0) { dotselectors[1] = dotselectors[4] = dotselectors[5] = true; } + dotselectors[10] = true; + } + if((visibleOnFace & ON_MAX_X) != 0){ + dotselectors[4] = true; + if((faceData & ~ON_MAX_X) != 0) { dotselectors[5] = dotselectors[6] = dotselectors[7] = true; } + dotselectors[11] = true; + } + if((visibleOnFace & ON_MAX_Y) != 0){ + dotselectors[1] = true; + if((faceData & ~ON_MAX_Y) != 0) { dotselectors[2] = dotselectors[5] = dotselectors[6] = true; } + dotselectors[12] = true; + } + if((visibleOnFace & ON_MAX_Z) != 0){ + dotselectors[2] = true; + if((faceData & ~ON_MAX_Z) != 0) { dotselectors[3] = dotselectors[6] = dotselectors[7] = true; } + dotselectors[13] = true; + } + + // 填充目标点,使用偏移量防止Z-Fighting或精度问题 + if (dotselectors[0]) targetPoints[targetSize++].add(position, 0.05, 0.05, 0.05); + if (dotselectors[1]) targetPoints[targetSize++].add(position, 0.05, 0.95, 0.05); + if (dotselectors[2]) targetPoints[targetSize++].add(position, 0.05, 0.95, 0.95); + if (dotselectors[3]) targetPoints[targetSize++].add(position, 0.05, 0.05, 0.95); + if (dotselectors[4]) targetPoints[targetSize++].add(position, 0.95, 0.05, 0.05); + if (dotselectors[5]) targetPoints[targetSize++].add(position, 0.95, 0.95, 0.05); + if (dotselectors[6]) targetPoints[targetSize++].add(position, 0.95, 0.95, 0.95); + if (dotselectors[7]) targetPoints[targetSize++].add(position, 0.95, 0.05, 0.95); + // 面中心点 + if (dotselectors[8]) targetPoints[targetSize++].add(position, 0.05, 0.5, 0.5); + if (dotselectors[9]) targetPoints[targetSize++].add(position, 0.5, 0.05, 0.5); + if (dotselectors[10]) targetPoints[targetSize++].add(position, 0.5, 0.5, 0.05); + if (dotselectors[11]) targetPoints[targetSize++].add(position, 0.95, 0.5, 0.5); + if (dotselectors[12]) targetPoints[targetSize++].add(position, 0.5, 0.95, 0.5); + if (dotselectors[13]) targetPoints[targetSize++].add(position, 0.5, 0.5, 0.95); + + return isVisible(viewerPosition, targetPoints, targetSize); + } + + // 优化:使用基本数据类型代替对象分配 + private boolean rayIntersection(int[] b, MutableVec3d rayOrigin, double dirX, double dirY, double dirZ) { + double invX = 1.0 / dirX; + double invY = 1.0 / dirY; + double invZ = 1.0 / dirZ; + + double t1 = (b[0] - rayOrigin.x) * invX; + double t2 = (b[0] + 1 - rayOrigin.x) * invX; + double t3 = (b[1] - rayOrigin.y) * invY; + double t4 = (b[1] + 1 - rayOrigin.y) * invY; + double t5 = (b[2] - rayOrigin.z) * invZ; + double t6 = (b[2] + 1 - rayOrigin.z) * invZ; + + double tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6)); + double tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6)); + + // tmax > 0: 射线与AABB相交,但AABB在身后 + // tmin > tmax: 射线不相交 + return tmax > 0 && tmin <= tmax; + } + + /** + * 基于网格的光线追踪 (DDA算法) + */ + private boolean isVisible(MutableVec3d start, MutableVec3d[] targets, int size) { + int startX = cameraPos[0]; + int startY = cameraPos[1]; + int startZ = cameraPos[2]; + + for (int v = 0; v < size; v++) { + MutableVec3d target = targets[v]; + + double relX = start.x - target.x; + double relY = start.y - target.y; + double relZ = start.z - target.z; + + // 优化:避免在此处创建新的Vec3d对象进行归一化 + if(allowRayChecks) { + double len = Math.sqrt(relX * relX + relY * relY + relZ * relZ); + // 传入归一化后的方向分量 + if (rayIntersection(lastHitBlock, start, relX / len, relY / len, relZ / len)) { + continue; + } + } + + double dimAbsX = Math.abs(relX); + double dimAbsY = Math.abs(relY); + double dimAbsZ = Math.abs(relZ); + + double dimFracX = 1f / dimAbsX; + double dimFracY = 1f / dimAbsY; + double dimFracZ = 1f / dimAbsZ; + + int intersectCount = 1; + int x_inc, y_inc, z_inc; + double t_next_y, t_next_x, t_next_z; + + // 初始化DDA步进参数 + if (dimAbsX == 0f) { + x_inc = 0; t_next_x = dimFracX; + } else if (target.x > start.x) { + x_inc = 1; + intersectCount += MiscUtils.fastFloor(target.x) - startX; + t_next_x = (startX + 1 - start.x) * dimFracX; + } else { + x_inc = -1; + intersectCount += startX - MiscUtils.fastFloor(target.x); + t_next_x = (start.x - startX) * dimFracX; + } + + if (dimAbsY == 0f) { + y_inc = 0; t_next_y = dimFracY; + } else if (target.y > start.y) { + y_inc = 1; + intersectCount += MiscUtils.fastFloor(target.y) - startY; + t_next_y = (startY + 1 - start.y) * dimFracY; + } else { + y_inc = -1; + intersectCount += startY - MiscUtils.fastFloor(target.y); + t_next_y = (start.y - startY) * dimFracY; + } + + if (dimAbsZ == 0f) { + z_inc = 0; t_next_z = dimFracZ; + } else if (target.z > start.z) { + z_inc = 1; + intersectCount += MiscUtils.fastFloor(target.z) - startZ; + t_next_z = (startZ + 1 - start.z) * dimFracZ; + } else { + z_inc = -1; + intersectCount += startZ - MiscUtils.fastFloor(target.z); + t_next_z = (start.z - startZ) * dimFracZ; + } + + boolean finished = stepRay(startX, startY, startZ, + dimFracX, dimFracY, dimFracZ, intersectCount, + x_inc, y_inc, z_inc, + t_next_y, t_next_x, t_next_z); + + provider.cleanup(); + if (finished) { + cacheResult(targets[0], true); + return true; + } else { + allowRayChecks = true; + } + } + cacheResult(targets[0], false); + return false; + } + + private boolean stepRay(int currentX, int currentY, int currentZ, + double distInX, double distInY, double distInZ, + int n, int x_inc, int y_inc, int z_inc, + double t_next_y, double t_next_x, double t_next_z) { + + allowWallClipping = true; // 初始允许穿墙直到移出起始方块 + + for (; n > 1; n--) { + // 检查缓存状态:2=遮挡 + int cVal = getCacheValue(currentX, currentY, currentZ); + if (cVal == 2 && !allowWallClipping) { + lastHitBlock[0] = currentX; lastHitBlock[1] = currentY; lastHitBlock[2] = currentZ; + return false; + } + + if (cVal == 0) { + // 未缓存,查询Provider + int chunkX = currentX >> 4; + int chunkZ = currentZ >> 4; + if (!provider.prepareChunk(chunkX, chunkZ)) return false; + + if (provider.isOpaqueFullCube(currentX, currentY, currentZ)) { + if (!allowWallClipping) { + cache.setLastHidden(); + lastHitBlock[0] = currentX; lastHitBlock[1] = currentY; lastHitBlock[2] = currentZ; + return false; + } + } else { + allowWallClipping = false; + cache.setLastVisible(); + } + } else if(cVal == 1) { + allowWallClipping = false; + } + + // DDA算法选择下一个体素 + if (t_next_y < t_next_x && t_next_y < t_next_z) { + currentY += y_inc; + t_next_y += distInY; + } else if (t_next_x < t_next_y && t_next_x < t_next_z) { + currentX += x_inc; + t_next_x += distInX; + } else { + currentZ += z_inc; + t_next_z += distInZ; + } + } + return true; + } + + // 缓存状态:-1=无效, 0=未检查, 1=可见, 2=遮挡 + private int getCacheValue(int x, int y, int z) { + x -= cameraPos[0]; + y -= cameraPos[1]; + z -= cameraPos[2]; + if (Math.abs(x) > reach - 2 || Math.abs(y) > reach - 2 || Math.abs(z) > reach - 2) { + return -1; + } + return cache.getState(x + reach, y + reach, z + reach); + } + + private void cacheResult(MutableVec3d vector, boolean result) { + int cx = MiscUtils.fastFloor(vector.x) - cameraPos[0] + reach; + int cy = MiscUtils.fastFloor(vector.y) - cameraPos[1] + reach; + int cz = MiscUtils.fastFloor(vector.z) - cameraPos[2] + reach; + if (result) cache.setVisible(cx, cy, cz); + else cache.setHidden(cx, cy, cz); + } + + public void resetCache() { + this.cache.resetCache(); + } + + private enum Relative { + INSIDE, POSITIVE, NEGATIVE; + public static Relative from(int min, int max, int pos) { + if (max > pos && min > pos) return POSITIVE; + else if (min < pos && max < pos) return NEGATIVE; + return INSIDE; + } + } + + public interface OcclusionCache { + void resetCache(); + void setVisible(int x, int y, int z); + void setHidden(int x, int y, int z); + int getState(int x, int y, int z); + void setLastHidden(); + void setLastVisible(); + } + + // 使用位运算压缩存储状态的缓存实现 + public static class ArrayOcclusionCache implements OcclusionCache { + private final int reachX2; + private final byte[] cache; + private int entry, offset; + + public ArrayOcclusionCache(int reach) { + this.reachX2 = reach * 2; + // 每一个位置占2位 + this.cache = new byte[(reachX2 * reachX2 * reachX2) / 4 + 1]; + } + + @Override + public void resetCache() { + Arrays.fill(cache, (byte) 0); + } + + private void calcIndex(int x, int y, int z) { + int positionKey = x + y * reachX2 + z * reachX2 * reachX2; + entry = positionKey / 4; + offset = (positionKey % 4) * 2; + } + + @Override + public void setVisible(int x, int y, int z) { + calcIndex(x, y, z); + cache[entry] |= 1 << offset; + } + + @Override + public void setHidden(int x, int y, int z) { + calcIndex(x, y, z); + cache[entry] |= 1 << (offset + 1); + } + + @Override + public int getState(int x, int y, int z) { + calcIndex(x, y, z); + return (cache[entry] >> offset) & 3; + } + + @Override + public void setLastVisible() { + cache[entry] |= 1 << offset; + } + + @Override + public void setLastHidden() { + cache[entry] |= 1 << (offset + 1); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/net/momirealms/craftengine/core/util/ResourceConfigUtils.java b/core/src/main/java/net/momirealms/craftengine/core/util/ResourceConfigUtils.java index c399e7b65..415893a3a 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/util/ResourceConfigUtils.java +++ b/core/src/main/java/net/momirealms/craftengine/core/util/ResourceConfigUtils.java @@ -134,7 +134,7 @@ public final class ResourceConfigUtils { } case String s -> { try { - return Integer.parseInt(s); + return Integer.parseInt(s.replace("_", "")); } catch (NumberFormatException e) { throw new LocalizedResourceConfigException("warning.config.type.int", e, s, option); } @@ -231,7 +231,7 @@ public final class ResourceConfigUtils { } case String s -> { try { - return Long.parseLong(s); + return Long.parseLong(s.replace("_", "")); } catch (NumberFormatException e) { throw new LocalizedResourceConfigException("warning.config.type.long", e, s, option); } diff --git a/core/src/main/java/net/momirealms/craftengine/core/world/MutableVec3d.java b/core/src/main/java/net/momirealms/craftengine/core/world/MutableVec3d.java new file mode 100644 index 000000000..1f4a989ac --- /dev/null +++ b/core/src/main/java/net/momirealms/craftengine/core/world/MutableVec3d.java @@ -0,0 +1,127 @@ +package net.momirealms.craftengine.core.world; + +import net.momirealms.craftengine.core.util.MiscUtils; + +public class MutableVec3d implements Position { + public double x; + public double y; + public double z; + + public MutableVec3d(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public MutableVec3d toCenter() { + this.x = MiscUtils.fastFloor(x) + 0.5; + this.y = MiscUtils.fastFloor(y) + 0.5; + this.z = MiscUtils.fastFloor(z) + 0.5; + return this; + } + + public MutableVec3d add(MutableVec3d vec) { + this.x += vec.x; + this.y += vec.y; + this.z += vec.z; + return this; + } + + public MutableVec3d add(double x, double y, double z) { + this.x += x; + this.y += y; + this.z += z; + return this; + } + + public MutableVec3d divide(MutableVec3d vec3d) { + this.x /= vec3d.x; + this.z /= vec3d.z; + this.y /= vec3d.y; + return this; + } + + public MutableVec3d normalize() { + double mag = Math.sqrt(x * x + y * y + z * z); + this.x /= mag; + this.y /= mag; + this.z /= mag; + return this; + } + + public static double distanceToSqr(MutableVec3d vec1, MutableVec3d vec2) { + double dx = vec2.x - vec1.x; + double dy = vec2.y - vec1.y; + double dz = vec2.z - vec1.z; + return dx * dx + dy * dy + dz * dz; + } + + public void set(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public void add(MutableVec3d vec3d, double x, double y, double z) { + this.x += (vec3d.x + x); + this.y += (vec3d.y + y); + this.z += (vec3d.z + z); + } + + public void add(Vec3d vec3d, double x, double y, double z) { + this.x += (vec3d.x + x); + this.y += (vec3d.y + y); + this.z += (vec3d.z + z); + } + + public void setX(double x) { + this.x = x; + } + + public void setY(double y) { + this.y = y; + } + + public void setZ(double z) { + this.z = z; + } + + @Override + public double x() { + return x; + } + + @Override + public double y() { + return y; + } + + @Override + public double z() { + return z; + } + + @Override + public final boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MutableVec3d vec3d)) return false; + return this.x == vec3d.x && this.y == vec3d.y && this.z == vec3d.z; + } + + @Override + public int hashCode() { + int result = Double.hashCode(x); + result = 31 * result + Double.hashCode(y); + result = 31 * result + Double.hashCode(z); + return result; + } + + @Override + public String toString() { + return "Vec3d{" + + "x=" + x + + ", y=" + y + + ", z=" + z + + '}'; + } +} diff --git a/core/src/main/java/net/momirealms/craftengine/core/world/Vec3d.java b/core/src/main/java/net/momirealms/craftengine/core/world/Vec3d.java index e24e99037..7165596a0 100644 --- a/core/src/main/java/net/momirealms/craftengine/core/world/Vec3d.java +++ b/core/src/main/java/net/momirealms/craftengine/core/world/Vec3d.java @@ -72,7 +72,7 @@ public class Vec3d implements Position { public final boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Vec3d vec3d)) return false; - return Double.compare(x, vec3d.x) == 0 && Double.compare(y, vec3d.y) == 0 && Double.compare(z, vec3d.z) == 0; + return this.x == vec3d.x && this.y == vec3d.y && this.z == vec3d.z; } @Override diff --git a/gradle.properties b/gradle.properties index 68950dd75..c374c81c9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ org.gradle.jvmargs=-Xmx1G # Project settings -project_version=0.0.65.11 +project_version=0.0.65.12 config_version=58 lang_version=40 project_group=net.momirealms @@ -47,7 +47,7 @@ mojang_brigadier_version=1.0.18 byte_buddy_version=1.18.1 ahocorasick_version=0.6.3 snake_yaml_version=2.5 -anti_grief_version=1.0.4 +anti_grief_version=1.0.5 nms_helper_version=1.0.135 evalex_version=3.5.0 reactive_streams_version=1.0.4