Skip to content

Commit 2f8b13b

Browse files
committed
Fine-tuned IPC for sharing player loc between MC instances
1 parent 4b9f257 commit 2f8b13b

6 files changed

Lines changed: 197 additions & 123 deletions

File tree

src/main/java/net/evmodder/evmod/Main.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import net.evmodder.evmod.onTick.AutoPlaceItemFrames;
3535
import net.evmodder.evmod.onTick.ContainerOpenCloseListener;
3636
import net.evmodder.evmod.onTick.MapLoaderBot;
37+
import net.evmodder.evmod.onTick.SyncPlayerPos;
3738
import net.evmodder.evmod.onTick.TooltipMapLoreMetadata;
3839
import net.evmodder.evmod.onTick.TooltipMapNameColor;
3940
import net.evmodder.evmod.onTick.TooltipRepairCost;
@@ -157,6 +158,7 @@ public class Main{
157158
if(settings.onTickContainer) TickListener.register(new UpdateContainerContents());
158159
if(settings.containerOpenCloseListener) TickListener.register(new ContainerOpenCloseListener(kbInvRestock));
159160
if(settings.mapLoaderBot) TickListener.register(new MapLoaderBot());
161+
if(settings.playerMoveListener) TickListener.register(new SyncPlayerPos());
160162

161163
if(settings.tooltipMapHighlights) Tooltip.register(new TooltipMapNameColor());
162164
if(settings.tooltipMapMetadata) Tooltip.register(new TooltipMapLoreMetadata());

src/main/java/net/evmodder/evmod/Settings.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ final class Settings{
1717
final boolean storeDataInInstanceFolder, database, epearlOwners;
1818
final boolean placementHelperIframeAutoPlace, placementHelperMapArt, placementHelperMapArtAutoPlace, placementHelperMapArtAutoRemove;
1919
final boolean serverJoinListener, serverQuitListener, gameMessageListener, gameMessageFilter, blockClickListener;
20-
final boolean onTickInventory, onTickContainer, onTickIframes, containerOpenCloseListener, mapLoaderBot, broadcaster;
20+
final boolean onTickInventory, onTickContainer, onTickIframes, containerOpenCloseListener, mapLoaderBot, playerMoveListener, broadcaster;
2121
final boolean tooltipMapHighlights, tooltipMapMetadata, tooltipRepairCost;
2222
final boolean cmdAssignPearl, cmdDeletedMapsNearby, cmdExportMapImg, cmdMapArtGroup, cmdMapHashCode, cmdSeen, cmdSendAs, cmdTimeOnline;
2323

@@ -78,6 +78,7 @@ private final boolean extractConfigValue(final HashMap<String, Boolean> config,
7878
placementHelperMapArtAutoPlace = placementHelperMapArt && extractConfigValue(settings, "placement_helper.mapart.autoplace");
7979
placementHelperMapArtAutoRemove = placementHelperMapArt && extractConfigValue(settings, "placement_helper.mapart.autoremove");
8080
mapLoaderBot = extractConfigValue(settings, "map_bot.loader");
81+
playerMoveListener = extractConfigValue(settings, "listener.player_move");
8182
serverJoinListener = extractConfigValue(settings, "listener.server_join");
8283
serverQuitListener = extractConfigValue(settings, "listener.server_quit");
8384
blockClickListener = extractConfigValue(settings, "listener.block_click");

src/main/java/net/evmodder/evmod/apis/JavaIPC.java

Lines changed: 0 additions & 121 deletions
This file was deleted.
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package net.evmodder.evmod.apis;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.io.RandomAccessFile;
6+
import java.lang.invoke.MethodHandles;
7+
import java.lang.invoke.VarHandle;
8+
import java.nio.ByteOrder;
9+
import java.nio.MappedByteBuffer;
10+
import java.nio.channels.FileChannel;
11+
import java.util.function.Consumer;
12+
13+
public final class PlayerPosIPC{
14+
private static final class Holder{private static final PlayerPosIPC INSTANCE = new PlayerPosIPC();}
15+
public static final PlayerPosIPC getInstance(){return Holder.INSTANCE;}
16+
17+
// Hopefully nobody is running more than this many Minecraft accounts on 1 device...
18+
private static final int MAX_SLOTS = 64;
19+
// UUID + serverHashCode + worldHashCode + x + y + z
20+
public static final int DATA_SIZE = 16 + 8 + 8 + 8 + 4 + 4; //=48
21+
// PID + TS + lock + data
22+
private static final int SLOT_SIZE = 8 + 8 + 8 + DATA_SIZE;
23+
// Treat PID as "dead" if no update for > 15s
24+
private static final long TIMEOUT_NS = 15_000l * 1000000l;
25+
26+
private static final int CLAIM_LOOP_MAX_ATTEMPTS = 3;
27+
private static final int LAST_SLOT_BASE = (MAX_SLOTS-1)*SLOT_SIZE; // Cached compuation
28+
29+
// Offsets within a slot
30+
private static final int TIME_OFFSET = 8;
31+
private static final int VERSION_OFFSET = 16;
32+
private static final int DATA_OFFSET = 24;
33+
34+
// private static final String MEM_TABLE_FILENAME = "minecraft_player_pos.bin";
35+
private static final VarHandle LONG_HANDLE = MethodHandles.byteBufferViewVarHandle(long[].class, ByteOrder.nativeOrder());
36+
37+
private static final long myPID = ProcessHandle.current().pid();
38+
private final long[] lastReadVersions = new long[MAX_SLOTS]; // Can't be static (static variables are shared across the entire JVM)
39+
private final byte[] data = new byte[DATA_SIZE];
40+
private final MappedByteBuffer buffer;
41+
private int mySlot;
42+
43+
private final void freeSlot(final long owner, final int base){
44+
if(base == LAST_SLOT_BASE) LONG_HANDLE.compareAndSet(buffer, base, owner, 0l);
45+
else if((long)LONG_HANDLE.getVolatile(buffer, base + SLOT_SIZE) != 0l) LONG_HANDLE.compareAndSet(buffer, base, owner, -1l);
46+
else if(LONG_HANDLE.compareAndSet(buffer, base, owner, -2l)){
47+
final long clearedVal = (long)LONG_HANDLE.getVolatile(buffer, base + SLOT_SIZE) == 0l ? 0l : -1l;
48+
LONG_HANDLE.compareAndSet(buffer, base, -2l, clearedVal);
49+
}
50+
}
51+
52+
private PlayerPosIPC(){
53+
final File file = new File(System.getProperty("java.io.tmpdir"), "minecraft_player_pos.bin");
54+
try(final RandomAccessFile raf = new RandomAccessFile(file, "rw")){
55+
final long TOTAL_SIZE = (long) MAX_SLOTS*SLOT_SIZE;
56+
if(raf.length() < TOTAL_SIZE) raf.setLength(TOTAL_SIZE);
57+
buffer = raf.getChannel().map(FileChannel.MapMode.READ_WRITE, 0, TOTAL_SIZE);
58+
}
59+
catch(IOException e){
60+
System.err.println("[EvMod] CRITICAL: Failed to initialize shared memory file");
61+
throw new ExceptionInInitializerError(e);
62+
}
63+
Runtime.getRuntime().addShutdownHook(new Thread(()->{
64+
// Mark owner PID as -1 so others can take it immediately (rather than waiting for heartbeat)
65+
final int base = mySlot*SLOT_SIZE;
66+
if(buffer != null && myPID == (long)LONG_HANDLE.getVolatile(buffer, base)) freeSlot(myPID, base);
67+
}));
68+
}
69+
70+
private final int claimSlot(){
71+
final long now = System.nanoTime();
72+
for(int j=0; j<CLAIM_LOOP_MAX_ATTEMPTS; ++j){
73+
for(int i=0; i<MAX_SLOTS; ++i){
74+
final int base = i*SLOT_SIZE;
75+
final long owner = (long)LONG_HANDLE.getVolatile(buffer, base);
76+
final long lastHeartbeat = (long)LONG_HANDLE.getVolatile(buffer, base + TIME_OFFSET);
77+
if(owner > 0l && now - lastHeartbeat < TIMEOUT_NS) continue;
78+
LONG_HANDLE.setVolatile(buffer, base + TIME_OFFSET, now); // Update ts (reduces contention fighting for this slot)
79+
if(LONG_HANDLE.compareAndSet(buffer, base, owner, myPID)) return i; // Nice, we snagged this slot!
80+
// i=-1; // Another PID grabbed the slot before us; start again from i=0.
81+
}
82+
}
83+
assert false : "shared-mem table is out of slots!";
84+
return -1; // Failed to acquire a slot!!
85+
}
86+
87+
public final void postData(final byte[] data){
88+
assert data.length == DATA_SIZE;
89+
// Ensure my slot is still valid (and update it if not).
90+
if(myPID != (long)LONG_HANDLE.getVolatile(buffer, mySlot*SLOT_SIZE) && (mySlot=claimSlot()) == -1) return;
91+
final int base = mySlot*SLOT_SIZE;
92+
final long currentVer = (long)LONG_HANDLE.get(buffer, base + VERSION_OFFSET); // Get current version
93+
final long nextEvenVer = (currentVer+1l)&-2l;
94+
LONG_HANDLE.setRelease(buffer, base + VERSION_OFFSET, nextEvenVer); // Move current version to EVEN (signals write in-progress)
95+
LONG_HANDLE.setVolatile(buffer, base + TIME_OFFSET, System.nanoTime()); // Heartbeat
96+
buffer.put(base + DATA_OFFSET, data); // Publish data
97+
LONG_HANDLE.setRelease(buffer, base + VERSION_OFFSET, nextEvenVer + 1l); // Move current version to ODD (signals write completed)
98+
}
99+
100+
public final void readData(final Consumer<byte[]> consumer){
101+
final long now = System.nanoTime();
102+
for(int i=0; i<MAX_SLOTS; ++i){
103+
final int base = i*SLOT_SIZE;
104+
final long owner = (long)LONG_HANDLE.getVolatile(buffer, base);
105+
if(owner < 0l || owner == myPID) continue;
106+
if(owner == 0l) return;
107+
final long lastHeartbeat = (long)LONG_HANDLE.getVolatile(buffer, base + TIME_OFFSET);
108+
if(now - lastHeartbeat > TIMEOUT_NS){freeSlot(owner, base); continue;}
109+
// while(((long)LONG_HANDLE.getAcquire(buffer, base + VERSION_OFFSET)&1) == 0) Thread.onSpinWait();
110+
final long version = (long)LONG_HANDLE.getAcquire(buffer, base + VERSION_OFFSET);
111+
if((version&1l) == 0l || version == lastReadVersions[i]) continue; // Skip busy or stale
112+
buffer.get(base + DATA_OFFSET, data); // Read data
113+
if(version == (long)LONG_HANDLE.getVolatile(buffer, base + VERSION_OFFSET)){ // Verify data wasn't altered mid-read
114+
consumer.accept(data);
115+
lastReadVersions[i] = version;
116+
}
117+
}
118+
}
119+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package net.evmodder.evmod.onTick;
2+
3+
import java.nio.ByteBuffer;
4+
import java.util.HashMap;
5+
import java.util.UUID;
6+
import net.evmodder.evmod.apis.MiscUtils;
7+
import net.evmodder.evmod.apis.PlayerPosIPC;
8+
import net.evmodder.evmod.apis.TickListener;
9+
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
10+
import net.minecraft.client.MinecraftClient;
11+
import net.minecraft.client.network.OtherClientPlayerEntity;
12+
import net.minecraft.client.network.PlayerListEntry;
13+
import net.minecraft.client.render.WorldRenderer;
14+
import net.minecraft.client.util.math.MatrixStack;
15+
import net.minecraft.util.math.BlockPos;
16+
import net.minecraft.util.math.Vec3d;
17+
18+
public final class SyncPlayerPos implements TickListener{
19+
private final static boolean ONLY_SHOW_PLAYERS_IN_LOADED_CHUNKS = true;
20+
private final static ByteBuffer bb = ByteBuffer.allocate(PlayerPosIPC.DATA_SIZE);
21+
private final MinecraftClient client = MinecraftClient.getInstance();
22+
23+
@Override public final void onTickEnd(final MinecraftClient client){
24+
if(client.player == null || client.world == null) return;
25+
bb.putInt(MiscUtils.getServerAddressHashCode());
26+
bb.putInt(MiscUtils.getDimensionId(client.world));
27+
bb.putLong(client.player.getUuid().getMostSignificantBits()).putLong(client.player.getUuid().getLeastSignificantBits());
28+
bb.putDouble(client.player.getX()).putDouble(client.player.getY()).putDouble(client.player.getZ());
29+
PlayerPosIPC.getInstance().postData(bb.array());
30+
bb.rewind();
31+
}
32+
33+
public SyncPlayerPos(){
34+
final HashMap<UUID, OtherClientPlayerEntity> fakePlayers = new HashMap<>();
35+
WorldRenderEvents.AFTER_ENTITIES.register(context -> {
36+
if (client.world == null || client.getNetworkHandler() == null) return;
37+
38+
final int myServer = MiscUtils.getServerAddressHashCode(), myWorld = MiscUtils.getDimensionId(client.world);
39+
PlayerPosIPC.getInstance().readData(b -> {
40+
final ByteBuffer bb = ByteBuffer.wrap(b);
41+
final int server = bb.getInt(), world = bb.getInt();
42+
if(server != myServer || world != myWorld) return;
43+
final UUID uuid = new UUID(bb.getLong(), bb.getLong());
44+
if(client.world.getPlayerByUuid(uuid) != null) return; // Already loaded
45+
final PlayerListEntry entry = client.getNetworkHandler().getPlayerListEntry(uuid);
46+
if(entry == null) return; // Not online!
47+
final double x = bb.getDouble(), y = bb.getDouble(), z = bb.getDouble();
48+
final BlockPos bp = BlockPos.ofFloored(x, y, z);
49+
final int light;
50+
if(!client.world.getChunkManager().isChunkLoaded(bp.getX() >> 4, bp.getZ() >> 4)){
51+
if(ONLY_SHOW_PLAYERS_IN_LOADED_CHUNKS) return;
52+
light = 0xF000F0; // Full brightness lightmap
53+
}
54+
else light = WorldRenderer.getLightmapCoordinates(client.world, bp);
55+
56+
final OtherClientPlayerEntity dummy = fakePlayers.computeIfAbsent(uuid, _0->{
57+
OtherClientPlayerEntity d = new OtherClientPlayerEntity(client.world, entry.getProfile());
58+
d.prevX = x; d.prevY = y; d.prevZ = z;
59+
return d;
60+
});
61+
dummy.setPos(x, y, z);
62+
63+
final float tickDelta = context.tickCounter().getTickDelta(true);
64+
final MatrixStack matrices = context.matrixStack();
65+
final Vec3d cameraPos = context.camera().getPos();
66+
matrices.push();
67+
matrices.translate(x - cameraPos.x, y - cameraPos.y, z - cameraPos.z);
68+
client.getEntityRenderDispatcher().render(dummy, 0d, 0d, 0d, tickDelta, matrices, context.consumers(), light);
69+
matrices.pop();
70+
});
71+
});
72+
}
73+
}

src/main/resources/assets/evmod/settings.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ database: true
3434
#
3535
listener.game_message.read: true # registers: listener, allows: share_ignores, whisper_sound, whisper_pearl_pull
3636
listener.game_message.filter: true # registers: listener, allows: borrow_ignores
37-
listener.player_move: true # registers: onTick(), allows: player_pos_sync
37+
listener.player_move: true # registers: onTick(), listener.worldRender, allows: player_pos_sync
3838
map_bot.loader: true # registers: onTick()
3939
broadcaster: false # registers: onTimer()
4040
epearl_owners: true # registers: onTick(), listener.chunkLoad, listener.chunkUnload, allows: uuid/xz

0 commit comments

Comments
 (0)