1515
1616import java .io .IOException ;
1717import java .io .OutputStream ;
18+ import java .net .InetAddress ;
1819import java .net .InetSocketAddress ;
20+ import java .nio .charset .StandardCharsets ;
1921import java .util .Locale ;
22+ import java .util .concurrent .ExecutorService ;
2023import java .util .concurrent .Executors ;
2124
2225public class PlayerCoordsAPIClient implements ClientModInitializer {
26+ private static final int PORT = 25565 ;
27+ private static final long START_RETRY_DELAY_MS = 5_000L ;
28+
2329 private HttpServer server ;
30+ private ExecutorService serverExecutor ;
2431 private boolean serverStarted = false ;
32+ private boolean lastConfigEnabled ;
33+ private long nextStartAttemptAt = 0L ;
2534 private volatile PlayerSnapshot latestSnapshot ;
26- // Hardcoded port value - no longer in config
27- private static final int PORT = 25565 ;
2835
2936 @ Override
3037 public void onInitializeClient () {
31- // Start server on init if enabled
32- if (PlayerCoordsAPI .getConfig ().enabled ) {
33- startServer ();
38+ lastConfigEnabled = PlayerCoordsAPI .getConfig ().enabled ;
39+
40+ if (lastConfigEnabled ) {
41+ tryStartServer ();
3442 }
3543
36- // Register tick event to constantly check config status
3744 ClientTickEvents .END_CLIENT_TICK .register (client -> {
3845 updateSnapshot (client );
39- boolean configEnabled = PlayerCoordsAPI .getConfig ().enabled ;
40-
41- // If enabled and server not started, start server
42- if (configEnabled && !serverStarted ) {
43- startServer ();
44- }
45-
46- // If disabled and server is running, stop server
47- if (!configEnabled && serverStarted ) {
48- stopServer ();
49- }
46+ handleConfigState (PlayerCoordsAPI .getConfig ().enabled );
5047 });
5148
5249 ClientWorldEvents .AFTER_CLIENT_WORLD_CHANGE .register ((client , world ) -> updateSnapshot (client ));
@@ -59,6 +56,26 @@ public void onInitializeClient() {
5956 PlayerCoordsAPI .LOGGER .info ("Registered config monitor" );
6057 }
6158
59+ private void handleConfigState (boolean configEnabled ) {
60+ if (configEnabled != lastConfigEnabled ) {
61+ lastConfigEnabled = configEnabled ;
62+
63+ if (configEnabled ) {
64+ nextStartAttemptAt = 0L ;
65+ tryStartServer ();
66+ } else {
67+ nextStartAttemptAt = 0L ;
68+ stopServer ();
69+ }
70+
71+ return ;
72+ }
73+
74+ if (configEnabled && !serverStarted ) {
75+ tryStartServer ();
76+ }
77+ }
78+
6279 private void updateSnapshot (MinecraftClient client ) {
6380 PlayerEntity player = client .player ;
6481 ClientWorld worldObj = client .world ;
@@ -81,7 +98,7 @@ private void updateSnapshot(MinecraftClient client) {
8198 player .getPitch (),
8299 worldObj .getRegistryKey ().getValue ().toString (),
83100 biome ,
84- player .getUuid (). toString (),
101+ player .getUuidAsString (),
85102 player .getName ().getString ()
86103 );
87104 }
@@ -90,39 +107,60 @@ private void clearSnapshot() {
90107 latestSnapshot = null ;
91108 }
92109
93- private void startServer () {
94- if (serverStarted ) return ;
110+ private void tryStartServer () {
111+ if (serverStarted ) {
112+ return ;
113+ }
114+
115+ long now = System .currentTimeMillis ();
116+
117+ if (now < nextStartAttemptAt ) {
118+ return ;
119+ }
95120
96121 try {
97122 PlayerCoordsAPI .LOGGER .info ("Starting PlayerCoordsAPI HTTP server on port " + PORT );
98- server = HttpServer .create (new InetSocketAddress (PORT ), 0 );
123+ server = HttpServer .create (new InetSocketAddress (InetAddress . getLoopbackAddress (), PORT ), 0 );
99124 server .createContext ("/api/coords" , this ::handleCoordsRequest );
100- server .setExecutor (Executors .newSingleThreadExecutor ());
125+ serverExecutor = Executors .newSingleThreadExecutor ();
126+ server .setExecutor (serverExecutor );
101127 server .start ();
102128 serverStarted = true ;
129+ nextStartAttemptAt = 0L ;
103130 PlayerCoordsAPI .LOGGER .info ("PlayerCoordsAPI HTTP server started successfully" );
104131 } catch (IOException e ) {
105- PlayerCoordsAPI .LOGGER .error ("Failed to start PlayerCoordsAPI HTTP server" , e );
132+ cleanupServerResources ();
133+ nextStartAttemptAt = now + START_RETRY_DELAY_MS ;
134+ PlayerCoordsAPI .LOGGER .warn (
135+ "Failed to start PlayerCoordsAPI HTTP server, retrying in {} seconds" ,
136+ START_RETRY_DELAY_MS / 1000 ,
137+ e
138+ );
106139 }
107140 }
108141
109142 private void stopServer () {
143+ if (server == null && serverExecutor == null ) {
144+ return ;
145+ }
146+
147+ PlayerCoordsAPI .LOGGER .info ("Stopping PlayerCoordsAPI HTTP server" );
148+ cleanupServerResources ();
149+ PlayerCoordsAPI .LOGGER .info ("PlayerCoordsAPI HTTP server stopped successfully" );
150+ }
151+
152+ private void cleanupServerResources () {
110153 if (server != null ) {
111- PlayerCoordsAPI .LOGGER .info ("Stopping PlayerCoordsAPI HTTP server" );
112-
113- // Create a separate thread to stop the server to prevent blocking
114- final HttpServer serverToStop = server ; // Create a final reference for the thread
115- Thread stopThread = new Thread (() -> {
116- serverToStop .stop (0 ); // Stop with no delay
117- PlayerCoordsAPI .LOGGER .info ("PlayerCoordsAPI HTTP server stopped successfully" );
118- });
119- stopThread .setDaemon (true );
120- stopThread .start ();
121-
122- // Set variables immediately so we know the server is being stopped
154+ server .stop (0 );
123155 server = null ;
124- serverStarted = false ;
125156 }
157+
158+ if (serverExecutor != null ) {
159+ serverExecutor .shutdown ();
160+ serverExecutor = null ;
161+ }
162+
163+ serverStarted = false ;
126164 }
127165
128166 private void handleCoordsRequest (HttpExchange exchange ) throws IOException {
@@ -133,8 +171,8 @@ private void handleCoordsRequest(HttpExchange exchange) throws IOException {
133171 }
134172
135173 // Check if the client is allowed to access (only localhost)
136- String remoteAddress = exchange .getRemoteAddress ().getAddress (). getHostAddress ();
137- if (! remoteAddress . equals ( "127.0.0.1" ) && !remoteAddress .equals ( "0:0:0:0:0:0:0:1" )) {
174+ InetAddress remoteAddress = exchange .getRemoteAddress ().getAddress ();
175+ if (remoteAddress == null || !remoteAddress .isLoopbackAddress ( )) {
138176 sendResponse (exchange , 403 , "{\" error\" : \" Access denied\" }" );
139177 return ;
140178 }
@@ -153,16 +191,43 @@ private void sendResponse(HttpExchange exchange, int statusCode, String response
153191 exchange .getResponseHeaders ().set ("Access-Control-Allow-Methods" , "GET, OPTIONS" );
154192 exchange .getResponseHeaders ().set ("Access-Control-Allow-Headers" , "Content-Type, Authorization" );
155193
156- // Set content type if response is not null
157194 if (response != null ) {
158- exchange .getResponseHeaders ().set ("Content-Type" , "application/json" );
159- exchange .sendResponseHeaders (statusCode , response .length ());
195+ byte [] responseBytes = response .getBytes (StandardCharsets .UTF_8 );
196+ exchange .getResponseHeaders ().set ("Content-Type" , "application/json; charset=utf-8" );
197+ exchange .sendResponseHeaders (statusCode , responseBytes .length );
160198 try (OutputStream os = exchange .getResponseBody ()) {
161- os .write (response . getBytes () );
199+ os .write (responseBytes );
162200 }
163201 } else {
164- exchange .sendResponseHeaders (statusCode , -1 ); // No response body
202+ exchange .sendResponseHeaders (statusCode , -1 );
203+ }
204+ }
205+
206+ private static String escapeJson (String value ) {
207+ StringBuilder escaped = new StringBuilder (value .length () + 16 );
208+
209+ for (int i = 0 ; i < value .length (); i ++) {
210+ char ch = value .charAt (i );
211+
212+ switch (ch ) {
213+ case '\\' -> escaped .append ("\\ \\ " );
214+ case '"' -> escaped .append ("\\ \" " );
215+ case '\b' -> escaped .append ("\\ b" );
216+ case '\f' -> escaped .append ("\\ f" );
217+ case '\n' -> escaped .append ("\\ n" );
218+ case '\r' -> escaped .append ("\\ r" );
219+ case '\t' -> escaped .append ("\\ t" );
220+ default -> {
221+ if (ch < 0x20 ) {
222+ escaped .append (String .format (Locale .ROOT , "\\ u%04x" , (int ) ch ));
223+ } else {
224+ escaped .append (ch );
225+ }
226+ }
227+ }
165228 }
229+
230+ return escaped .toString ();
166231 }
167232
168233 private record PlayerSnapshot (
@@ -179,7 +244,15 @@ private record PlayerSnapshot(
179244 private String toJson () {
180245 return String .format (Locale .US ,
181246 "{\" x\" : %.2f, \" y\" : %.2f, \" z\" : %.2f, \" yaw\" : %.2f, \" pitch\" : %.2f, \" world\" : \" %s\" , \" biome\" : \" %s\" , \" uuid\" : \" %s\" , \" username\" : \" %s\" }" ,
182- x , y , z , yaw , pitch , world , biome , uuid , username
247+ x ,
248+ y ,
249+ z ,
250+ yaw ,
251+ pitch ,
252+ escapeJson (world ),
253+ escapeJson (biome ),
254+ escapeJson (uuid ),
255+ escapeJson (username )
183256 );
184257 }
185258 }
0 commit comments