Skip to content

Commit 46b10d9

Browse files
committed
fix(client-mod): better physics and collision management with primitives
1 parent 6936f1f commit 46b10d9

File tree

4 files changed

+556
-0
lines changed

4 files changed

+556
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
package com.moud.client.collision;
2+
3+
import com.moud.client.network.ClientPacketWrapper;
4+
import com.moud.client.physics.ClientPhysicsBodyIds;
5+
import com.moud.client.physics.ClientPhysicsWorld;
6+
import com.moud.network.MoudPackets;
7+
import net.minecraft.client.MinecraftClient;
8+
import net.minecraft.client.world.ClientWorld;
9+
import org.joml.Vector3f;
10+
import org.joml.Quaternionf;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
import java.io.ByteArrayInputStream;
15+
import java.io.IOException;
16+
import java.util.ArrayDeque;
17+
import java.util.Map;
18+
import java.util.Set;
19+
import java.util.concurrent.ConcurrentHashMap;
20+
import java.util.concurrent.ExecutorService;
21+
import java.util.concurrent.Executors;
22+
import java.util.zip.GZIPInputStream;
23+
24+
public final class ClientChunkCollisionManager {
25+
private static final Logger LOGGER = LoggerFactory.getLogger(ClientChunkCollisionManager.class);
26+
private static final ClientChunkCollisionManager INSTANCE = new ClientChunkCollisionManager();
27+
private static final int MAX_REQUESTS_PER_TICK = Integer.getInteger("moud.physics.chunkCollisionRequestsPerTick", 2);
28+
private static final ExecutorService CHUNK_COLLISION_EXECUTOR = Executors.newSingleThreadExecutor(r -> {
29+
Thread t = new Thread(r, "Moud-ChunkCollisionLoader");
30+
t.setDaemon(true);
31+
return t;
32+
});
33+
34+
private final Set<Long> loadedChunks = ConcurrentHashMap.newKeySet();
35+
private final Set<Long> requestedChunks = ConcurrentHashMap.newKeySet();
36+
private final Set<Long> queuedChunks = ConcurrentHashMap.newKeySet();
37+
private final ArrayDeque<Long> requestQueue = new ArrayDeque<>();
38+
private final Map<Long, Integer> loadSequence = new ConcurrentHashMap<>();
39+
40+
private ClientChunkCollisionManager() {
41+
}
42+
43+
public static ClientChunkCollisionManager getInstance() {
44+
return INSTANCE;
45+
}
46+
47+
public void onChunkLoad(int chunkX, int chunkZ) {
48+
long key = packChunkKey(chunkX, chunkZ);
49+
loadedChunks.add(key);
50+
enqueueChunkCollisionRequest(chunkX, chunkZ);
51+
}
52+
53+
public void onChunkUnload(int chunkX, int chunkZ) {
54+
long key = packChunkKey(chunkX, chunkZ);
55+
loadedChunks.remove(key);
56+
requestedChunks.remove(key);
57+
queuedChunks.remove(key);
58+
synchronized (requestQueue) {
59+
requestQueue.remove(key);
60+
}
61+
loadSequence.remove(key);
62+
removeChunkBody(chunkX, chunkZ);
63+
}
64+
65+
public void tick() {
66+
if (MAX_REQUESTS_PER_TICK <= 0) {
67+
return;
68+
}
69+
for (int i = 0; i < MAX_REQUESTS_PER_TICK; i++) {
70+
Long keyObj;
71+
synchronized (requestQueue) {
72+
keyObj = requestQueue.pollFirst();
73+
}
74+
if (keyObj == null) {
75+
break;
76+
}
77+
long key = keyObj;
78+
queuedChunks.remove(key);
79+
if (!loadedChunks.contains(key) || requestedChunks.contains(key)) {
80+
continue;
81+
}
82+
int chunkX = unpackChunkX(key);
83+
int chunkZ = unpackChunkZ(key);
84+
requestedChunks.add(key);
85+
ClientPacketWrapper.sendToServer(new MoudPackets.RequestChunkCollisionPacket(chunkX, chunkZ));
86+
}
87+
}
88+
89+
public void handleChunkCollisionPacket(MoudPackets.ChunkCollisionPacket packet) {
90+
if (packet == null) {
91+
return;
92+
}
93+
94+
MinecraftClient client = MinecraftClient.getInstance();
95+
ClientWorld world = client != null ? client.world : null;
96+
if (world == null) {
97+
return;
98+
}
99+
100+
int chunkX = packet.chunkX();
101+
int chunkZ = packet.chunkZ();
102+
103+
long key = packChunkKey(chunkX, chunkZ);
104+
if (!loadedChunks.contains(key)) {
105+
removeChunkBody(chunkX, chunkZ);
106+
return;
107+
}
108+
109+
if (packet.remove()) {
110+
removeChunkBody(chunkX, chunkZ);
111+
return;
112+
}
113+
int seq = loadSequence.merge(key, 1, Integer::sum);
114+
byte[] vertsBytes = packet.compressedVertices();
115+
byte[] idxBytes = packet.compressedIndices();
116+
if (vertsBytes == null || idxBytes == null) {
117+
return;
118+
}
119+
CHUNK_COLLISION_EXECUTOR.execute(() -> {
120+
float[] vertices = gunzipFloats(vertsBytes);
121+
int[] indices = gunzipInts(idxBytes);
122+
if (vertices == null || indices == null) {
123+
LOGGER.warn(
124+
"Failed to apply chunk collision mesh for ({}, {}): missing/invalid data",
125+
chunkX,
126+
chunkZ
127+
);
128+
return;
129+
}
130+
131+
ClientPhysicsWorld physics = ClientPhysicsWorld.getInstance();
132+
if (!physics.isInitialized()) {
133+
return;
134+
}
135+
var shape = ClientPhysicsWorld.buildMeshShape(vertices, indices, new Vector3f(1, 1, 1));
136+
if (shape == null) {
137+
return;
138+
}
139+
140+
MinecraftClient.getInstance().execute(() -> {
141+
if (!loadedChunks.contains(key) || loadSequence.getOrDefault(key, 0) != seq) {
142+
return;
143+
}
144+
if (!physics.isInitialized()) {
145+
return;
146+
}
147+
long bodyId = ClientPhysicsBodyIds.chunk(chunkX, chunkZ);
148+
physics.addStaticMeshShape(bodyId, shape, new Vector3f(0, 0, 0), new Quaternionf());
149+
});
150+
});
151+
}
152+
153+
public void clear() {
154+
ClientPhysicsWorld physics = ClientPhysicsWorld.getInstance();
155+
if (physics.isInitialized()) {
156+
for (long key : loadedChunks) {
157+
int chunkX = unpackChunkX(key);
158+
int chunkZ = unpackChunkZ(key);
159+
physics.removeStaticMesh(ClientPhysicsBodyIds.chunk(chunkX, chunkZ));
160+
}
161+
}
162+
loadedChunks.clear();
163+
requestedChunks.clear();
164+
queuedChunks.clear();
165+
synchronized (requestQueue) {
166+
requestQueue.clear();
167+
}
168+
loadSequence.clear();
169+
}
170+
171+
private void enqueueChunkCollisionRequest(int chunkX, int chunkZ) {
172+
long key = packChunkKey(chunkX, chunkZ);
173+
if (requestedChunks.contains(key) || !loadedChunks.contains(key) || !queuedChunks.add(key)) {
174+
return;
175+
}
176+
synchronized (requestQueue) {
177+
requestQueue.addLast(key);
178+
}
179+
}
180+
181+
private void removeChunkBody(int chunkX, int chunkZ) {
182+
ClientPhysicsWorld physics = ClientPhysicsWorld.getInstance();
183+
if (!physics.isInitialized()) {
184+
return;
185+
}
186+
physics.removeStaticMesh(ClientPhysicsBodyIds.chunk(chunkX, chunkZ));
187+
}
188+
189+
private static long packChunkKey(int chunkX, int chunkZ) {
190+
return (((long) chunkX) << 32) ^ (chunkZ & 0xFFFF_FFFFL);
191+
}
192+
193+
private static int unpackChunkX(long key) {
194+
return (int) (key >> 32);
195+
}
196+
197+
private static int unpackChunkZ(long key) {
198+
return (int) key;
199+
}
200+
201+
private static float[] gunzipFloats(byte[] data) {
202+
if (data == null || data.length == 0) {
203+
return null;
204+
}
205+
try (GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(data))) {
206+
byte[] buf = gis.readAllBytes();
207+
if (buf.length % 4 != 0) {
208+
return null;
209+
}
210+
int count = buf.length / 4;
211+
float[] out = new float[count];
212+
int o = 0;
213+
for (int i = 0; i < buf.length; i += 4) {
214+
int bits = (buf[i] & 0xFF)
215+
| ((buf[i + 1] & 0xFF) << 8)
216+
| ((buf[i + 2] & 0xFF) << 16)
217+
| ((buf[i + 3] & 0xFF) << 24);
218+
out[o++] = Float.intBitsToFloat(bits);
219+
}
220+
return out;
221+
} catch (IOException ignored) {
222+
return null;
223+
}
224+
}
225+
226+
private static int[] gunzipInts(byte[] data) {
227+
if (data == null || data.length == 0) {
228+
return null;
229+
}
230+
try (GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(data))) {
231+
byte[] buf = gis.readAllBytes();
232+
if (buf.length % 4 != 0) {
233+
return null;
234+
}
235+
int count = buf.length / 4;
236+
int[] out = new int[count];
237+
int o = 0;
238+
for (int i = 0; i < buf.length; i += 4) {
239+
int v = (buf[i] & 0xFF)
240+
| ((buf[i + 1] & 0xFF) << 8)
241+
| ((buf[i + 2] & 0xFF) << 16)
242+
| ((buf[i + 3] & 0xFF) << 24);
243+
out[o++] = v;
244+
}
245+
return out;
246+
} catch (IOException ignored) {
247+
return null;
248+
}
249+
}
250+
}

