diff --git a/.idea/GrepConsole.xml b/.idea/GrepConsole.xml
new file mode 100644
index 00000000..2a246e4f
--- /dev/null
+++ b/.idea/GrepConsole.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/core/src/cn/harryh/arkpets/ArkPets.java b/core/src/cn/harryh/arkpets/ArkPets.java
index f0af399e..6e74ddbc 100644
--- a/core/src/cn/harryh/arkpets/ArkPets.java
+++ b/core/src/cn/harryh/arkpets/ArkPets.java
@@ -6,10 +6,11 @@
import cn.harryh.arkpets.animations.AnimData;
import cn.harryh.arkpets.animations.GeneralBehavior;
import cn.harryh.arkpets.assets.AssetItem;
-import cn.harryh.arkpets.socket.SocketClient;
+import cn.harryh.arkpets.concurrent.SocketClient;
import cn.harryh.arkpets.transitions.TernaryFunction;
import cn.harryh.arkpets.transitions.TransitionFloat;
import cn.harryh.arkpets.transitions.TransitionVector2;
+import cn.harryh.arkpets.tray.MemberTrayImpl;
import cn.harryh.arkpets.utils.HWndCtrl;
import cn.harryh.arkpets.utils.Logger;
import cn.harryh.arkpets.utils.Plane;
@@ -32,7 +33,7 @@ public class ArkPets extends ApplicationAdapter implements InputProcessor {
public Plane plane;
public ArkChar cha;
public ArkConfig config;
- public ArkTray tray;
+ public MemberTrayImpl tray;
public GeneralBehavior behavior;
public TransitionFloat windowAlpha; // Window Opacity Easing
@@ -88,7 +89,7 @@ public void create() {
ArkConfig.Monitor primaryMonitor = ArkConfig.Monitor.getMonitors()[0];
initWindow((int)(primaryMonitor.size[0] * 0.1f), (int)(primaryMonitor.size[0] * 0.1f));
// 5.Tray icon setup
- tray = new ArkTray(this, new SocketClient(), UUID.randomUUID());
+ tray = new MemberTrayImpl(this, new SocketClient(), UUID.randomUUID());
// Setup complete
Logger.info("App", "Render");
}
diff --git a/core/src/cn/harryh/arkpets/ArkTray.java b/core/src/cn/harryh/arkpets/ArkTray.java
deleted file mode 100644
index 59555d1a..00000000
--- a/core/src/cn/harryh/arkpets/ArkTray.java
+++ /dev/null
@@ -1,254 +0,0 @@
-/** Copyright (c) 2022-2024, Harry Huang
- * At GPL-3.0 License
- */
-package cn.harryh.arkpets;
-
-import cn.harryh.arkpets.animations.AnimData;
-import cn.harryh.arkpets.socket.SocketClient;
-import cn.harryh.arkpets.socket.SocketData;
-import cn.harryh.arkpets.tray.Tray;
-import cn.harryh.arkpets.utils.Logger;
-import com.badlogic.gdx.Gdx;
-
-import javax.swing.*;
-import java.awt.*;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.geom.AffineTransform;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Objects;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.UUID;
-
-import static cn.harryh.arkpets.Const.*;
-
-
-public class ArkTray extends Tray {
- private final ArkPets arkPets;
- private final SocketClient socketClient;
- public AnimData keepAnim;
- public static Font font;
- private final JDialog popWindow;
- private final JPopupMenu popMenu;
- private final boolean[] button = {false, false};
-
- static {
- try {
- InputStream inputStream = Objects.requireNonNull(ArkTray.class.getResourceAsStream(fontFileRegular));
- font = Font.createFont(Font.TRUETYPE_FONT, inputStream);
- if (font != null) {
- UIManager.put("Label.font", font.deriveFont(9f).deriveFont(Font.ITALIC));
- UIManager.put("MenuItem.font", font.deriveFont(11f));
- }
- } catch (FontFormatException | IOException e) {
- Logger.error("Tray", "Failed to load tray menu font, details see below.", e);
- font = null;
- }
- }
-
- /** Initializes the ArkPets tray icon instance.
- * Must be used after Gdx.app was initialized.
- * @param boundArkPets The ArkPets instance that bound to the tray icon.
- */
- public ArkTray(ArkPets boundArkPets, SocketClient socket, UUID uuid) {
- super(uuid);
- arkPets = boundArkPets;
- socketClient = socket;
- popWindow = new JDialog();
- popWindow.setUndecorated(true);
- popWindow.setSize(1, 1);
-
- // PopupMenu:
- popMenu = new JPopupMenu() {
- @Override
- public void firePopupMenuWillBecomeInvisible() {
- popWindow.setVisible(false); // Hide the container when the menu is invisible.
- }
- };
- name = (arkPets.config.character_label == null || arkPets.config.character_label.isEmpty()) ? "Unknown" : arkPets.config.character_label;
- socketClient.connect(socketData -> {
- if (socketData == null) {
- Image image = Toolkit.getDefaultToolkit().createImage(getClass().getResource(iconFilePng));
- TrayIcon icon = getTrayIcon(image);
-
- // Add the icon to the system tray.
- try {
- SystemTray.getSystemTray().add(icon);
- Logger.info("Tray", "Tray icon applied");
- } catch (AWTException e) {
- Logger.error("Tray", "Unable to apply tray icon, details see below", e);
- }
- socketClient.disconnect();
- socketClient.reconnect(() -> {
- SystemTray.getSystemTray().remove(icon);
- socketClient.sendRequest(new SocketData(this.uuid, SocketData.OperateType.LOGIN, name, arkPets.canChangeStage()));
- if (button[0]) {
- socketClient.sendRequest(new SocketData(this.uuid, SocketData.OperateType.KEEP_ACTION));
- }
- if (button[1]) {
- socketClient.sendRequest(new SocketData(this.uuid, SocketData.OperateType.TRANSPARENT_MODE));
- }
- });
- return;
- }
- if (socketData.uuid.compareTo(uuid) != 0)
- return;
- switch (socketData.operateType) {
- case LOGOUT -> optExitHandler();
- case KEEP_ACTION -> optKeepAnimEnHandler();
- case NO_KEEP_ACTION -> optKeepAnimDisHandler();
- case TRANSPARENT_MODE -> optTransparentEnHandler();
- case NO_TRANSPARENT_MODE -> optTransparentDisHandler();
- case CHANGE_STAGE -> optChangeStageHandler();
- }
- });
- socketClient.sendRequest(new SocketData(this.uuid, SocketData.OperateType.LOGIN, name, arkPets.canChangeStage()));
- addComponent();
- }
-
- private TrayIcon getTrayIcon(Image image) {
- TrayIcon icon = new TrayIcon(image, name);
- icon.setImageAutoSize(true);
- icon.addMouseListener(new MouseAdapter() {
- @Override
- public void mouseReleased(MouseEvent e) {
- if (e.getButton() == 3 && e.isPopupTrigger()) {
- // After right-click on the tray icon.
- int x = e.getX();
- int y = e.getY();
- showDialog(x + 5, y);
- }
- }
- });
- return icon;
- }
-
- @Override
- protected void addComponent() {
- JLabel innerLabel = new JLabel(" " + name + " ");
- innerLabel.setAlignmentX(0.5f);
- popMenu.add(innerLabel);
-
- optKeepAnimEn.addActionListener(e -> socketClient.sendRequest(new SocketData(uuid, SocketData.OperateType.KEEP_ACTION)));
- optKeepAnimDis.addActionListener(e -> socketClient.sendRequest(new SocketData(uuid, SocketData.OperateType.NO_KEEP_ACTION)));
- optTransparentEn.addActionListener(e -> socketClient.sendRequest(new SocketData(uuid, SocketData.OperateType.TRANSPARENT_MODE)));
- optTransparentDis.addActionListener(e -> socketClient.sendRequest(new SocketData(uuid, SocketData.OperateType.NO_TRANSPARENT_MODE)));
- optChangeStage.addActionListener(e -> {
- if (keepAnim != null)
- socketClient.sendRequest(new SocketData(uuid, SocketData.OperateType.CHANGE_STAGE));
- });
- optExit.addActionListener(e -> socketClient.sendRequest(new SocketData(uuid, SocketData.OperateType.LOGOUT)));
-
- popMenu.add(optKeepAnimEn);
- popMenu.add(optTransparentEn);
- if (arkPets.canChangeStage()) popMenu.add(optChangeStage);
- popMenu.add(optExit);
- popMenu.setSize(100, 24 * popMenu.getSubElements().length);
- }
-
- @Override
- protected void optExitHandler() {
- Logger.info("Tray", "Request to exit");
- arkPets.windowAlpha.reset(0f);
- removeTray();
- new Timer().schedule(new TimerTask() {
- @Override
- public void run() {
- Gdx.app.exit();
- }
- }, (int) (linearEasingDuration * 1000));
- }
-
- @Override
- protected void optChangeStageHandler() {
- Logger.info("Tray", "Request to change stage");
- arkPets.changeStage();
- if (keepAnim != null) {
- keepAnim = null;
- popMenu.remove(optKeepAnimDis);
- popMenu.add(optKeepAnimEn, 1);
- }
- }
-
- @Override
- protected void optTransparentDisHandler() {
- Logger.info("Tray", "Transparent disabled");
- button[1] = false;
- arkPets.windowAlpha.reset(1f);
- arkPets.hWndMine.setWindowTransparent(false);
- popMenu.remove(optTransparentDis);
- popMenu.add(optTransparentEn, 2);
- }
-
- @Override
- protected void optTransparentEnHandler() {
- Logger.info("Tray", "Transparent enabled");
- button[1] = true;
- arkPets.windowAlpha.reset(0.75f);
- arkPets.hWndMine.setWindowTransparent(true);
- popMenu.remove(optTransparentEn);
- popMenu.add(optTransparentDis, 2);
- }
-
- @Override
- protected void optKeepAnimDisHandler() {
- Logger.info("Tray", "Keep-Anim disabled");
- button[0] = false;
- keepAnim = null;
- popMenu.remove(optKeepAnimDis);
- popMenu.add(optKeepAnimEn, 1);
- }
-
- @Override
- protected void optKeepAnimEnHandler() {
- Logger.info("Tray", "Keep-Anim enabled");
- button[0] = true;
- keepAnim = arkPets.cha.getPlaying();
- popMenu.remove(optKeepAnimEn);
- popMenu.add(optKeepAnimDis, 1);
- }
-
- @Override
- public void removeTray() {
- popMenu.removeAll();
- popWindow.dispose();
- socketClient.disconnect();
- }
-
- /** Hides the menu.
- */
- public void hideDialog() {
- if (popMenu.isVisible()) {
- popMenu.setVisible(false);
- Logger.debug("Tray", "Hidden");
- }
- }
-
- /** Shows the menu at the given coordinate.
- */
- public void showDialog(int x, int y) {
- /* Use `System.setProperty("sun.java2d.uiScale", "1")` can also avoid system scaling.
- Here we will adapt the coordinate for system scaling artificially. See below. */
- AffineTransform at = popWindow.getGraphicsConfiguration().getDefaultTransform();
- int scaledX = (int) (x / at.getScaleX());
- int scaledY = (int) (y / at.getScaleY());
-
- // Show the JDialog together with the JPopupMenu.
- popWindow.setVisible(true);
- popWindow.setLocation(scaledX, scaledY - popMenu.getHeight());
- popMenu.show(popWindow, 0, 0);
- Logger.debug("Tray", "Shown @ " + x + ", " + y);
- }
-
- /** Toggles the menu at the given coordinate.
- */
- public void toggleDialog(int x, int y) {
- if (popMenu.isVisible()) {
- hideDialog();
- } else {
- showDialog(x, y);
- }
- }
-}
diff --git a/core/src/cn/harryh/arkpets/Const.java b/core/src/cn/harryh/arkpets/Const.java
index 937c3bf4..912827cd 100644
--- a/core/src/cn/harryh/arkpets/Const.java
+++ b/core/src/cn/harryh/arkpets/Const.java
@@ -3,9 +3,16 @@
*/
package cn.harryh.arkpets;
+import cn.harryh.arkpets.utils.Logger;
import cn.harryh.arkpets.utils.Version;
import javafx.util.Duration;
+import javax.swing.*;
+import java.awt.*;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
/** Constants definition class.
*/
@@ -51,21 +58,23 @@ public final class Const {
public static final String configExternal = "ArkPetsConfig.json";
public static final String configInternal = "/ArkPetsConfigDefault.json";
public static final String iconFilePng = "/icons/icon.png";
- public static final String fontFileRegular = "/fonts/SourceHanSansCN-Regular.otf";
- public static final String fontFileBold = "/fonts/SourceHanSansCN-Bold.otf";
public static final String startupTarget = "ArkPets.exe";
public static final String startUpScript = "ArkPetsStartupService.vbs";
// Changeable constants
- public static boolean isHttpsTrustAll = false;
- public static boolean isUpdateAvailable = false;
+ public static boolean isHttpsTrustAll = false;
+ public static boolean isUpdateAvailable = false;
public static boolean isDatasetIncompatible = false;
- public static boolean isNewcomer = false;
+ public static boolean isNewcomer = false;
+
+ // Socket C/S constants
+ public static final String serverHost = "localhost";
+ public static final int[] serverPorts = {8686, 8866, 8989, 8899, 8800};
+ public static final int reconnectPeriodMillis = 5 * 1000;
// Misc constants
public static String ipPortRegex = "^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?):\\d{1,5}$";
-
/** Paths presets definition class.
*/
public static class PathConfig {
@@ -103,8 +112,29 @@ public static class LogConfig {
public static final String debugArg = "--debug";
}
- // SocketServer Ports
-
- public static final int[] serverPorts = {8686, 8866, 8989, 8899, 8800};
-
+ public static class FontsConfig {
+ private static final String fontFileRegular = "/fonts/SourceHanSansCN-Regular.otf";
+ private static final String fontFileBold = "/fonts/SourceHanSansCN-Bold.otf";
+
+ public static void loadFontsToJavafx() {
+ javafx.scene.text.Font.loadFont(FontsConfig.class.getResourceAsStream(fontFileRegular),
+ javafx.scene.text.Font.getDefault().getSize());
+ javafx.scene.text.Font.loadFont(FontsConfig.class.getResourceAsStream(fontFileBold),
+ javafx.scene.text.Font.getDefault().getSize());
+ }
+
+ public static void loadFontsToSwing() {
+ try {
+ InputStream in = Objects.requireNonNull(FontsConfig.class.getResourceAsStream(fontFileRegular));
+ java.awt.Font font = java.awt.Font.createFont(java.awt.Font.TRUETYPE_FONT, in);
+ if (font != null) {
+ UIManager.put("Label.font", font.deriveFont(10f).deriveFont(Font.ITALIC));
+ UIManager.put("Menu.font", font.deriveFont(11f));
+ UIManager.put("MenuItem.font", font.deriveFont(11f));
+ }
+ } catch (FontFormatException | IOException e) {
+ Logger.error("System", "Failed to load tray menu font, details see below.", e);
+ }
+ }
+ }
}
diff --git a/core/src/cn/harryh/arkpets/concurrent/PortUtils.java b/core/src/cn/harryh/arkpets/concurrent/PortUtils.java
new file mode 100644
index 00000000..53680560
--- /dev/null
+++ b/core/src/cn/harryh/arkpets/concurrent/PortUtils.java
@@ -0,0 +1,82 @@
+package cn.harryh.arkpets.concurrent;
+
+import com.alibaba.fastjson2.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.DatagramSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.UUID;
+
+
+public class PortUtils {
+ /** Gets server port for client to connect to.
+ * @param expectedPorts The candidate ports to query.
+ * @return A server port.
+ * @throws NoServerRunningException If no server is running.
+ */
+ public static int getServerPort(int[] expectedPorts)
+ throws NoServerRunningException {
+ for (int serverPort : expectedPorts) {
+ try (Socket socket = new Socket("localhost", serverPort)) {
+ socket.setSoTimeout(100);
+ PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
+ BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ out.println(JSONObject.toJSONString(new SocketData(UUID.randomUUID(), SocketData.Operation.VERIFY)));
+ SocketData socketData = JSONObject.parseObject(in.readLine(), SocketData.class);
+ out.close();
+ in.close();
+ if (socketData.operation == SocketData.Operation.SERVER_ONLINE)
+ return serverPort;
+ } catch (IOException ignored) {
+ }
+ }
+ throw new NoServerRunningException();
+ }
+
+ /** Gets an available port for server to bind to.
+ * @param expectedPorts The candidate ports to query.
+ * @return A port number.
+ * @throws NoPortAvailableException If every port is busy.
+ * @throws ServerCollisionException If a server is already running.
+ */
+ public static int getAvailablePort(int[] expectedPorts)
+ throws NoPortAvailableException, ServerCollisionException {
+ try {
+ getServerPort(expectedPorts);
+ throw new ServerCollisionException();
+ } catch (NoServerRunningException ignored) {
+ }
+ for (int serverPort : expectedPorts) {
+ try (DatagramSocket ignored = new DatagramSocket(serverPort)) {
+ return serverPort;
+ } catch (SocketException ignored) {
+ }
+ }
+ throw new NoPortAvailableException();
+ }
+
+
+ public static class NoPortAvailableException extends IllegalStateException {
+ public NoPortAvailableException() {
+ super("No port is available.");
+ }
+ }
+
+
+ public static class ServerCollisionException extends IllegalStateException {
+ public ServerCollisionException() {
+ super("A server is already running.");
+ }
+ }
+
+
+ public static class NoServerRunningException extends IllegalStateException {
+ public NoServerRunningException() {
+ super("No running server is found.");
+ }
+ }
+}
diff --git a/core/src/cn/harryh/arkpets/process_pool/ProcessPool.java b/core/src/cn/harryh/arkpets/concurrent/ProcessPool.java
similarity index 51%
rename from core/src/cn/harryh/arkpets/process_pool/ProcessPool.java
rename to core/src/cn/harryh/arkpets/concurrent/ProcessPool.java
index 68bc102e..c2eaa2af 100644
--- a/core/src/cn/harryh/arkpets/process_pool/ProcessPool.java
+++ b/core/src/cn/harryh/arkpets/concurrent/ProcessPool.java
@@ -1,20 +1,27 @@
-package cn.harryh.arkpets.process_pool;
-
-import cn.harryh.arkpets.socket.InteriorSocketServer;
+package cn.harryh.arkpets.concurrent;
import java.io.File;
-import java.util.*;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Future;
-import java.util.concurrent.FutureTask;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.*;
public class ProcessPool {
- private final Set processHolderHashSet = new HashSet<>();
- private final java.util.concurrent.ExecutorService executorService = InteriorSocketServer.getThreadPool();
+ public static final ExecutorService executorService =
+ new ThreadPoolExecutor(20,
+ Integer.MAX_VALUE,
+ 60L,
+ TimeUnit.SECONDS,
+ new SynchronousQueue<>(),
+ r -> {
+ Thread thread = Executors.defaultThreadFactory().newThread(r);
+ thread.setDaemon(true);
+ return thread;
+ });
+
private static ProcessPool instance = null;
- public static ProcessPool getInstance() {
+ public static synchronized ProcessPool getInstance() {
if (instance == null)
instance = new ProcessPool();
return instance;
@@ -23,16 +30,12 @@ public static ProcessPool getInstance() {
private ProcessPool() {
}
- public void shutdown() {
- processHolderHashSet.forEach(processHolder -> processHolder.getProcess().destroy());
- }
-
public Future> submit(Runnable task) {
return executorService.submit(task);
}
- public FutureTask submit(Class> clazz, List jvmArgs, List args) {
- Callable task = () -> {
+ public FutureTask submit(Class> clazz, List jvmArgs, List args) {
+ Callable task = () -> {
// Attributes preparation
String javaHome = System.getProperty("java.home");
String javaBin = javaHome + File.separator + "bin" + File.separator + "java";
@@ -51,17 +54,18 @@ public FutureTask submit(Class> clazz, List jvmArgs, List<
// Process execution
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.inheritIO().start();
- ProcessHolder processHolder = ProcessHolder.holder(process);
- processHolderHashSet.add(processHolder);
- int status = process.waitFor();
- processHolderHashSet.remove(processHolder);
- if (status == 0) {
- return TaskStatus.ofSuccess(process.pid());
- }
- return TaskStatus.ofFailure(process.pid());
+ int exitValue = process.waitFor();
+ return new ProcessResult(exitValue, process.pid());
};
- FutureTask futureTask = new FutureTask<>(task);
+ FutureTask futureTask = new FutureTask<>(task);
executorService.submit(futureTask);
return futureTask;
}
+
+
+ public record ProcessResult(int exitValue, long processId) {
+ public boolean isSuccess() {
+ return exitValue() == 0;
+ }
+ }
}
diff --git a/core/src/cn/harryh/arkpets/concurrent/SocketClient.java b/core/src/cn/harryh/arkpets/concurrent/SocketClient.java
new file mode 100644
index 00000000..3431aeb5
--- /dev/null
+++ b/core/src/cn/harryh/arkpets/concurrent/SocketClient.java
@@ -0,0 +1,123 @@
+package cn.harryh.arkpets.concurrent;
+
+import cn.harryh.arkpets.tray.MemberTrayImpl;
+import cn.harryh.arkpets.utils.Logger;
+import com.alibaba.fastjson2.JSONException;
+import com.alibaba.fastjson2.JSONObject;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.UUID;
+
+import static cn.harryh.arkpets.Const.*;
+
+
+public class SocketClient {
+ private boolean connected = false;
+ private Socket socket;
+ private SocketSession session;
+ private Timer timer;
+
+ public SocketClient() {
+ }
+
+ public void connectWithRetry(Runnable onConnected) {
+ timer = new Timer();
+ timer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ connect(onConnected);
+ if (connected)
+ timer.cancel();
+ }
+ }, 0, reconnectPeriodMillis);
+ }
+
+ public void connect(Runnable onConnected) {
+ if (connected)
+ return;
+ try {
+ int port = PortUtils.getServerPort(serverPorts);
+ Logger.info("SocketClient", "Connecting to server on port" + port);
+ try {
+ socket = new Socket(serverHost, port);
+ connected = true;
+ if (onConnected != null)
+ onConnected.run();
+ } catch (IOException e) {
+ Logger.error("SocketClient", "Connecting to server on port " + port + "failed, details see below.", e);
+ }
+ } catch (PortUtils.NoServerRunningException e) {
+ Logger.warn("SocketClient", "Connecting to server failed. " + e.getMessage());
+ }
+ }
+
+ public void setHandler(SocketSession session) {
+ if (!connected)
+ throw new IllegalStateException("The socket was not yet connected.");
+ if (this.session != null)
+ this.session.close();
+ Thread listener = new Thread(() -> ProcessPool.executorService.execute(session));
+ ProcessPool.executorService.execute(listener);
+ this.session = session;
+ }
+
+ public void disconnect() {
+ if (!connected)
+ return;
+ connected = false;
+ session.close();
+ }
+
+ public void sendRequest(SocketData socketData) {
+ if (!connected)
+ return;
+ String data = JSONObject.toJSONString(socketData);
+ session.send(data);
+ }
+
+
+ public static class ClientSocketSession extends SocketSession {
+ private final SocketClient client;
+ private final MemberTrayImpl memberTray;
+ private UUID uuid = null;
+
+ public ClientSocketSession(SocketClient client, MemberTrayImpl memberTray) {
+ super(client.socket);
+ this.client = client;
+ this.memberTray = memberTray;
+ }
+
+ @Override
+ public void receive(String request) {
+ try {
+ SocketData socketData = JSONObject.parseObject(request, SocketData.class);
+ if (socketData.operation == null)
+ return;
+ if (uuid == null)
+ uuid = socketData.uuid;
+ if (socketData.uuid.compareTo(this.uuid) == 0) {
+ // If the connection is normal:
+ switch (socketData.operation) {
+ case LOGOUT -> memberTray.onExit();
+ case KEEP_ACTION -> memberTray.onKeepAnimEn();
+ case NO_KEEP_ACTION -> memberTray.onKeepAnimDis();
+ case TRANSPARENT_MODE -> memberTray.onTransparentEn();
+ case NO_TRANSPARENT_MODE -> memberTray.onTransparentDis();
+ case CHANGE_STAGE -> memberTray.onChangeStage();
+ }
+ }
+ } catch (JSONException ignored) {
+ }
+ }
+
+ @Override
+ protected void onBroken() {
+ memberTray.onDisconnected();
+ client.disconnect();
+ client.connectWithRetry(memberTray::onReconnected);
+ }
+ }
+}
diff --git a/core/src/cn/harryh/arkpets/concurrent/SocketData.java b/core/src/cn/harryh/arkpets/concurrent/SocketData.java
new file mode 100644
index 00000000..21924c0d
--- /dev/null
+++ b/core/src/cn/harryh/arkpets/concurrent/SocketData.java
@@ -0,0 +1,37 @@
+package cn.harryh.arkpets.concurrent;
+
+import java.io.Serializable;
+import java.nio.charset.Charset;
+import java.util.UUID;
+
+
+public class SocketData implements Serializable {
+ public enum Operation {
+ LOGIN,
+ LOGOUT,
+ KEEP_ACTION,
+ NO_KEEP_ACTION,
+ TRANSPARENT_MODE,
+ NO_TRANSPARENT_MODE,
+ CHANGE_STAGE,
+ VERIFY,
+ SERVER_ONLINE,
+ ACTIVATE_LAUNCHER
+ }
+
+ public UUID uuid;
+ public Operation operation;
+ public byte[] name;
+ public boolean canChangeStage;
+
+ public SocketData(UUID uuid, Operation operation) {
+ this(uuid, operation, "", false);
+ }
+
+ public SocketData(UUID uuid, Operation operation, String name, boolean canChangeStage) {
+ this.uuid = uuid;
+ this.operation = operation;
+ this.name = name.getBytes(Charset.forName("GBK"));
+ this.canChangeStage = canChangeStage;
+ }
+}
diff --git a/core/src/cn/harryh/arkpets/concurrent/SocketServer.java b/core/src/cn/harryh/arkpets/concurrent/SocketServer.java
new file mode 100644
index 00000000..13b419ed
--- /dev/null
+++ b/core/src/cn/harryh/arkpets/concurrent/SocketServer.java
@@ -0,0 +1,139 @@
+package cn.harryh.arkpets.concurrent;
+
+import cn.harryh.arkpets.tray.HostTray;
+import cn.harryh.arkpets.tray.MemberTray;
+import cn.harryh.arkpets.tray.MemberTrayProxy;
+import cn.harryh.arkpets.utils.Logger;
+import com.alibaba.fastjson2.JSONException;
+import com.alibaba.fastjson2.JSONObject;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.RejectedExecutionException;
+
+import static cn.harryh.arkpets.Const.serverPorts;
+
+
+public class SocketServer {
+ private int port;
+ private ServerSocket serverSocket = null;
+ private final Set sessionList = new CopyOnWriteArraySet<>();
+ private Thread listener;
+
+ private static SocketServer instance = null;
+
+ public static synchronized SocketServer getInstance() {
+ if (instance == null)
+ instance = new SocketServer();
+ return instance;
+ }
+
+ private SocketServer() {
+ }
+
+ public synchronized void startServer(HostTray hostTray)
+ throws PortUtils.NoPortAvailableException, PortUtils.ServerCollisionException {
+ Logger.info("SocketServer", "Request to start server");
+ this.port = PortUtils.getAvailablePort(serverPorts);
+ listener = new Thread(() -> {
+ try {
+ serverSocket = new ServerSocket(port);
+ Logger.info("SocketServer", "Server is running on port " + port);
+ while (!listener.isInterrupted() && !ProcessPool.executorService.isShutdown()) {
+ Socket clientSocket = serverSocket.accept();
+ ServerSocketSession session = new ServerSocketSession(clientSocket, hostTray);
+ sessionList.add(session);
+ ProcessPool.executorService.execute(session);
+ Logger.info("SocketServer", "(+)" + session + " connected");
+ }
+ serverSocket.close();
+ Logger.info("SocketServer", "Server was stopped");
+ } catch (IOException e) {
+ Logger.error("SocketServer", "An unexpected error occurred while listening, details see below.", e);
+ } catch (RejectedExecutionException ignored) {
+ }
+ });
+ ProcessPool.executorService.execute(listener);
+ }
+
+ public synchronized void stopServer() {
+ Logger.info("SocketServer", "Request to stop server");
+ if (listener != null)
+ listener.interrupt();
+ sessionList.forEach(ServerSocketSession::close);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append("SocketServer:\n");
+ stringBuilder.append("\tlisten: 0.0.0.0:").append(port).append("\n");
+ stringBuilder.append("\tclients: ");
+ if (sessionList.isEmpty())
+ return stringBuilder.append("None").toString();
+ stringBuilder.append("\n");
+ sessionList.forEach(socket -> stringBuilder
+ .append("\t\t")
+ .append(socket.getHostAddress())
+ .append(":")
+ .append(socket.getPort())
+ .append("\n")
+ );
+ return stringBuilder.toString();
+ }
+
+
+ public static class ServerSocketSession extends SocketSession {
+ private final HostTray hostTray;
+ private MemberTray tray;
+ private UUID uuid = null;
+
+ public ServerSocketSession(Socket target, HostTray hostTray) {
+ super(target);
+ this.hostTray = hostTray;
+ }
+
+ @Override
+ public void receive(String request) {
+ try {
+ SocketData socketData = JSONObject.parseObject(request, SocketData.class);
+ if (socketData.operation == null)
+ return;
+ if (uuid == null)
+ uuid = socketData.uuid;
+
+ switch (socketData.operation) {
+ case VERIFY -> {
+ this.send(JSONObject.toJSONString(new SocketData(uuid, SocketData.Operation.SERVER_ONLINE)));
+ close();
+ }
+ case ACTIVATE_LAUNCHER -> hostTray.showStage();
+ case LOGIN -> {
+ tray = new MemberTrayProxy(socketData, target, hostTray);
+ hostTray.addMemberTray(socketData.uuid, tray);
+ }
+ case LOGOUT -> {
+ hostTray.removeMemberTray(socketData.uuid);tray.onExit();
+ close();
+ }
+ case KEEP_ACTION -> tray.onKeepAnimEn();
+ case NO_KEEP_ACTION -> tray.onKeepAnimDis();
+ case TRANSPARENT_MODE -> tray.onTransparentEn();
+ case NO_TRANSPARENT_MODE -> tray.onTransparentDis();
+ case CHANGE_STAGE -> tray.onChangeStage();
+ }
+ } catch (JSONException ignored) {
+ }
+ }
+
+ @Override
+ protected void onClosed() {
+ Logger.info("SocketServer", "(-)" + this + " closed");
+ SocketServer.getInstance().sessionList.remove(this);
+ }
+ }
+}
diff --git a/core/src/cn/harryh/arkpets/concurrent/SocketSession.java b/core/src/cn/harryh/arkpets/concurrent/SocketSession.java
new file mode 100644
index 00000000..6be13df3
--- /dev/null
+++ b/core/src/cn/harryh/arkpets/concurrent/SocketSession.java
@@ -0,0 +1,93 @@
+package cn.harryh.arkpets.concurrent;
+
+import cn.harryh.arkpets.utils.Logger;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.net.SocketException;
+
+
+abstract public class SocketSession implements Runnable {
+ protected final Socket target;
+ protected final BufferedReader in;
+ protected final PrintWriter out;
+ protected boolean hasRun = false;
+ protected boolean hasClosed = false;
+
+ public SocketSession(Socket target) {
+ try {
+ this.target = target;
+ in = new BufferedReader(new InputStreamReader(target.getInputStream()));
+ out = new PrintWriter(target.getOutputStream(), true);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public final String getHostAddress() {
+ return target.getInetAddress().getHostAddress();
+ }
+
+ public final int getPort() {
+ return target.getPort();
+ }
+
+ public final void close() {
+ if (hasClosed)
+ return;
+ hasClosed = true;
+ try {
+ target.close();
+ this.onClosed();
+ } catch (IOException ignored) {
+ }
+ }
+
+ @Override
+ public final void run() {
+ if (hasRun)
+ throw new IllegalStateException("The session thread has run yet.");
+ try {
+ while (!target.isClosed()) {
+ try {
+ String request = in.readLine();
+ if (request == null) {
+ Logger.debug("SocketSession", this + " -x");
+ this.onBroken();
+ this.close();
+ } else {
+ Logger.debug("SocketSession", this + " -> " + request);
+ receive(request);
+ }
+ } catch (SocketException e) {
+ Logger.debug("SocketSession", this + " -x (" + e.getMessage() + ")");
+ this.onBroken();
+ this.close();
+ }
+ }
+ } catch (IOException e) {
+ Logger.error("SocketSession", "An unexpected error occurred on " + this + ", details see below.", e);
+ }
+ }
+
+ public final void send(String request) {
+ Logger.debug("SocketSession", this + " <- " + request);
+ out.println(request);
+ }
+
+ abstract public void receive(String request);
+
+ protected void onClosed() {
+ }
+
+ protected void onBroken() {
+ }
+
+ @Override
+ public String toString() {
+ return "[" + getHostAddress() + ":" + getPort() +"]";
+ }
+}
diff --git a/core/src/cn/harryh/arkpets/exception/NoPortAvailableException.java b/core/src/cn/harryh/arkpets/exception/NoPortAvailableException.java
deleted file mode 100644
index 814b8c20..00000000
--- a/core/src/cn/harryh/arkpets/exception/NoPortAvailableException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package cn.harryh.arkpets.exception;
-
-public class NoPortAvailableException extends Exception {
- public NoPortAvailableException() {
- super("No port available");
- }
-}
diff --git a/core/src/cn/harryh/arkpets/exception/NoServerRunningException.java b/core/src/cn/harryh/arkpets/exception/NoServerRunningException.java
deleted file mode 100644
index 0817fda6..00000000
--- a/core/src/cn/harryh/arkpets/exception/NoServerRunningException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package cn.harryh.arkpets.exception;
-
-public class NoServerRunningException extends Exception {
- public NoServerRunningException() {
- super("Can not find a running server");
- }
-}
diff --git a/core/src/cn/harryh/arkpets/exception/ServerRunningException.java b/core/src/cn/harryh/arkpets/exception/ServerRunningException.java
deleted file mode 100644
index ec8ba0f0..00000000
--- a/core/src/cn/harryh/arkpets/exception/ServerRunningException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package cn.harryh.arkpets.exception;
-
-public class ServerRunningException extends Exception {
- public ServerRunningException() {
- super("Server is already running");
- }
-}
diff --git a/core/src/cn/harryh/arkpets/process_pool/ProcessHolder.java b/core/src/cn/harryh/arkpets/process_pool/ProcessHolder.java
deleted file mode 100644
index 1932f920..00000000
--- a/core/src/cn/harryh/arkpets/process_pool/ProcessHolder.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package cn.harryh.arkpets.process_pool;
-
-public class ProcessHolder {
- private final Long processID;
- private final Process process;
-
- public Long getProcessID() {
- return processID;
- }
-
- public Process getProcess() {
- return process;
- }
-
- public static ProcessHolder holder(Process process) {
- return new ProcessHolder(process);
- }
-
- private ProcessHolder(Process process) {
- this.process = process;
- this.processID = process.pid();
- }
-
- @Override
- public String toString() {
- return "ProcessHolder [processID=" + processID + "]";
- }
-}
diff --git a/core/src/cn/harryh/arkpets/process_pool/TaskStatus.java b/core/src/cn/harryh/arkpets/process_pool/TaskStatus.java
deleted file mode 100644
index e44e2619..00000000
--- a/core/src/cn/harryh/arkpets/process_pool/TaskStatus.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package cn.harryh.arkpets.process_pool;
-
-public class TaskStatus {
-
- public enum Status {
- SUCCESS,
- FAILURE
- }
-
- private final Status status;
- private final Throwable exception;
- private final Long processId;
-
- public Status getStatus() {
- return status;
- }
-
- public Throwable getException() {
- return exception;
- }
-
- public Long getProcessId() {
- return processId;
- }
-
- private TaskStatus(Status status, Long processId, Throwable exception) {
- this.status = status;
- this.processId = processId;
- this.exception = exception;
- }
-
- public static TaskStatus ofSuccess(Long processId) {
- return new TaskStatus(Status.SUCCESS, processId, null);
- }
-
- public static TaskStatus ofFailure(Long processId) {
- return new TaskStatus(Status.FAILURE, processId, null);
- }
-
- public static TaskStatus ofFailure(Long processId, Throwable exception) {
- return new TaskStatus(Status.FAILURE, processId, exception);
- }
-}
diff --git a/core/src/cn/harryh/arkpets/socket/InteriorSocketServer.java b/core/src/cn/harryh/arkpets/socket/InteriorSocketServer.java
deleted file mode 100644
index 1d508f0b..00000000
--- a/core/src/cn/harryh/arkpets/socket/InteriorSocketServer.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package cn.harryh.arkpets.socket;
-
-import cn.harryh.arkpets.exception.NoPortAvailableException;
-import cn.harryh.arkpets.exception.ServerRunningException;
-import cn.harryh.arkpets.tray.ClientTrayHandler;
-import cn.harryh.arkpets.utils.Logger;
-
-import java.io.IOException;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.*;
-
-import static cn.harryh.arkpets.utils.IOUtils.NetUtils.getAvailablePort;
-
-
-public class InteriorSocketServer {
- private int port;
- private boolean checked = false;
- private static final List clientSockets = new ArrayList<>();
- private static final List clientHandlers = new ArrayList<>();
- private static final ExecutorService executorService =
- new ThreadPoolExecutor(20, Integer.MAX_VALUE,
- 60L, TimeUnit.SECONDS, new SynchronousQueue<>(),
- r -> {
- Thread thread = Executors.defaultThreadFactory().newThread(r);
- thread.setDaemon(true);
- return thread;
- });
- private ServerSocket serverSocket = null;
- private static Thread mainThread;
- private static InteriorSocketServer instance = null;
-
- public static ExecutorService getThreadPool() {
- return executorService;
- }
-
- public static InteriorSocketServer getInstance() {
- if (instance == null) {
- instance = new InteriorSocketServer();
- }
- return instance;
- }
-
- public int checkServerAvailable() {
- try {
- this.port = getAvailablePort();
- checked = true;
- return 1;
- } catch (NoPortAvailableException e) {
- return 0;
- } catch (ServerRunningException e) {
- return -1;
- }
- }
-
- private InteriorSocketServer() {
- }
-
- public synchronized void startServer() {
- if (!checked)
- return;
- mainThread = new Thread(() -> {
- try {
- serverSocket = new ServerSocket(port);
- Logger.info("Socket", "Server is running on port " + port);
- while (true) {
- Socket clientSocket = serverSocket.accept();
- clientSockets.add(clientSocket);
- Logger.info("Socket", "New client connection from %s:%d".formatted(clientSocket.getInetAddress().getHostAddress(), clientSocket.getPort()));
- ClientTrayHandler clientTrayHandler = new ClientTrayHandler(clientSocket);
- clientHandlers.add(clientTrayHandler);
- if (executorService.isShutdown())
- break;
- executorService.execute(clientTrayHandler);
- }
- Logger.info("Socket", "Server stop");
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- });
- executorService.execute(mainThread);
- }
-
- public synchronized void stopServer() {
- if (!checked)
- return;
- mainThread.interrupt();
- clientHandlers.forEach(ClientTrayHandler::stopThread);
- executorService.shutdown();
- }
-
- public void removeClientSocket(Socket socket) {
- if (!checked)
- return;
- clientSockets.remove(socket);
- }
-
- public void removeClientHandler(ClientTrayHandler handler) {
- if (!checked)
- return;
- clientHandlers.remove(handler);
- }
-
- @Override
- public String toString() {
- StringBuilder stringBuilder = new StringBuilder();
- stringBuilder.append("InteriorSocketServer:\n");
- stringBuilder.append("\tlisten: 0.0.0.0:").append(port).append("\n");
- stringBuilder.append("\tclients: ");
- if (clientSockets.isEmpty()) {
- return stringBuilder.append("None").toString();
- }
- stringBuilder.append("\n");
- for (Socket socket : clientSockets) {
- stringBuilder
- .append("\t\t")
- .append(socket.getInetAddress().getHostAddress())
- .append(":")
- .append(socket.getPort())
- .append("\n");
- }
- return stringBuilder.toString();
- }
-}
diff --git a/core/src/cn/harryh/arkpets/socket/SocketClient.java b/core/src/cn/harryh/arkpets/socket/SocketClient.java
deleted file mode 100644
index af06a394..00000000
--- a/core/src/cn/harryh/arkpets/socket/SocketClient.java
+++ /dev/null
@@ -1,130 +0,0 @@
-package cn.harryh.arkpets.socket;
-
-import cn.harryh.arkpets.exception.NoServerRunningException;
-import cn.harryh.arkpets.utils.Logger;
-import com.alibaba.fastjson2.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.PrintWriter;
-import java.net.Socket;
-import java.net.SocketException;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.function.Consumer;
-
-import static cn.harryh.arkpets.utils.IOUtils.NetUtils.getServerPort;
-
-
-public class SocketClient {
- private class Task extends TimerTask {
-
- private final Runnable callback;
-
- public Task(Runnable callback) {
- this.callback = callback;
- }
-
- @Override
- public void run() {
- try {
- Logger.info("Socket", "Searching server");
- port = getServerPort();
- Logger.info("Socket", "Server found, connecting");
- timer.cancel();
- receiveThreadBreakFlag = false;
- connect(consumer);
- callback.run();
- } catch (NoServerRunningException ignored) {
- }
- }
- }
-
- private final static String host = "localhost";
- private int port;
- private boolean connected = false;
- private Socket socket;
- private PrintWriter socketOut;
- private BufferedReader socketIn;
- private Thread thread = null;
- private Timer timer;
- private Consumer consumer;
- private volatile boolean receiveThreadBreakFlag = false;
-
- public SocketClient() {
- try {
- port = getServerPort();
- } catch (NoServerRunningException e) {
- throw new RuntimeException(e);
- }
- }
-
- public void reconnect(Runnable callback) {
- timer = new Timer();
- timer.schedule(new Task(callback), 0, 5000);
- }
-
- public void connect() {
- if (connected) {
- return;
- }
- try {
- socket = new Socket(host, port);
- socketOut = new PrintWriter(socket.getOutputStream(), true);
- socketIn = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- connected = true;
- } catch (IOException e) {
- Logger.error("Socket", "Error connecting to %s:%d".formatted(host, port));
- throw new RuntimeException(e);
- }
- }
-
- public void connect(Consumer consumer) {
- connect();
- this.consumer = consumer;
- thread = new Thread(() -> {
- while (!receiveThreadBreakFlag) {
- try {
- String receive = socketIn.readLine();
- Logger.debug("Socket", receive);
- consumer.accept(JSONObject.parseObject(receive, SocketData.class));
- } catch (SocketException e) {
- consumer.accept(null);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- });
- thread.setDaemon(true);
- thread.start();
- }
-
- public void disconnect() {
- if (!connected) {
- return;
- }
- receiveThreadBreakFlag = true;
- try {
- if (thread != null)
- thread.interrupt();
- socket.close();
- socketOut.close();
- socketIn.close();
- connected = false;
- } catch (IOException e) {
- Logger.error("Socket", "Error disconnecting to %s:%d".formatted(host, port));
- throw new RuntimeException(e);
- }
- }
-
- public void sendRequest(SocketData socketData) {
- if (!connected) {
- return;
- }
- String data = JSONObject.toJSONString(socketData);
- Logger.debug("SocketClient", data);
- socketOut.println(data);
- }
-
-}
diff --git a/core/src/cn/harryh/arkpets/socket/SocketData.java b/core/src/cn/harryh/arkpets/socket/SocketData.java
deleted file mode 100644
index 7b511bbc..00000000
--- a/core/src/cn/harryh/arkpets/socket/SocketData.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package cn.harryh.arkpets.socket;
-
-import java.util.UUID;
-
-
-public class SocketData {
- public enum OperateType {
- LOGIN,
- LOGOUT,
- KEEP_ACTION,
- NO_KEEP_ACTION,
- TRANSPARENT_MODE,
- NO_TRANSPARENT_MODE,
- CHANGE_STAGE,
- VERIFY,
- SERVER_ONLINE,
- ACTIVATE_LAUNCHER
- }
-
- public UUID uuid;
- public OperateType operateType;
-
- public byte[] name;
- public boolean canChangeStage;
-
- public SocketData(UUID uuid, OperateType operateType) {
- this(uuid, operateType, "", false);
- }
-
- public SocketData(UUID uuid, OperateType operateType, String name, boolean canChangeStage) {
- this.uuid = uuid;
- this.operateType = operateType;
- this.name = name.getBytes();
- this.canChangeStage = canChangeStage;
- }
-}
diff --git a/core/src/cn/harryh/arkpets/tray/ClientTrayHandler.java b/core/src/cn/harryh/arkpets/tray/ClientTrayHandler.java
deleted file mode 100644
index 70e3b9f2..00000000
--- a/core/src/cn/harryh/arkpets/tray/ClientTrayHandler.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package cn.harryh.arkpets.tray;
-
-import cn.harryh.arkpets.socket.InteriorSocketServer;
-import cn.harryh.arkpets.socket.SocketData;
-import cn.harryh.arkpets.utils.Logger;
-import com.alibaba.fastjson2.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.PrintWriter;
-import java.net.Socket;
-import java.util.UUID;
-
-
-public class ClientTrayHandler implements Runnable {
- private final Socket clientSocket;
- private Tray tray;
- private UUID uuid = null;
- private final static SystemTrayManager systemTrayManager = SystemTrayManager.getInstance();
- private volatile boolean threadExitFlag = false;
-
- public ClientTrayHandler(Socket clientSocket) {
- this.clientSocket = clientSocket;
-
- }
-
- public synchronized void stopThread() {
- threadExitFlag = true;
- try {
- clientSocket.close();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public void run() {
- try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
- boolean flag = false;
- while (!threadExitFlag) {
- String request = in.readLine();
- if (request == null)
- break;
- Logger.debug("SocketServer", request);
- SocketData socketData = JSONObject.parseObject(request, SocketData.class);
- if (uuid == null)
- uuid = socketData.uuid;
- switch (socketData.operateType) {
- case VERIFY -> {
- PrintWriter socketOut = new PrintWriter(clientSocket.getOutputStream(), true);
- socketOut.println(JSONObject.toJSONString(new SocketData(uuid, SocketData.OperateType.SERVER_ONLINE)));
- socketOut.close();
- clientSocket.close();
- return;
- }
- case ACTIVATE_LAUNCHER -> {
- SystemTrayManager.getInstance().showLauncher();
- clientSocket.close();
- return;
- }
- case LOGIN -> {
- tray = new TrayInstance(socketData.uuid, clientSocket, new String(socketData.name, "GBK"), socketData.canChangeStage);
- systemTrayManager.addTray(socketData.uuid, tray);
- }
- case LOGOUT -> {
- systemTrayManager.removeTray(socketData.uuid);
- flag = true;
- }
- case KEEP_ACTION -> tray.optKeepAnimEnHandler();
- case NO_KEEP_ACTION -> tray.optKeepAnimDisHandler();
- case TRANSPARENT_MODE -> tray.optTransparentEnHandler();
- case NO_TRANSPARENT_MODE -> tray.optTransparentDisHandler();
- case CHANGE_STAGE -> tray.optChangeStageHandler();
- }
- if (flag)
- break;
- }
-
- Logger.info("Socket", "Client(%s:%d) disconnected.".formatted(clientSocket.getInetAddress().getHostAddress(), clientSocket.getPort()));
- tray.optExitHandler();
- InteriorSocketServer.getInstance().removeClientSocket(clientSocket);
- InteriorSocketServer.getInstance().removeClientHandler(this);
- if (clientSocket.isClosed()) {
- return;
- }
- clientSocket.close();
- } catch (IOException e) {
- Logger.error("Socket", e.getMessage());
- InteriorSocketServer.getInstance().removeClientSocket(clientSocket);
- InteriorSocketServer.getInstance().removeClientHandler(this);
- }
- }
-}
diff --git a/core/src/cn/harryh/arkpets/tray/HostTray.java b/core/src/cn/harryh/arkpets/tray/HostTray.java
new file mode 100644
index 00000000..07929a68
--- /dev/null
+++ b/core/src/cn/harryh/arkpets/tray/HostTray.java
@@ -0,0 +1,155 @@
+package cn.harryh.arkpets.tray;
+
+import cn.harryh.arkpets.Const;
+import cn.harryh.arkpets.utils.Logger;
+import javafx.application.Platform;
+import javafx.stage.Stage;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+
+public class HostTray {
+ protected TrayIcon trayIcon;
+ protected boolean initialized = false;
+ protected Map arkPetTrays = new HashMap<>();
+
+ private JPopupMenu popupMenu;
+ private JDialog popWindow;
+ private JMenu playerMenu;
+ private Stage stage;
+
+ static {
+ Const.FontsConfig.loadFontsToSwing();
+ }
+
+ public HostTray(Stage boundStage) {
+ if (SystemTray.isSupported()) {
+ Platform.setImplicitExit(false);
+ SystemTray tray = SystemTray.getSystemTray();
+
+ // Ui Components:
+ popWindow = new JDialog();
+ popWindow.setUndecorated(true);
+ popWindow.setSize(1, 1);
+ JLabel innerLabel = new JLabel(" ArkPets ");
+ innerLabel.setAlignmentX(0.5f);
+
+ playerMenu = new JMenu("角色管理");
+ JMenuItem exitItem = new JMenuItem("退出程序");
+ exitItem.addActionListener(e -> {
+ Logger.info("HostTray", "Request to exit");
+ Platform.exit();
+ });
+
+ popupMenu = new JPopupMenu() {
+ @Override
+ public void firePopupMenuWillBecomeInvisible() {
+ popWindow.setVisible(false); // Hide the container when the menu is invisible.
+ }
+ };
+ popupMenu.add(innerLabel);
+ popupMenu.addSeparator();
+ popupMenu.add(playerMenu);
+ popupMenu.add(exitItem);
+ popupMenu.setSize(100, 24 * popupMenu.getSubElements().length);
+
+ Image image = Toolkit.getDefaultToolkit().getImage(HostTray.class.getResource(Const.iconFilePng));
+ trayIcon = new TrayIcon(image, "ArkPets");
+ trayIcon.setImageAutoSize(true);
+
+ trayIcon.addMouseListener(new MouseAdapter() {
+ public void mouseReleased(MouseEvent e) {
+ if (e.getButton() == 3 && e.isPopupTrigger())
+ showDialog(e.getX() + 5, e.getY());
+ }
+ });
+
+ // Bind JavaFX stage:
+ stage = boundStage;
+ stage.xProperty().addListener((observable, oldValue, newValue) -> {
+ });
+ stage.yProperty().addListener(((observable, oldValue, newValue) -> {
+ }));
+
+ stage.iconifiedProperty().addListener(((observable, oldValue, newValue) -> {
+ if (newValue)
+ hideStage();
+ }));
+ stage.setOnCloseRequest(e -> hideStage());
+ trayIcon.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (e.getButton() == 1)
+ showStage();
+ }
+ });
+
+ try {
+ tray.add(trayIcon);
+ initialized = true;
+ Logger.info("HostTray", "HostTray icon applied");
+ } catch (AWTException e) {
+ Logger.error("HostTray", "Unable to apply HostTray icon, details see below.", e);
+ }
+ } else {
+ Logger.error("HostTray", "Tray is not supported.");
+ }
+ }
+
+ public void showDialog(int x, int y) {
+ AffineTransform at = popWindow.getGraphicsConfiguration().getDefaultTransform();
+
+ // Show the JDialog together with the JPopupMenu.
+ popWindow.setVisible(true);
+ popWindow.setLocation((int) (x / at.getScaleX()), (int) (y / at.getScaleY()) - popupMenu.getHeight());
+ popupMenu.show(popWindow, 0, 0);
+ }
+
+ public void hideStage() {
+ if (!initialized)
+ return;
+ stage.hide();
+ }
+
+ public void showStage() {
+ if (!initialized)
+ return;
+ Platform.runLater(() -> {
+ if (stage.isIconified()) {
+ stage.setIconified(false);
+ }
+ if (!stage.isShowing()) {
+ stage.show();
+ }
+ stage.toFront();
+ });
+ }
+
+ public MemberTray getMemberTray(UUID uuid) {
+ return arkPetTrays.get(uuid);
+ }
+
+ public void addMemberTray(JMenu menu) {
+ playerMenu.add(menu);
+ }
+
+ public void removeMemberTray(JMenu menu) {
+ playerMenu.remove(menu);
+ }
+
+ public void addMemberTray(UUID uuid, MemberTray tray) {
+ arkPetTrays.put(uuid, tray);
+ }
+
+ public void removeMemberTray(UUID uuid) {
+ getMemberTray(uuid).remove();
+ arkPetTrays.remove(uuid);
+ }
+}
diff --git a/core/src/cn/harryh/arkpets/tray/MemberTray.java b/core/src/cn/harryh/arkpets/tray/MemberTray.java
new file mode 100644
index 00000000..798b7311
--- /dev/null
+++ b/core/src/cn/harryh/arkpets/tray/MemberTray.java
@@ -0,0 +1,58 @@
+package cn.harryh.arkpets.tray;
+
+import cn.harryh.arkpets.Const;
+import cn.harryh.arkpets.concurrent.SocketData;
+
+import javax.swing.*;
+import java.util.UUID;
+
+
+public abstract class MemberTray {
+ protected JMenuItem optKeepAnimEn = new JMenuItem("保持动作");
+ protected JMenuItem optKeepAnimDis = new JMenuItem("取消保持");
+ protected JMenuItem optTransparentEn = new JMenuItem("透明模式");
+ protected JMenuItem optTransparentDis = new JMenuItem("取消透明");
+ protected JMenuItem optChangeStage = new JMenuItem("切换形态");
+ protected JMenuItem optExit = new JMenuItem("退出");
+ protected final UUID uuid;
+ protected final String name;
+
+ static {
+ Const.FontsConfig.loadFontsToSwing();
+ }
+
+ public MemberTray(UUID uuid, String name) {
+ this.uuid = uuid;
+ this.name = name;
+
+ optKeepAnimEn .addActionListener(e -> onKeepAnimEn());
+ optKeepAnimDis .addActionListener(e -> onKeepAnimDis());
+ optTransparentEn .addActionListener(e -> onTransparentEn());
+ optTransparentDis .addActionListener(e -> onTransparentDis());
+ optChangeStage .addActionListener(e -> onChangeStage());
+ optExit .addActionListener(e -> onExit());
+
+ optKeepAnimEn .addActionListener(e -> sendRequest(SocketData.Operation.KEEP_ACTION));
+ optKeepAnimDis .addActionListener(e -> sendRequest(SocketData.Operation.NO_KEEP_ACTION));
+ optTransparentEn .addActionListener(e -> sendRequest(SocketData.Operation.TRANSPARENT_MODE));
+ optTransparentDis .addActionListener(e -> sendRequest(SocketData.Operation.NO_TRANSPARENT_MODE));
+ optChangeStage .addActionListener(e -> sendRequest(SocketData.Operation.CHANGE_STAGE));
+ optExit .addActionListener(e -> sendRequest(SocketData.Operation.LOGOUT));
+ }
+
+ abstract public void onExit();
+
+ abstract public void onChangeStage();
+
+ abstract public void onTransparentDis();
+
+ abstract public void onTransparentEn();
+
+ abstract public void onKeepAnimDis();
+
+ abstract public void onKeepAnimEn();
+
+ abstract public void remove();
+
+ abstract protected void sendRequest(SocketData.Operation operation);
+}
diff --git a/core/src/cn/harryh/arkpets/tray/MemberTrayImpl.java b/core/src/cn/harryh/arkpets/tray/MemberTrayImpl.java
new file mode 100644
index 00000000..eb079818
--- /dev/null
+++ b/core/src/cn/harryh/arkpets/tray/MemberTrayImpl.java
@@ -0,0 +1,222 @@
+/** Copyright (c) 2022-2024, Harry Huang
+ * At GPL-3.0 License
+ */
+package cn.harryh.arkpets.tray;
+
+import cn.harryh.arkpets.ArkPets;
+import cn.harryh.arkpets.animations.AnimData;
+import cn.harryh.arkpets.concurrent.SocketClient;
+import cn.harryh.arkpets.concurrent.SocketData;
+import cn.harryh.arkpets.utils.Logger;
+import com.badlogic.gdx.Gdx;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.geom.AffineTransform;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.UUID;
+
+import static cn.harryh.arkpets.Const.iconFilePng;
+import static cn.harryh.arkpets.Const.linearEasingDuration;
+
+
+public class MemberTrayImpl extends MemberTray {
+ private final ArkPets arkPets;
+ private final SocketClient socketClient;
+ private final JDialog popWindow;
+ private final JPopupMenu popMenu;
+ private TrayIcon icon;
+ public AnimData keepAnim;
+
+ /** Initializes the ArkPets tray icon instance.
+ * Must be used after Gdx.app was initialized.
+ * @param boundArkPets The ArkPets instance that bound to the tray icon.
+ */
+ public MemberTrayImpl(ArkPets boundArkPets, SocketClient socketClient, UUID uuid) {
+ super(uuid, getName(boundArkPets));
+ arkPets = boundArkPets;
+ this.socketClient = socketClient;
+
+ // Ui Components:
+ popWindow = new JDialog();
+ popWindow.setUndecorated(true);
+ popWindow.setSize(1, 1);
+ JLabel innerLabel = new JLabel(" " + name + " ");
+ innerLabel.setAlignmentX(0.5f);
+
+ popMenu = new JPopupMenu() {
+ @Override
+ public void firePopupMenuWillBecomeInvisible() {
+ popWindow.setVisible(false); // Hide the container when the menu is invisible.
+ }
+ };
+ popMenu.add(innerLabel);
+ popMenu.add(optKeepAnimEn);
+ popMenu.add(optTransparentEn);
+ if (arkPets.canChangeStage())
+ popMenu.add(optChangeStage);
+ popMenu.add(optExit);
+ popMenu.setSize(100, 24 * popMenu.getSubElements().length);
+
+ socketClient.connectWithRetry(() -> {
+ socketClient.setHandler(new SocketClient.ClientSocketSession(socketClient, this));
+ socketClient.sendRequest(new SocketData(this.uuid, SocketData.Operation.LOGIN, name, arkPets.canChangeStage()));
+ });
+ }
+
+ private static String getName(ArkPets boundArkPets) {
+ return (boundArkPets.config.character_label == null || boundArkPets.config.character_label.isEmpty()) ?
+ "Unknown" : boundArkPets.config.character_label;
+ }
+
+ private TrayIcon getTrayIcon(Image image) {
+ if (icon == null) {
+ icon = new TrayIcon(image, name);
+ icon.setImageAutoSize(true);
+ icon.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ if (e.getButton() == 3 && e.isPopupTrigger())
+ showDialog(e.getX() + 5, e.getY());
+ }
+ });
+ }
+ return icon;
+ }
+
+ @Override
+ public void onExit() {
+ Logger.info("MemberTray", "Request to exit");
+ arkPets.windowAlpha.reset(0f);
+ remove();
+ new Timer().schedule(new TimerTask() {
+ @Override
+ public void run() {
+ Gdx.app.exit();
+ }
+ }, (int)(linearEasingDuration * 1000));
+ }
+
+ @Override
+ public void onChangeStage() {
+ Logger.info("MemberTray", "Request to change stage");
+ arkPets.changeStage();
+ if (keepAnim != null) {
+ keepAnim = null;
+ popMenu.remove(optKeepAnimDis);
+ popMenu.add(optKeepAnimEn, 1);
+ }
+ }
+
+ @Override
+ public void onTransparentDis() {
+ Logger.info("MemberTray", "Transparent disabled");
+ arkPets.windowAlpha.reset(1f);
+ arkPets.hWndMine.setWindowTransparent(false);
+ popMenu.remove(optTransparentDis);
+ popMenu.add(optTransparentEn, 2);
+ }
+
+ @Override
+ public void onTransparentEn() {
+ Logger.info("MemberTray", "Transparent enabled");
+ arkPets.windowAlpha.reset(0.75f);
+ arkPets.hWndMine.setWindowTransparent(true);
+ popMenu.remove(optTransparentEn);
+ popMenu.add(optTransparentDis, 2);
+ }
+
+ @Override
+ public void onKeepAnimDis() {
+ Logger.info("MemberTray", "Keep-Anim disabled");
+ keepAnim = null;
+ popMenu.remove(optKeepAnimDis);
+ popMenu.add(optKeepAnimEn, 1);
+ }
+
+ @Override
+ public void onKeepAnimEn() {
+ Logger.info("MemberTray", "Keep-Anim enabled");
+ keepAnim = arkPets.cha.getPlaying();
+ popMenu.remove(optKeepAnimEn);
+ popMenu.add(optKeepAnimDis, 1);
+ }
+
+ @Override
+ protected void sendRequest(SocketData.Operation operation) {
+ socketClient.sendRequest(new SocketData(uuid, operation));
+ }
+
+ @Override
+ public void remove() {
+ popMenu.removeAll();
+ popWindow.dispose();
+ socketClient.disconnect();
+ }
+
+ public void onDisconnected() {
+ // When connection was broken:
+ Logger.info("MemberTray", "Integrated tray service disconnected");
+ Image image = Toolkit.getDefaultToolkit().createImage(getClass().getResource(iconFilePng));
+ TrayIcon icon = getTrayIcon(image);
+
+ // Add the ISOLATED tray icon to the system tray.
+ try {
+ SystemTray.getSystemTray().add(icon);
+ Logger.info("MemberTray", "Isolated tray icon applied");
+ } catch (AWTException e) {
+ Logger.error("MemberTray", "Unable to apply isolated tray icon, details see below", e);
+ }
+ }
+
+ public void onReconnected() {
+ // If integration was succeeded, remove the ISOLATED tray icon.
+ Logger.info("MemberTray", "Integrated tray service reconnected");
+ SystemTray.getSystemTray().remove(icon);
+ socketClient.sendRequest(new SocketData(this.uuid, SocketData.Operation.LOGIN, name, arkPets.canChangeStage()));
+ for (MenuElement element : popMenu.getSubElements()) {
+ if (element.equals(optKeepAnimDis))
+ sendRequest(SocketData.Operation.KEEP_ACTION);
+ if (element.equals(optTransparentDis))
+ sendRequest(SocketData.Operation.TRANSPARENT_MODE);
+ }
+ }
+
+ /** Hides the menu.
+ */
+ public void hideDialog() {
+ if (popMenu.isVisible()) {
+ popMenu.setVisible(false);
+ Logger.debug("MemberTray", "Hidden");
+ }
+ }
+
+ /** Shows the menu at the given coordinate.
+ */
+ public void showDialog(int x, int y) {
+ /* Use `System.setProperty("sun.java2d.uiScale", "1")` can also avoid system scaling.
+ Here we will adapt the coordinate for system scaling artificially. See below. */
+ AffineTransform at = popWindow.getGraphicsConfiguration().getDefaultTransform();
+ int scaledX = (int) (x / at.getScaleX());
+ int scaledY = (int) (y / at.getScaleY());
+
+ // Show the JDialog together with the JPopupMenu.
+ popWindow.setVisible(true);
+ popWindow.setLocation(scaledX, scaledY - popMenu.getHeight());
+ popMenu.show(popWindow, 0, 0);
+ Logger.debug("MemberTray", "Shown @ " + x + ", " + y);
+ }
+
+ /** Toggles the menu at the given coordinate.
+ */
+ public void toggleDialog(int x, int y) {
+ if (popMenu.isVisible()) {
+ hideDialog();
+ } else {
+ showDialog(x, y);
+ }
+ }
+}
diff --git a/core/src/cn/harryh/arkpets/tray/MemberTrayProxy.java b/core/src/cn/harryh/arkpets/tray/MemberTrayProxy.java
new file mode 100644
index 00000000..ce675909
--- /dev/null
+++ b/core/src/cn/harryh/arkpets/tray/MemberTrayProxy.java
@@ -0,0 +1,96 @@
+package cn.harryh.arkpets.tray;
+
+import cn.harryh.arkpets.concurrent.SocketData;
+import cn.harryh.arkpets.utils.Logger;
+import com.alibaba.fastjson2.JSONObject;
+
+import javax.swing.*;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.nio.charset.Charset;
+
+
+public class MemberTrayProxy extends MemberTray {
+ private final PrintWriter socketOut;
+ private final HostTray hostTray;
+ private final JMenu popMenu;
+
+ public MemberTrayProxy(SocketData socketData, Socket clientSocket, HostTray hostTray) {
+ super(socketData.uuid, new String(socketData.name, Charset.forName("GBK")));
+ this.hostTray = hostTray;
+ try {
+ socketOut = new PrintWriter(clientSocket.getOutputStream(), true);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ // Ui Components:
+ JLabel innerLabel = new JLabel(" " + name + " ");
+ innerLabel.setAlignmentX(0.5f);
+
+ popMenu = new JMenu(name);
+ popMenu.add(innerLabel);
+ popMenu.add(optKeepAnimEn);
+ popMenu.add(optTransparentEn);
+ if (socketData.canChangeStage)
+ popMenu.add(optChangeStage);
+ popMenu.add(optExit);
+ popMenu.setSize(100, 24 * popMenu.getSubElements().length);
+
+ hostTray.addMemberTray(popMenu);
+ }
+
+ @Override
+ public void onExit() {
+ Logger.info("ProxyTray", "Request to exit");
+ remove();
+ }
+
+ @Override
+ public void onChangeStage() {
+ Logger.info("ProxyTray", "Request to change stage");
+ popMenu.remove(optKeepAnimDis);
+ popMenu.add(optKeepAnimEn, 1);
+ }
+
+ @Override
+ public void onTransparentDis() {
+ Logger.info("ProxyTray", "Transparent disabled");
+ popMenu.remove(optTransparentDis);
+ popMenu.add(optTransparentEn, 2);
+ }
+
+ @Override
+ public void onTransparentEn() {
+ Logger.info("ProxyTray", "Transparent enabled");
+ popMenu.remove(optTransparentEn);
+ popMenu.add(optTransparentDis, 2);
+ }
+
+ @Override
+ public void onKeepAnimDis() {
+ Logger.info("ProxyTray", "Keep-Anim disabled");
+ popMenu.remove(optKeepAnimDis);
+ popMenu.add(optKeepAnimEn, 1);
+ }
+
+ @Override
+ public void onKeepAnimEn() {
+ Logger.info("ProxyTray", "Keep-Anim enabled");
+ popMenu.remove(optKeepAnimEn);
+ popMenu.add(optKeepAnimDis, 1);
+ }
+
+ @Override
+ protected void sendRequest(SocketData.Operation operation) {
+ socketOut.println(JSONObject.toJSONString(new SocketData(uuid, operation)));
+ }
+
+ @Override
+ public void remove() {
+ hostTray.removeMemberTray(popMenu);
+ sendRequest(SocketData.Operation.LOGOUT);
+ socketOut.close();
+ }
+}
diff --git a/core/src/cn/harryh/arkpets/tray/SystemTrayManager.java b/core/src/cn/harryh/arkpets/tray/SystemTrayManager.java
deleted file mode 100644
index b0f14bbe..00000000
--- a/core/src/cn/harryh/arkpets/tray/SystemTrayManager.java
+++ /dev/null
@@ -1,176 +0,0 @@
-package cn.harryh.arkpets.tray;
-
-import cn.harryh.arkpets.utils.Logger;
-import javafx.application.Platform;
-import javafx.stage.Stage;
-
-import javax.swing.*;
-import java.awt.*;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.event.MouseListener;
-import java.awt.geom.AffineTransform;
-import java.util.UUID;
-
-import static cn.harryh.arkpets.Const.iconFilePng;
-
-
-public class SystemTrayManager extends TrayManager {
- private static SystemTrayManager instance = null;
- private volatile JPopupMenu popupMenu;
- private volatile JDialog popWindow;
- private volatile JMenu playerMenu;
- private static Stage stage;
- private static double x = 0;
- private static double y = 0;
-
- public static synchronized SystemTrayManager getInstance() {
- if (instance == null)
- instance = new SystemTrayManager();
- return instance;
- }
-
- private SystemTrayManager() {
- super();
- if (SystemTray.isSupported()) {
- Platform.setImplicitExit(false);
- tray = SystemTray.getSystemTray();
-
- popWindow = new JDialog();
- popWindow.setUndecorated(true);
- popWindow.setSize(1, 1);
-
- popupMenu = new JPopupMenu() {
- @Override
- public void firePopupMenuWillBecomeInvisible() {
- popWindow.setVisible(false);
- }
- };
-
- JLabel innerLabel = new JLabel(" ArkPets ");
- innerLabel.setAlignmentX(0.5f);
- popupMenu.add(innerLabel);
-
- playerMenu = new JMenu("干员管理");
- JMenuItem exitItem = new JMenuItem("退出程序");
- exitItem.addActionListener(e -> Platform.exit());
-
- popupMenu.addSeparator();
- popupMenu.add(playerMenu);
- popupMenu.add(exitItem);
- popupMenu.setSize(100, 24 * popupMenu.getSubElements().length);
-
- Image image = Toolkit.getDefaultToolkit().getImage(SystemTrayManager.class.getResource(iconFilePng));
- trayIcon = new TrayIcon(image, "ArkPets");
- trayIcon.setImageAutoSize(true);
-
- trayIcon.addMouseListener(new MouseAdapter() {
- public void mouseReleased(MouseEvent e) {
- if (e.getButton() == 3 && e.isPopupTrigger()) {
- showDialog(e.getX() + 5, e.getY());
- }
- }
- });
-
- try {
- tray.add(trayIcon);
- initialized = true;
- Logger.info("SystemTrayManager", "SystemTray icon applied");
- } catch (AWTException e) {
- Logger.error("SystemTrayManager", "Unable to apply tray icon, details see below", e);
- }
- return;
- }
- Logger.error("SystemTrayManager", "SystemTray is not supported.");
- }
-
- @Override
- public void showDialog(int x, int y) {
- AffineTransform at = popWindow.getGraphicsConfiguration().getDefaultTransform();
-
- // Show the JDialog together with the JPopupMenu.
- popWindow.setVisible(true);
- popWindow.setLocation((int) (x / at.getScaleX()), (int) (y / at.getScaleY()) - popupMenu.getHeight());
- popupMenu.show(popWindow, 0, 0);
- }
-
- @Override
- public void listen(Stage stage) {
- if (!initialized)
- return;
- SystemTrayManager.stage = stage;
- trayIcon.removeMouseListener(new MouseAdapter() {
- });
- MouseListener mouseListener = new MouseAdapter() {
- @Override
- public void mouseClicked(MouseEvent e) {
- if (e.getButton() == MouseEvent.BUTTON1) {
- showStage();
- }
- }
- };
- x = stage.getX();
- y = stage.getY();
- trayIcon.addMouseListener(mouseListener);
- }
-
- @Override
- public void hide() {
- if (!initialized)
- return;
- Platform.runLater(() -> {
- if (SystemTray.isSupported()) {
- x = stage.getX();
- y = stage.getY();
- stage.hide();
- return;
- }
- System.exit(0);
- });
- }
-
- private void showStage() {
- if (!initialized)
- return;
- Platform.runLater(() -> {
- if (stage.isIconified()) {
- stage.setIconified(false);
- }
- if (!stage.isShowing()) {
- stage.setX(x);
- stage.setY(y);
- stage.show();
- }
- stage.toFront();
- });
- }
-
- public void showLauncher() {
- showStage();
- }
-
- @Override
- public void addTray(UUID uuid, Tray tray) {
- arkPetTrays.put(uuid, tray);
-// sendInfoMessage("新桌宠连接", "%s", tray.getName());
- }
-
- @Override
- public void removeTray(UUID uuid) {
- getTray(uuid).removeTray();
- arkPetTrays.remove(uuid);
- }
-
- @Override
- public Tray getTray(UUID uuid) {
- return arkPetTrays.get(uuid);
- }
-
- public void addTray(JMenu menu) {
- playerMenu.add(menu);
- }
-
- public void removeTray(JMenu menu) {
- playerMenu.remove(menu);
- }
-}
diff --git a/core/src/cn/harryh/arkpets/tray/Tray.java b/core/src/cn/harryh/arkpets/tray/Tray.java
deleted file mode 100644
index 766e1f40..00000000
--- a/core/src/cn/harryh/arkpets/tray/Tray.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package cn.harryh.arkpets.tray;
-
-import javax.swing.*;
-import java.util.UUID;
-
-
-public abstract class Tray {
- protected JMenuItem optKeepAnimEn = new JMenuItem("保持动作");
- protected JMenuItem optKeepAnimDis = new JMenuItem("取消保持");
- protected JMenuItem optTransparentEn = new JMenuItem("透明模式");
- protected JMenuItem optTransparentDis = new JMenuItem("取消透明");
- protected JMenuItem optChangeStage = new JMenuItem("切换形态");
- protected JMenuItem optExit = new JMenuItem("退出");
- protected final UUID uuid;
- protected String name;
-
- public Tray(UUID uuid) {
- this.uuid = uuid;
- // This Dialog is the container (the "anchor") of the PopupMenu:
- optKeepAnimEn.addActionListener(e -> optKeepAnimEnHandler());
- optKeepAnimDis.addActionListener(e -> optKeepAnimDisHandler());
- optTransparentEn.addActionListener(e -> optTransparentEnHandler());
- optTransparentDis.addActionListener(e -> optTransparentDisHandler());
- optChangeStage.addActionListener(e -> optChangeStageHandler());
- optExit.addActionListener(e -> optExitHandler());
- }
-
- public String getName() {
- return name;
- }
-
- protected abstract void addComponent();
-
- protected abstract void optExitHandler();
-
- protected abstract void optChangeStageHandler();
-
- protected abstract void optTransparentDisHandler();
-
- protected abstract void optTransparentEnHandler();
-
- protected abstract void optKeepAnimDisHandler();
-
- protected abstract void optKeepAnimEnHandler();
-
- public abstract void removeTray();
-}
diff --git a/core/src/cn/harryh/arkpets/tray/TrayInstance.java b/core/src/cn/harryh/arkpets/tray/TrayInstance.java
deleted file mode 100644
index a2b8ba12..00000000
--- a/core/src/cn/harryh/arkpets/tray/TrayInstance.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package cn.harryh.arkpets.tray;
-
-import cn.harryh.arkpets.socket.SocketData;
-import cn.harryh.arkpets.utils.Logger;
-import com.alibaba.fastjson2.JSONObject;
-
-import javax.swing.*;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.net.Socket;
-import java.util.UUID;
-
-
-public class TrayInstance extends Tray {
- private final PrintWriter socketOut;
- private final boolean canChangeStage;
- private final JMenu popMenu;
-
- public TrayInstance(UUID uuid, Socket socket, String name, boolean canChangeStage) {
- super(uuid);
- this.name = name;
- this.canChangeStage = canChangeStage;
- popMenu = new JMenu(name);
- try {
- socketOut = new PrintWriter(socket.getOutputStream(), true);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- addComponent();
- }
-
- private void sendRequest(SocketData data) {
- socketOut.println(JSONObject.toJSONString(data));
- }
-
- @Override
- protected void addComponent() {
- JLabel innerLabel = new JLabel(" " + name + " ");
- innerLabel.setAlignmentX(0.5f);
- popMenu.add(innerLabel);
-
- optKeepAnimEn.addActionListener(e -> sendRequest(new SocketData(uuid, SocketData.OperateType.KEEP_ACTION)));
- optKeepAnimDis.addActionListener(e -> sendRequest(new SocketData(uuid, SocketData.OperateType.NO_KEEP_ACTION)));
- optTransparentEn.addActionListener(e -> sendRequest(new SocketData(uuid, SocketData.OperateType.TRANSPARENT_MODE)));
- optTransparentDis.addActionListener(e -> sendRequest(new SocketData(uuid, SocketData.OperateType.NO_TRANSPARENT_MODE)));
- optChangeStage.addActionListener(e -> sendRequest(new SocketData(uuid, SocketData.OperateType.CHANGE_STAGE)));
- optExit.addActionListener(e -> sendRequest(new SocketData(uuid, SocketData.OperateType.LOGOUT)));
-
- popMenu.add(optKeepAnimEn);
- popMenu.add(optTransparentEn);
- if (canChangeStage) popMenu.add(optChangeStage);
- popMenu.add(optExit);
- popMenu.setSize(100, 24 * popMenu.getSubElements().length);
- SystemTrayManager.getInstance().addTray(popMenu);
- }
-
- @Override
- public void removeTray() {
- SystemTrayManager.getInstance().removeTray(popMenu);
- sendRequest(new SocketData(uuid, SocketData.OperateType.LOGOUT));
- socketOut.close();
- }
-
- @Override
- protected void optExitHandler() {
- Logger.info("SocketTray", "Request to exit");
- removeTray();
- }
-
- @Override
- protected void optChangeStageHandler() {
- Logger.info("SocketTray", "Request to change stage");
- popMenu.remove(optKeepAnimDis);
- popMenu.add(optKeepAnimEn, 1);
- }
-
- @Override
- protected void optTransparentDisHandler() {
- Logger.info("SocketTray", "Transparent disabled");
- popMenu.remove(optTransparentDis);
- popMenu.add(optTransparentEn, 2);
- }
-
- @Override
- protected void optTransparentEnHandler() {
- Logger.info("SocketTray", "Transparent enabled");
- popMenu.remove(optTransparentEn);
- popMenu.add(optTransparentDis, 2);
- }
-
- @Override
- protected void optKeepAnimDisHandler() {
- Logger.info("SocketTray", "Keep-Anim disabled");
- popMenu.remove(optKeepAnimDis);
- popMenu.add(optKeepAnimEn, 1);
- }
-
- @Override
- protected void optKeepAnimEnHandler() {
- Logger.info("SocketTray", "Keep-Anim enabled");
- popMenu.remove(optKeepAnimEn);
- popMenu.add(optKeepAnimDis, 1);
- }
-}
diff --git a/core/src/cn/harryh/arkpets/tray/TrayManager.java b/core/src/cn/harryh/arkpets/tray/TrayManager.java
deleted file mode 100644
index e172def5..00000000
--- a/core/src/cn/harryh/arkpets/tray/TrayManager.java
+++ /dev/null
@@ -1,84 +0,0 @@
-package cn.harryh.arkpets.tray;
-
-import cn.harryh.arkpets.ArkTray;
-import cn.harryh.arkpets.utils.Logger;
-import javafx.stage.Stage;
-
-import javax.swing.*;
-import java.awt.*;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.UUID;
-
-import static cn.harryh.arkpets.Const.fontFileRegular;
-
-public abstract class TrayManager {
- protected volatile SystemTray tray;
- protected volatile TrayIcon trayIcon;
- protected boolean initialized = false;
- protected Map arkPetTrays = new HashMap<>();
- public static Font font;
-
- static {
- try {
- InputStream inputStream = Objects.requireNonNull(ArkTray.class.getResourceAsStream(fontFileRegular));
- font = Font.createFont(Font.TRUETYPE_FONT, inputStream);
- if (font != null) {
- UIManager.put("Label.font", font.deriveFont(9f).deriveFont(Font.ITALIC));
- UIManager.put("MenuItem.font", font.deriveFont(11f));
- }
- } catch (FontFormatException | IOException e) {
- Logger.error("Tray", "Failed to load tray menu font, details see below.", e);
- font = null;
- }
- }
-
- public abstract void showDialog(int x, int y);
-
- public abstract void listen(Stage stage);
-
- public abstract void hide();
-
- public abstract void addTray(UUID uuid, Tray tray);
-
- public abstract void removeTray(UUID uuid);
-
- public abstract Tray getTray(UUID uuid);
-
-
- public SystemTray getTray() {
- return tray;
- }
-
- /**
- * Send system tray information
- * @param messageType info type
- * @param title title
- * @param content content
- * @param args content args
- */
- private void sendMessage(TrayIcon.MessageType messageType, String title, String content, Object... args) {
- if (!initialized)
- return;
- trayIcon.displayMessage(title, content.formatted(args), messageType);
- }
-
- public void sendInfoMessage(String title, String content, Object... args) {
- sendMessage(TrayIcon.MessageType.INFO, title, content, args);
- }
-
- public void sendErrorMessage(String title, String content, Object... args) {
- sendMessage(TrayIcon.MessageType.ERROR, title, content, args);
- }
-
- public void sendWarnMessage(String title, String content, Object... args) {
- sendMessage(TrayIcon.MessageType.WARNING, title, content, args);
- }
-
- public void sendMessage(String title, String content, Object... args) {
- sendMessage(TrayIcon.MessageType.NONE, title, content, args);
- }
-}
diff --git a/core/src/cn/harryh/arkpets/utils/IOUtils.java b/core/src/cn/harryh/arkpets/utils/IOUtils.java
index 92aa0802..98376600 100644
--- a/core/src/cn/harryh/arkpets/utils/IOUtils.java
+++ b/core/src/cn/harryh/arkpets/utils/IOUtils.java
@@ -3,16 +3,7 @@
*/
package cn.harryh.arkpets.utils;
-import cn.harryh.arkpets.exception.NoPortAvailableException;
-import cn.harryh.arkpets.exception.NoServerRunningException;
-import cn.harryh.arkpets.exception.ServerRunningException;
-import cn.harryh.arkpets.socket.SocketData;
-import com.alibaba.fastjson2.JSONObject;
-
import java.io.*;
-import java.net.DatagramSocket;
-import java.net.Socket;
-import java.net.SocketException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -21,64 +12,13 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Enumeration;
-import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
-import static cn.harryh.arkpets.Const.serverPorts;
import static cn.harryh.arkpets.Const.zipBufferSizeDefault;
public class IOUtils {
-
- public static class NetUtils {
- /**
- * Get available server port for client to connect
- * @return int
- * @throws NoServerRunningException if server is not running
- */
- public static int getServerPort() throws NoServerRunningException {
- for (int serverPort : serverPorts) {
- try (Socket socket = new Socket("localhost", serverPort)) {
- socket.setSoTimeout(100);
- PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
- BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
- out.println(JSONObject.toJSONString(new SocketData(UUID.randomUUID(), SocketData.OperateType.VERIFY)));
- SocketData socketData = JSONObject.parseObject(in.readLine(), SocketData.class);
- out.close();
- in.close();
- if (socketData.operateType == SocketData.OperateType.SERVER_ONLINE) {
- return serverPort;
- }
- } catch (IOException ignored) {
- }
- }
- throw new NoServerRunningException();
- }
-
- /**
- * Get available server port for server to bind
- * @return int
- * @throws NoPortAvailableException if every port was bound
- * @throws ServerRunningException if the server is running
- */
- public static int getAvailablePort() throws NoPortAvailableException, ServerRunningException {
- try {
- getServerPort();
- throw new ServerRunningException();
- } catch (NoServerRunningException ignore) {
- }
- for (int serverPort : serverPorts) {
- try (DatagramSocket ignored = new DatagramSocket(serverPort)) {
- return serverPort;
- } catch (SocketException ignored) {
- }
- }
- throw new NoPortAvailableException();
- }
-
- }
-
public static class FileUtil {
/** Reads the entire file into a byte array.
* @param file The file to be read.
diff --git a/desktop/src/cn/harryh/arkpets/ArkHomeFX.java b/desktop/src/cn/harryh/arkpets/ArkHomeFX.java
index c13a1a41..91ff2ab6 100644
--- a/desktop/src/cn/harryh/arkpets/ArkHomeFX.java
+++ b/desktop/src/cn/harryh/arkpets/ArkHomeFX.java
@@ -4,14 +4,12 @@
package cn.harryh.arkpets;
import cn.harryh.arkpets.assets.ModelsDataset;
+import cn.harryh.arkpets.concurrent.*;
import cn.harryh.arkpets.controllers.BehaviorModule;
import cn.harryh.arkpets.controllers.ModelsModule;
import cn.harryh.arkpets.controllers.RootModule;
import cn.harryh.arkpets.controllers.SettingsModule;
-import cn.harryh.arkpets.socket.InteriorSocketServer;
-import cn.harryh.arkpets.socket.SocketClient;
-import cn.harryh.arkpets.socket.SocketData;
-import cn.harryh.arkpets.tray.SystemTrayManager;
+import cn.harryh.arkpets.tray.HostTray;
import cn.harryh.arkpets.utils.FXMLHelper;
import cn.harryh.arkpets.utils.FXMLHelper.LoadFXMLResult;
import cn.harryh.arkpets.utils.Logger;
@@ -22,7 +20,6 @@
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
-import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
@@ -39,6 +36,7 @@ public class ArkHomeFX extends Application {
public Stage stage;
public ArkConfig config;
public ModelsDataset modelsDataset;
+ public HostTray hostTray;
public StackPane root;
public RootModule rootModule;
@@ -46,44 +44,34 @@ public class ArkHomeFX extends Application {
public BehaviorModule behaviorModule;
public SettingsModule settingsModule;
+ static {
+ FontsConfig.loadFontsToJavafx();
+ }
+
@Override
public void start(Stage stage) throws Exception {
Logger.info("Launcher", "Starting");
this.stage = stage;
- // Socket server initialize
- Logger.info("Socket", "Check server status");
- int status = InteriorSocketServer.getInstance().checkServerAvailable();
- switch (status) {
- case 1: {
- // No server started, start server
- Logger.info("Socket", "Server starting");
- InteriorSocketServer.getInstance().startServer();
- break;
- }
- case 0: {
- // No available port
- Logger.error("Socket", "No available port");
- // TODO
- // What if there are no port available
- Platform.exit();
- }
- case -1: {
- // Server is running
- Logger.error("Socket", "Server is running");
- SocketClient socketClient = new SocketClient();
- socketClient.connect();
- socketClient.sendRequest(new SocketData(UUID.randomUUID(), SocketData.OperateType.ACTIVATE_LAUNCHER));
+ // Initialize socket server and HostTray
+ hostTray = new HostTray(stage);
+ try {
+ SocketServer.getInstance().startServer(hostTray);
+ } catch (PortUtils.NoPortAvailableException ignored) {
+ Logger.error("SocketServer", "No available port");
+ // TODO What if there are no port available
+ Platform.exit();
+ } catch (PortUtils.ServerCollisionException ignored) {
+ Logger.error("SocketServer", "Server is already running");
+ SocketClient socketClient = new SocketClient();
+ socketClient.connect(() -> {
+ socketClient.sendRequest(new SocketData(UUID.randomUUID(), SocketData.Operation.ACTIVATE_LAUNCHER));
socketClient.disconnect();
Logger.info("Launcher", "ArkPets Launcher has started.");
- System.exit(0);
- }
+ });
+ Platform.exit();
}
- // Load fonts.
- Font.loadFont(getClass().getResourceAsStream(fontFileRegular), Font.getDefault().getSize());
- Font.loadFont(getClass().getResourceAsStream(fontFileBold), Font.getDefault().getSize());
-
// Load FXML for root node.
LoadFXMLResult fxml0 = FXMLHelper.loadFXML(getClass().getResource("/UI/RootModule.fxml"));
fxml0.initializeWith(this);
@@ -101,19 +89,6 @@ public void start(Stage stage) throws Exception {
stage.setTitle(desktopTitle);
rootModule.titleText.setText(desktopTitle);
- // Add system tray and listen stage
- SystemTrayManager.getInstance().listen(stage);
-
- // Listen for tray click event
- stage.iconifiedProperty().addListener(((observable, oldValue, newValue) -> {
- if (newValue) {
- SystemTrayManager.getInstance().hide();
- }
- }));
-
- // Listen window close event
- stage.setOnCloseRequest(e -> SystemTrayManager.getInstance().hide());
-
// After the stage is shown, do initialize modules.
stage.show();
rootModule.popSplashScreen(e -> {
@@ -142,9 +117,8 @@ public void start(Stage stage) throws Exception {
@Override
public void stop() throws Exception {
super.stop();
- InteriorSocketServer.getInstance().stopServer();
- // function shutdown will kill all arkpets started by this launcher
-// ProcessPool.getInstance().shutdown();
+ SocketServer.getInstance().stopServer();
+ ProcessPool.executorService.shutdown();
}
public boolean initModelsDataset(boolean popNotice) {
diff --git a/desktop/src/cn/harryh/arkpets/DesktopLauncher.java b/desktop/src/cn/harryh/arkpets/DesktopLauncher.java
index be97e21f..48d07e6d 100644
--- a/desktop/src/cn/harryh/arkpets/DesktopLauncher.java
+++ b/desktop/src/cn/harryh/arkpets/DesktopLauncher.java
@@ -3,7 +3,7 @@
*/
package cn.harryh.arkpets;
-import cn.harryh.arkpets.process_pool.ProcessPool;
+import cn.harryh.arkpets.concurrent.ProcessPool;
import cn.harryh.arkpets.utils.ArgPending;
import cn.harryh.arkpets.utils.Logger;
import javafx.application.Application;
@@ -13,8 +13,7 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
-import static cn.harryh.arkpets.Const.LogConfig;
-import static cn.harryh.arkpets.Const.appVersion;
+import static cn.harryh.arkpets.Const.*;
/** The entrance of the whole program, also the bootstrap for ArkHomeFX.
diff --git a/desktop/src/cn/harryh/arkpets/controllers/RootModule.java b/desktop/src/cn/harryh/arkpets/controllers/RootModule.java
index a8caa13c..095c18e8 100644
--- a/desktop/src/cn/harryh/arkpets/controllers/RootModule.java
+++ b/desktop/src/cn/harryh/arkpets/controllers/RootModule.java
@@ -8,8 +8,7 @@
import cn.harryh.arkpets.EmbeddedLauncher;
import cn.harryh.arkpets.guitasks.CheckAppUpdateTask;
import cn.harryh.arkpets.guitasks.GuiTask;
-import cn.harryh.arkpets.process_pool.ProcessPool;
-import cn.harryh.arkpets.process_pool.TaskStatus;
+import cn.harryh.arkpets.concurrent.ProcessPool;
import cn.harryh.arkpets.utils.ArgPending;
import cn.harryh.arkpets.utils.GuiPrefabs;
import cn.harryh.arkpets.utils.JavaProcess;
@@ -146,9 +145,9 @@ protected Boolean call() throws InterruptedException, ExecutionException {
// Start ArkPets core.
Logger.info("Launcher", "Launching " + app.config.character_asset);
Logger.debug("Launcher", "With args " + args);
- FutureTask future = ProcessPool.getInstance().submit(EmbeddedLauncher.class, List.of(), args);
+ FutureTask future = ProcessPool.getInstance().submit(EmbeddedLauncher.class, List.of(), args);
// ArkPets core finalized.
- if (Objects.equals(future.get().getStatus(), TaskStatus.Status.FAILURE)) {
+ if (!future.get().isSuccess()) {
Logger.warn("Launcher", "Detected an abnormal finalization of an ArkPets thread (exit code -1). Please check the log file for details.");
lastLaunchFailed = new JavaProcess.UnexpectedExitCodeException(-1);
return false;