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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
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();
|
||||
}
|
||||
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"));
|
||||
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");
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user