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;