client-mod/src/main/java/com/moud/client/collision/ModelCollisionManager.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ public long pick(Vec3d origin, Vec3d direction, double maxDistance) {
146146
if (origin == null || direction == null || volumes.isEmpty()) {
147147
return -1L;
148148
}
149+
ClientCollisionManager.RaycastHit meshHit = ClientCollisionManager.raycastAny(origin, direction, maxDistance);
150+
if (meshHit != null) {
151+
return meshHit.modelId();
152+
}
149153
double bestDistance = maxDistance;
150154
long bestId = -1L;
151155
for (ModelCollisionVolume volume : volumes.values()) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.moud.client.physics;
2+
3+
public final class ClientPhysicsBodyIds {
4+
private static final long KIND_PRIMITIVE = 0x8000_0000_0000_0000L;
5+
private static final long KIND_CHUNK = 0xC000_0000_0000_0000L;
6+
private static final long PAYLOAD_MASK = 0x3FFF_FFFF_FFFF_FFFFL;
7+
private static final int CHUNK_COORD_OFFSET = 1 << 30;
8+
9+
private ClientPhysicsBodyIds() {
10+
}
11+
12+
public static long primitive(long primitiveId) {
13+
return KIND_PRIMITIVE | (primitiveId & PAYLOAD_MASK);
14+
}
15+
16+
public static long chunk(int chunkX, int chunkZ) {
17+
long x = (long) chunkX + CHUNK_COORD_OFFSET;
18+
long z = (long) chunkZ + CHUNK_COORD_OFFSET;
19+
if ((x & ~0x7FFF_FFFFL) != 0L || (z & ~0x7FFF_FFFFL) != 0L) {
20+
long fallback = (((long) chunkX) << 32) ^ (chunkZ & 0xFFFF_FFFFL);
21+
return KIND_CHUNK | (fallback & PAYLOAD_MASK);
22+
}
23+
long payload = (x & 0x7FFF_FFFFL) | ((z & 0x7FFF_FFFFL) << 31);
24+
return KIND_CHUNK | payload;
25+
}
26+
}
27+

0 commit comments

Comments
 (0)