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+ }
0 commit comments