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

添加self host速率限制

This commit is contained in:
XiaoMoMi
2025-11-21 23:45:23 +08:00
parent c48ad4bbf3
commit 8ef24a336f
9 changed files with 751 additions and 72 deletions

View File

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

View File

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

View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> 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;
}
}

View File

@@ -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<String, Boolean> oneTimePackUrls = Caffeine.newBuilder()
.maximumSize(256)
.maximumSize(1024)
.scheduler(Scheduler.systemScheduler())
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
private final Cache<String, Bucket> 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<FullHttpRequest> {
@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();

View File

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

View File

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

View File

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

View File

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

View File

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