diff --git a/common/src/main/java/com/github/squi2rel/mcft/AutoBlink.java b/common/src/main/java/com/github/squi2rel/mcft/AutoBlink.java index 80ab275..0ebd6d2 100644 --- a/common/src/main/java/com/github/squi2rel/mcft/AutoBlink.java +++ b/common/src/main/java/com/github/squi2rel/mcft/AutoBlink.java @@ -20,12 +20,12 @@ public class AutoBlink { private static long lastUpdateTime = System.currentTimeMillis(); public static void update() { - if (!config.autoBlink || config.autoSwitchBlink && OSC.lastReceived != lastUpdateTime && System.currentTimeMillis() - OSC.lastReceived < 3000) { + long currentTime = System.currentTimeMillis(); + if (!config.autoBlink || config.autoSwitchBlink && currentTime - OSC.lastOscReceived < 3000) { enabled = false; return; } enabled = true; - long currentTime = System.currentTimeMillis(); float delta = currentTime - lastUpdateTime; lastUpdateTime = currentTime; last += delta; @@ -50,6 +50,5 @@ public static void update() { } } model.eyeL.percent = model.eyeR.percent = eyeOpenness * config.blinkMaxY; - OSC.lastReceived = lastUpdateTime; } } diff --git a/common/src/main/java/com/github/squi2rel/mcft/Config.java b/common/src/main/java/com/github/squi2rel/mcft/Config.java index ce32be1..5c2bf44 100644 --- a/common/src/main/java/com/github/squi2rel/mcft/Config.java +++ b/common/src/main/java/com/github/squi2rel/mcft/Config.java @@ -4,6 +4,8 @@ public class Config { public int httpPort = 8999; public int oscReceivePort = 9000; public int oscSendPort = 9001; + public String oscBindHost = "127.0.0.1"; + public String oscTargetHost = "127.0.0.1"; public FTModel model = new FTModel(); public float eyeXMul = 0.5f, eyeYMul = 0.3f; diff --git a/common/src/main/java/com/github/squi2rel/mcft/MCFT.java b/common/src/main/java/com/github/squi2rel/mcft/MCFT.java index a5b1d79..d4cfb92 100644 --- a/common/src/main/java/com/github/squi2rel/mcft/MCFT.java +++ b/common/src/main/java/com/github/squi2rel/mcft/MCFT.java @@ -4,7 +4,6 @@ import com.github.squi2rel.mcft.network.TrackingParamsPayload; import com.github.squi2rel.mcft.network.TrackingUpdatePayload; import com.google.gson.Gson; -import net.minecraft.client.resource.language.I18n; import net.minecraft.server.network.ServerPlayerEntity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,7 +30,7 @@ public static void onInitialize() { ServerPacketHandler.registerC2S(TrackingParamsPayload.ID, TrackingParamsPayload.CODEC, (payload, p) -> { FTModel old = models.get(p.getUuid()); - if (old == null) LOGGER.info(I18n.translate("mcft.logging.playerconnected", Objects.requireNonNull(p.getDisplayName()).getString())); + if (old == null) LOGGER.info("Player {} is using MCFT", Objects.requireNonNull(p.getDisplayName()).getString()); FTModel model = new FTModel(payload.eyeR(), payload.eyeL(), payload.mouth(), payload.flat()); model.validate(true); if (old != null) model.enabled = old.enabled; @@ -49,7 +48,7 @@ public static void onInitialize() { model.validate(false); if (!model.enabled) { model.enabled = true; - LOGGER.info(I18n.translate("mcft.logging.oscreceived", Objects.requireNonNull(p.getDisplayName()).getString())); + LOGGER.info("Player {} has OSC connected", Objects.requireNonNull(p.getDisplayName()).getString()); TrackingParamsPayload packet = new TrackingParamsPayload(p.getUuid(), model.eyeR, model.eyeL, model.mouth, model.isFlat); for (ServerPlayerEntity player : Objects.requireNonNull(p.getServer()).getPlayerManager().getPlayerList()) ServerPacketHandler.sendS2C(player, packet); } @@ -59,6 +58,10 @@ public static void onInitialize() { )) ServerPacketHandler.sendS2C(player, packet); }); + ServerPacketHandler.registerS2C(TrackingParamsPayload.ID, TrackingParamsPayload.CODEC); + ServerPacketHandler.registerS2C(TrackingUpdatePayload.ID, TrackingUpdatePayload.CODEC); + ServerPacketHandler.registerS2C(ConfigPayload.ID, ConfigPayload.CODEC); + Platform.register(); } @@ -95,4 +98,4 @@ public static void saveConfig(Object config, Path path) { throw new RuntimeException(e); } } -} \ No newline at end of file +} diff --git a/common/src/main/java/com/github/squi2rel/mcft/MCFTClient.java b/common/src/main/java/com/github/squi2rel/mcft/MCFTClient.java index d673331..9f46f1c 100644 --- a/common/src/main/java/com/github/squi2rel/mcft/MCFTClient.java +++ b/common/src/main/java/com/github/squi2rel/mcft/MCFTClient.java @@ -67,6 +67,7 @@ public static void onInitializeClient() { public static void update() { if (MC.player == null || MC.world == null) return; AutoBlink.update(); + OSC.applyPendingParameters(); if (model.active() && System.currentTimeMillis() - lastSync > 1000 / fps) { FTClient.writeSync(model); lastSync = System.currentTimeMillis(); @@ -86,4 +87,4 @@ private static boolean checkVersion(String v) { if (p1.length < 2 || p2.length < 2) return false; return p1[0].equals(p2[0]) && p1[1].equals(p2[1]); } -} \ No newline at end of file +} diff --git a/common/src/main/java/com/github/squi2rel/mcft/ServerPacketHandler.java b/common/src/main/java/com/github/squi2rel/mcft/ServerPacketHandler.java index 6d2de88..b0ae473 100644 --- a/common/src/main/java/com/github/squi2rel/mcft/ServerPacketHandler.java +++ b/common/src/main/java/com/github/squi2rel/mcft/ServerPacketHandler.java @@ -15,6 +15,11 @@ public static
void registerC2S(CustomPayload.Id
id, throw new AssertionError(); } + @ExpectPlatform + public static
void registerS2C(CustomPayload.Id
id, PacketCodec void sendS2C(ServerPlayerEntity player, P packet) {
throw new AssertionError();
diff --git a/common/src/main/java/com/github/squi2rel/mcft/services/DNS.java b/common/src/main/java/com/github/squi2rel/mcft/services/DNS.java
index b8f6f5a..84ec3f1 100644
--- a/common/src/main/java/com/github/squi2rel/mcft/services/DNS.java
+++ b/common/src/main/java/com/github/squi2rel/mcft/services/DNS.java
@@ -7,24 +7,67 @@
import java.util.concurrent.TimeUnit;
public class DNS {
+ private static final Object lock = new Object();
+ private static volatile boolean running;
+ private static Thread thread;
public static final int port = 5353;
public static void init() {
- Thread dns = new Thread(() -> {
- while (true) {
- try {
- TimeUnit.SECONDS.sleep(10);
- sendReply();
- } catch (InterruptedException ignored) {
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
+ synchronized (lock) {
+ if (running) return;
+ running = true;
+ Thread candidate = new Thread(DNS::run);
+ candidate.setName("MCFT DNS");
+ candidate.setDaemon(true);
+ thread = candidate;
+ try {
+ candidate.start();
+ } catch (RuntimeException e) {
+ thread = null;
+ running = false;
+ throw e;
}
- });
- dns.setDaemon(true);
- dns.start();
+ }
MCFT.LOGGER.info("DNS started on port {}", port);
}
+ public static void shutdown() {
+ Thread current;
+ synchronized (lock) {
+ running = false;
+ current = thread;
+ thread = null;
+ }
+ if (current != null) current.interrupt();
+ }
+
+ private static void run() {
+ Thread current = Thread.currentThread();
+ boolean failureLogged = false;
+ while (running && thread == current) {
+ try {
+ sendReply();
+ if (failureLogged) MCFT.LOGGER.info("DNS replies resumed");
+ failureLogged = false;
+ } catch (Exception e) {
+ if (!failureLogged) MCFT.LOGGER.error("DNS reply failed", e);
+ failureLogged = true;
+ }
+ if (!running || thread != current) break;
+ try {
+ TimeUnit.SECONDS.sleep(10);
+ } catch (InterruptedException ignored) {
+ current.interrupt();
+ break;
+ }
+ }
+ synchronized (lock) {
+ if (thread == current) {
+ thread = null;
+ running = false;
+ }
+ }
+ }
+
private static void sendReply() throws Exception {
Message response = new Message();
Header header = response.getHeader();
@@ -44,4 +87,4 @@ private static void sendReply() throws Exception {
socket.send(packet);
}
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/java/com/github/squi2rel/mcft/services/HTTP.java b/common/src/main/java/com/github/squi2rel/mcft/services/HTTP.java
index f456fff..b883d9d 100644
--- a/common/src/main/java/com/github/squi2rel/mcft/services/HTTP.java
+++ b/common/src/main/java/com/github/squi2rel/mcft/services/HTTP.java
@@ -14,8 +14,12 @@
import net.minecraft.client.session.Session;
import java.io.*;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
@@ -25,37 +29,168 @@
import java.util.function.Consumer;
public class HTTP {
+ private static final Object lifecycleLock = new Object();
+ private static final Object lock = new Object();
+ private static final String HTTP_LOOPBACK_HOST = "127.0.0.1";
+ private static final int SOCKET_READ_TIMEOUT = 5000;
+ private static final int MAX_HEADER_BYTES = 16384;
public static final int port = MCFTClient.config.httpPort;
- public static void init() throws IOException {
- createInfo();
- Thread http = new Thread(() -> {
- try (ServerSocket serverSocket = new ServerSocket(port)) {
- OSC.init();
+ private static ServerSocket serverSocket;
+ private static Thread httpThread;
+
+ public static void init() {
+ synchronized (lifecycleLock) {
+ initServices();
+ }
+ }
+
+ private static void initServices() {
+ try {
+ OSC.init();
+ } catch (Exception e) {
+ MCFT.LOGGER.error("OSC start failed", e);
+ }
+ if (MinecraftClient.getInstance().getSession().getUuidOrNull() == null) {
+ MCFT.LOGGER.warn("OSCQuery start skipped without a session UUID");
+ return;
+ }
+ try {
+ createInfo();
+ } catch (Exception e) {
+ MCFT.LOGGER.error("OSC avatar info creation failed", e);
+ }
+ boolean httpStarted = false;
+ try {
+ httpStarted = startServer();
+ } catch (Exception e) {
+ MCFT.LOGGER.error("HTTP start failed", e);
+ }
+ if (httpStarted) {
+ try {
DNS.init();
- MCFT.LOGGER.info("HTTP started on port {}", port);
- while (true) {
- try (Socket s = serverSocket.accept()) {
- BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));
- while (true) {
- String headerLine = in.readLine();
- if (headerLine == null || headerLine.isEmpty()) {
- break;
- }
- }
- String response = getString();
- OutputStream out = s.getOutputStream();
- out.write(response.getBytes(StandardCharsets.UTF_8));
- out.flush();
- } catch (IOException ignored) {
- }
- }
} catch (Exception e) {
- MCFT.LOGGER.error("Service start failed", e);
+ MCFT.LOGGER.error("DNS start failed", e);
}
- });
- http.setName("MCFT Service");
- http.setDaemon(true);
- http.start();
+ }
+ }
+
+ public static void shutdown() {
+ synchronized (lifecycleLock) {
+ shutdownServices();
+ }
+ }
+
+ private static void shutdownServices() {
+ ServerSocket current;
+ synchronized (lock) {
+ current = serverSocket;
+ serverSocket = null;
+ httpThread = null;
+ }
+ if (current != null) {
+ try {
+ current.close();
+ } catch (IOException e) {
+ MCFT.LOGGER.error("Failed to close HTTP server", e);
+ }
+ }
+ OSC.shutdown();
+ DNS.shutdown();
+ }
+
+ private static boolean startServer() throws IOException {
+ synchronized (lock) {
+ if (serverSocket != null && !serverSocket.isClosed() && httpThread != null && httpThread.isAlive()) return true;
+ requirePort(port);
+ ServerSocket candidate = new ServerSocket();
+ try {
+ candidate.bind(new InetSocketAddress(InetAddress.getByName(HTTP_LOOPBACK_HOST), port));
+ Thread thread = new Thread(() -> runServer(candidate));
+ thread.setName("MCFT HTTP");
+ thread.setDaemon(true);
+ serverSocket = candidate;
+ httpThread = thread;
+ thread.start();
+ MCFT.LOGGER.info("HTTP started on {}:{}", HTTP_LOOPBACK_HOST, port);
+ return true;
+ } catch (IOException | RuntimeException e) {
+ if (serverSocket == candidate) {
+ serverSocket = null;
+ httpThread = null;
+ }
+ try {
+ candidate.close();
+ } catch (IOException closeException) {
+ e.addSuppressed(closeException);
+ }
+ throw e;
+ }
+ }
+ }
+
+ private static void runServer(ServerSocket socket) {
+ try {
+ while (!socket.isClosed()) {
+ Socket client;
+ try {
+ client = socket.accept();
+ } catch (IOException e) {
+ if (!socket.isClosed()) MCFT.LOGGER.error("HTTP accept failed", e);
+ break;
+ }
+ try (client) {
+ handleClient(client);
+ } catch (SocketTimeoutException | SocketException ignored) {
+ } catch (IOException e) {
+ MCFT.LOGGER.error("HTTP request failed", e);
+ } catch (RuntimeException e) {
+ MCFT.LOGGER.error("HTTP request failed", e);
+ }
+ }
+ } finally {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ MCFT.LOGGER.error("Failed to close HTTP server", e);
+ }
+ boolean stoppedCurrent = false;
+ synchronized (lock) {
+ if (serverSocket == socket) {
+ serverSocket = null;
+ httpThread = null;
+ stoppedCurrent = true;
+ }
+ }
+ if (stoppedCurrent) DNS.shutdown();
+ }
+ }
+
+ private static void handleClient(Socket client) throws IOException {
+ client.setSoTimeout(SOCKET_READ_TIMEOUT);
+ readHeaders(client.getInputStream());
+ String response = getString();
+ OutputStream out = client.getOutputStream();
+ out.write(response.getBytes(StandardCharsets.UTF_8));
+ out.flush();
+ }
+
+ private static void readHeaders(InputStream input) throws IOException {
+ int third = -1;
+ int second = -1;
+ int previous = -1;
+ for (int count = 0; count < MAX_HEADER_BYTES; count++) {
+ int current = input.read();
+ if (current == -1) return;
+ if (previous == '\n' && current == '\n' || third == '\r' && second == '\n' && previous == '\r' && current == '\n') return;
+ third = second;
+ second = previous;
+ previous = current;
+ }
+ throw new IOException("HTTP request headers exceed " + MAX_HEADER_BYTES + " bytes");
+ }
+
+ private static void requirePort(int value) {
+ if (value < 1 || value > 65535) throw new IllegalArgumentException("httpPort must be between 1 and 65535");
}
private static String getString() throws IOException {
@@ -160,4 +295,4 @@ public void serialize(Object value, JsonGenerator gen, SerializerProvider serial
gen.writeEndArray();
}
}
-}
\ No newline at end of file
+}
diff --git a/common/src/main/java/com/github/squi2rel/mcft/services/OSC.java b/common/src/main/java/com/github/squi2rel/mcft/services/OSC.java
index 1dff517..2089ca9 100644
--- a/common/src/main/java/com/github/squi2rel/mcft/services/OSC.java
+++ b/common/src/main/java/com/github/squi2rel/mcft/services/OSC.java
@@ -9,54 +9,163 @@
import com.illposed.osc.transport.OSCPortOut;
import net.minecraft.client.MinecraftClient;
+import java.io.IOException;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import static com.github.squi2rel.mcft.FTModel.model;
import static com.github.squi2rel.mcft.MCFTClient.config;
public class OSC {
- public static long lastReceived = 0;
- public static Map> c = allParameters.get(msg.getAddress().replace("/v2/", ""));
- if (c != null) {
- lastReceived = System.currentTimeMillis();
- c.accept(msg.getArguments());
+ }
+ MCFT.LOGGER.info("OSC started on {}:{}", bindAddress.getHostAddress(), receivePort);
+ try {
+ InetAddress targetAddress = resolveLoopback(config.oscTargetHost, "oscTargetHost");
+ int sendPort = requirePort(config.oscSendPort, "oscSendPort");
+ sendAvatarChange(targetAddress, sendPort);
+ } catch (Exception e) {
+ MCFT.LOGGER.error("OSC avatar change target is invalid", e);
+ }
+ }
+
+ public static void applyPendingParameters() {
+ lastReceived = AutoBlink.enabled ? System.currentTimeMillis() : lastOscReceived;
+ pendingParameters.forEach((name, value) -> {
+ if (pendingParameters.remove(name, value)) {
+ try {
+ allParameters.get(name).accept(value);
+ } catch (RuntimeException e) {
+ MCFT.LOGGER.error("Failed to apply OSC parameter {}", name, e);
+ }
}
});
- receiver.startListening();
- MCFT.LOGGER.info("OSC started on port {}", 9000);
- sender.send(new OSCMessage("/avatar/change", List.of(Objects.requireNonNull(MinecraftClient.getInstance().getSession().getUuidOrNull()).toString())));
+ }
+
+ public static void shutdown() {
+ synchronized (lock) {
+ try {
+ closeReceiver();
+ } catch (IOException e) {
+ MCFT.LOGGER.error("Failed to close OSC receiver", e);
+ }
+ pendingParameters.clear();
+ }
+ }
+
+ private static void handleMessage(OSCMessageEvent event) {
+ String address = "unknown";
+ try {
+ OSCMessage message = event.getMessage();
+ address = message.getAddress();
+ if (!address.startsWith("/v2/")) return;
+ String parameter = address.substring(4);
+ if (!allParameters.containsKey(parameter)) return;
+ List