diff --git a/.gitignore b/.gitignore index 13559cee..d8216730 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ test*/ # Exclude runtime files /ArkPetsConfig.json /models_data.json +/desktop/src/main/resource/assets/models/ +/desktop/src/main/resource/assets/models_data.json diff --git a/assets/ArkPetsConfigDefault.json b/assets/ArkPetsConfigDefault.json index a376aefc..cf4491f5 100644 --- a/assets/ArkPetsConfigDefault.json +++ b/assets/ArkPetsConfigDefault.json @@ -18,5 +18,7 @@ "physic_gravity_acc":800.0, "physic_speed_limit_x":1000.0, "physic_speed_limit_y":1000.0, - "physic_static_friction_acc":500.0 + "physic_static_friction_acc":500.0, + "server_port": 8080, + "separate_arkpet_from_launcher": false } \ No newline at end of file diff --git a/core/src/cn/harryh/arkpets/ArkConfig.java b/core/src/cn/harryh/arkpets/ArkConfig.java index e94bc746..890063fe 100644 --- a/core/src/cn/harryh/arkpets/ArkConfig.java +++ b/core/src/cn/harryh/arkpets/ArkConfig.java @@ -63,6 +63,8 @@ public class ArkConfig { public float physic_static_friction_acc; public float physic_speed_limit_x; public float physic_speed_limit_y; + public int server_port = 8080; + public boolean separate_arkpet_from_launcher; private ArkConfig() { } diff --git a/core/src/cn/harryh/arkpets/ArkPets.java b/core/src/cn/harryh/arkpets/ArkPets.java index 7a97bac4..cc06b5cb 100644 --- a/core/src/cn/harryh/arkpets/ArkPets.java +++ b/core/src/cn/harryh/arkpets/ArkPets.java @@ -3,17 +3,26 @@ */ package cn.harryh.arkpets; -import cn.harryh.arkpets.animations.*; +import cn.harryh.arkpets.animations.AnimData; +import cn.harryh.arkpets.animations.GeneralBehavior; import cn.harryh.arkpets.assets.AssetItem; -import cn.harryh.arkpets.utils.*; -import cn.harryh.arkpets.transitions.*; +import cn.harryh.arkpets.socket.SocketClient; +import cn.harryh.arkpets.transitions.TernaryFunction; +import cn.harryh.arkpets.transitions.TransitionFloat; +import cn.harryh.arkpets.transitions.TransitionVector2; +import cn.harryh.arkpets.utils.HWndCtrl; +import cn.harryh.arkpets.utils.Logger; +import cn.harryh.arkpets.utils.Plane; import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.math.Vector2; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Objects; +import java.util.UUID; import static cn.harryh.arkpets.Const.*; @@ -79,7 +88,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); + tray = new ArkTray(this, new SocketClient(config.server_port), UUID.randomUUID()); // Setup complete Logger.info("App", "Render"); } @@ -133,7 +142,7 @@ public void resize(int x, int y) { @Override public void dispose() { Logger.info("App", "Dispose"); - tray.remove(); + tray.removeTray(); } public boolean canChangeStage() { diff --git a/core/src/cn/harryh/arkpets/ArkTray.java b/core/src/cn/harryh/arkpets/ArkTray.java index fcb0ea88..ab820675 100644 --- a/core/src/cn/harryh/arkpets/ArkTray.java +++ b/core/src/cn/harryh/arkpets/ArkTray.java @@ -4,32 +4,31 @@ 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.UUID; -import static cn.harryh.arkpets.Const.*; +import static cn.harryh.arkpets.Const.fontFileRegular; +import static cn.harryh.arkpets.Const.linearEasingDuration; -public class ArkTray { +public class ArkTray extends Tray { private final ArkPets arkPets; - private final SystemTray tray; - private final TrayIcon icon; - private final JDialog popWindow; - private final JPopupMenu popMenu; - private boolean isTrayIconApplied; - public String name; - public String title; + private final SocketClient socketClient; public AnimData keepAnim; public static Font font; + private final JDialog popWindow; + private final JPopupMenu popMenu; static { try { @@ -49,18 +48,10 @@ public class ArkTray { * Must be used after Gdx.app was initialized. * @param boundArkPets The ArkPets instance that bound to the tray icon. */ - public ArkTray(ArkPets boundArkPets) { + public ArkTray(ArkPets boundArkPets, SocketClient socket, UUID uuid) { + super(uuid); arkPets = boundArkPets; - tray = SystemTray.getSystemTray(); - name = (arkPets.config.character_label == null || arkPets.config.character_label.isEmpty()) ? "Unknown" : arkPets.config.character_label; - title = name + " - " + appName; - - // Load the tray icon image. - Image image = Toolkit.getDefaultToolkit().createImage(getClass().getResource(iconFilePng)); - icon = new TrayIcon(image, name); - icon.setImageAutoSize(true); - - // This Dialog is the container (the "anchor") of the PopupMenu: + socketClient = socket; popWindow = new JDialog(); popWindow.setUndecorated(true); popWindow.setSize(1, 1); @@ -72,101 +63,109 @@ 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 || 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(); + } + + + @Override + protected void addComponent() { JLabel innerLabel = new JLabel(" " + name + " "); innerLabel.setAlignmentX(0.5f); popMenu.add(innerLabel); - // Menu options: - JMenuItem optKeepAnimEn = new JMenuItem("保持动作"); - JMenuItem optKeepAnimDis = new JMenuItem("取消保持"); - JMenuItem optTransparentEn = new JMenuItem("透明模式"); - JMenuItem optTransparentDis = new JMenuItem("取消透明"); - JMenuItem optChangeStage = new JMenuItem("切换形态"); - JMenuItem optExit = new JMenuItem("退出"); - optKeepAnimEn.addActionListener(e -> { - Logger.info("Tray", "Keep-Anim enabled"); - keepAnim = arkPets.cha.getPlaying(); - popMenu.remove(optKeepAnimEn); - popMenu.add(optKeepAnimDis, 1); - }); - optKeepAnimDis.addActionListener(e -> { - Logger.info("Tray","Keep-Anim disabled"); - keepAnim = null; - popMenu.remove(optKeepAnimDis); - popMenu.add(optKeepAnimEn, 1); - }); - optTransparentEn.addActionListener(e -> { - Logger.info("Tray", "Transparent enabled"); - arkPets.windowAlpha.reset(0.75f); - arkPets.hWndMine.setWindowTransparent(true); - popMenu.remove(optTransparentEn); - popMenu.add(optTransparentDis, 2); - }); - optTransparentDis.addActionListener(e -> { - Logger.info("Tray", "Transparent disabled"); - arkPets.windowAlpha.reset(1f); - arkPets.hWndMine.setWindowTransparent(false); - popMenu.remove(optTransparentDis); - popMenu.add(optTransparentEn, 2); - }); + 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 -> { - Logger.info("Tray","Request to change stage"); - arkPets.changeStage(); - if (keepAnim != null) { - keepAnim = null; - popMenu.remove(optKeepAnimDis); - popMenu.add(optKeepAnimEn, 1); - } - }); - optExit.addActionListener(e -> { - Logger.info("Tray","Request to exit"); - arkPets.windowAlpha.reset(0f); - remove(); - try { - Thread.sleep((long)(linearEasingDuration * 1000)); - Gdx.app.exit(); - } catch (InterruptedException ignored) { - } + 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); + } - // Mouse event listener: - 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); - } - } - }); - - // Add the icon to the system tray. + @Override + protected void optExitHandler() { + Logger.info("Tray", "Request to exit"); + arkPets.windowAlpha.reset(0f); + removeTray(); try { - tray.add(icon); - isTrayIconApplied = true; - Logger.info("Tray", "Tray icon applied, titled \"" + title + "\""); - } catch (AWTException e) { - isTrayIconApplied = false; - Logger.error("Tray", "Unable to apply tray icon, details see below", e); + Thread.sleep((long) (linearEasingDuration * 1000)); + Gdx.app.exit(); + } catch (InterruptedException ignored) { } } - /** Removes the icon from system tray. - */ - public void remove() { - if (isTrayIconApplied) { - tray.remove(icon); - popMenu.removeAll(); - popWindow.dispose(); + @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); } - isTrayIconApplied = false; + } + + @Override + protected void optTransparentDisHandler() { + Logger.info("Tray", "Transparent disabled"); + arkPets.windowAlpha.reset(1f); + arkPets.hWndMine.setWindowTransparent(false); + popMenu.remove(optTransparentDis); + popMenu.add(optTransparentEn, 2); + } + + @Override + protected void optTransparentEnHandler() { + Logger.info("Tray", "Transparent enabled"); + 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"); + keepAnim = null; + popMenu.remove(optKeepAnimDis); + popMenu.add(optKeepAnimEn, 1); + } + + @Override + protected void optKeepAnimEnHandler() { + Logger.info("Tray", "Keep-Anim enabled"); + keepAnim = arkPets.cha.getPlaying(); + popMenu.remove(optKeepAnimEn); + popMenu.add(optKeepAnimDis, 1); + } + + @Override + public void removeTray() { + popMenu.removeAll(); + popWindow.dispose(); + socketClient.disconnect(); } /** Hides the menu. @@ -184,8 +183,8 @@ 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()); + int scaledX = (int) (x / at.getScaleX()); + int scaledY = (int) (y / at.getScaleY()); // Show the JDialog together with the JPopupMenu. popWindow.setVisible(true); diff --git a/core/src/cn/harryh/arkpets/process_pool/ProcessHolder.java b/core/src/cn/harryh/arkpets/process_pool/ProcessHolder.java new file mode 100644 index 00000000..1932f920 --- /dev/null +++ b/core/src/cn/harryh/arkpets/process_pool/ProcessHolder.java @@ -0,0 +1,28 @@ +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/ProcessPool.java b/core/src/cn/harryh/arkpets/process_pool/ProcessPool.java new file mode 100644 index 00000000..8b008e11 --- /dev/null +++ b/core/src/cn/harryh/arkpets/process_pool/ProcessPool.java @@ -0,0 +1,57 @@ +package cn.harryh.arkpets.process_pool; + +import java.io.File; +import java.util.*; +import java.util.concurrent.*; + +public class ProcessPool { + private final Set processHolderHashSet = new HashSet<>(); + private final java.util.concurrent.ExecutorService executorService; + + public ProcessPool() { + this.executorService = Executors.newFixedThreadPool(10); + } + + public void shutdown() { + processHolderHashSet.forEach(processHolder -> processHolder.getProcess().destroy()); + executorService.shutdown(); + } + + public Future submit(Runnable task) { + return executorService.submit(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"; + String classpath = System.getProperty("java.class.path"); + String className = clazz.getName(); + // Command preparation + List command = new ArrayList<>(); + command.add(javaBin); + if (!jvmArgs.isEmpty()) + command.addAll(jvmArgs); + command.add("-cp"); + command.add(classpath); + command.add(className); + if (!args.isEmpty()) + command.addAll(args); + // 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()); + }; + FutureTask futureTask = new FutureTask<>(task); + executorService.submit(futureTask); + return futureTask; + } +} diff --git a/core/src/cn/harryh/arkpets/process_pool/Status.java b/core/src/cn/harryh/arkpets/process_pool/Status.java new file mode 100644 index 00000000..96e068c5 --- /dev/null +++ b/core/src/cn/harryh/arkpets/process_pool/Status.java @@ -0,0 +1,6 @@ +package cn.harryh.arkpets.process_pool; + +public enum Status { + SUCCESS, + FAILURE +} diff --git a/core/src/cn/harryh/arkpets/process_pool/TaskStatus.java b/core/src/cn/harryh/arkpets/process_pool/TaskStatus.java new file mode 100644 index 00000000..7c20587d --- /dev/null +++ b/core/src/cn/harryh/arkpets/process_pool/TaskStatus.java @@ -0,0 +1,37 @@ +package cn.harryh.arkpets.process_pool; + +public class TaskStatus { + 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 new file mode 100644 index 00000000..a4b2f166 --- /dev/null +++ b/core/src/cn/harryh/arkpets/socket/InteriorSocketServer.java @@ -0,0 +1,101 @@ +package cn.harryh.arkpets.socket; + +import cn.harryh.arkpets.ArkConfig; +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.Objects; +import java.util.concurrent.*; + +public class InteriorSocketServer { + private final int port; + 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; + private static volatile boolean mainThreadExitFlag = false; + private static InteriorSocketServer instance = null; + public static InteriorSocketServer getInstance() { + if (instance == null) + instance = new InteriorSocketServer(); + return instance; + } + + private InteriorSocketServer() { + this.port = Objects.requireNonNull(ArkConfig.getConfig()).server_port; + } + + public synchronized void startServer() { + executorService.execute(() -> { + try { + serverSocket = new ServerSocket(port); + Logger.info("Socket", "Server is running on port " + port); + while (!mainThreadExitFlag) { + 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); + executorService.execute(clientTrayHandler); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + public synchronized void stopServer() { + mainThreadExitFlag = true; + clientSockets.forEach(socket -> { + try { + socket.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + if (!(Objects.requireNonNull(ArkConfig.getConfig()).separate_arkpet_from_launcher)) + clientHandlers.forEach(ClientTrayHandler::stopThread); + executorService.shutdown(); + } + + public void removeClientSocket(Socket socket) { + clientSockets.remove(socket); + } + + public void removeClientHandler(ClientTrayHandler handler) { + 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 new file mode 100644 index 00000000..a612bdef --- /dev/null +++ b/core/src/cn/harryh/arkpets/socket/SocketClient.java @@ -0,0 +1,82 @@ +package cn.harryh.arkpets.socket; + +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.function.Consumer; + +public class SocketClient { + private final String host; + private final int port; + private boolean connected = false; + private Socket socket; + private PrintWriter socketOut; + private BufferedReader socketIn; + private volatile boolean receiveThreadBreakFlag = false; + + public SocketClient(String host, int port) { + this.host = host; + this.port = port; + } + + public SocketClient(int port) { + this("localhost", port); + } + + public void connect(Consumer consumer) { + if (connected) { + return; + } + try { + socket = new Socket(host, port); + socketOut = new PrintWriter(socket.getOutputStream(), true); + socketIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); + Thread thread = new Thread(() -> { + while (!receiveThreadBreakFlag) { + try { + consumer.accept(JSONObject.parseObject(socketIn.readLine(), SocketData.class)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + thread.setDaemon(true); + thread.start(); + connected = true; + } catch (IOException e) { + Logger.error("Socket", "Error connecting to %s:%d".formatted(host, port)); + throw new RuntimeException(e); + } + } + + public void disconnect() { + if (!connected) { + return; + } + receiveThreadBreakFlag = true; + try { + 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 new file mode 100644 index 00000000..ce1b487d --- /dev/null +++ b/core/src/cn/harryh/arkpets/socket/SocketData.java @@ -0,0 +1,32 @@ +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 + } + + 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 new file mode 100644 index 00000000..5c0e01b6 --- /dev/null +++ b/core/src/cn/harryh/arkpets/tray/ClientTrayHandler.java @@ -0,0 +1,76 @@ +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.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; + } + + @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 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()); + tray.optExitHandler(); + InteriorSocketServer.getInstance().removeClientSocket(clientSocket); + InteriorSocketServer.getInstance().removeClientHandler(this); + } + } +} diff --git a/core/src/cn/harryh/arkpets/tray/SystemTrayManager.java b/core/src/cn/harryh/arkpets/tray/SystemTrayManager.java new file mode 100644 index 00000000..55b46702 --- /dev/null +++ b/core/src/cn/harryh/arkpets/tray/SystemTrayManager.java @@ -0,0 +1,168 @@ +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 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; + trayIcon.removeMouseListener(new MouseAdapter() { + }); + MouseListener mouseListener = new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + showStage(stage); + } + } + }; + x = stage.getX(); + y = stage.getY(); + trayIcon.addMouseListener(mouseListener); + } + + @Override + public void hide(Stage stage) { + if (!initialized) + return; + Platform.runLater(() -> { + if (SystemTray.isSupported()) { + x = stage.getX(); + y = stage.getY(); + stage.hide(); + return; + } + System.exit(0); + }); + } + + private void showStage(Stage stage) { + if (!initialized) + return; + Platform.runLater(() -> { + if (stage.isIconified()) { + stage.setIconified(false); + } + if (!stage.isShowing()) { + stage.setX(x); + stage.setY(y); + stage.show(); + } + stage.toFront(); + }); + } + + @Override + public void addTray(UUID uuid, Tray tray) { + arkPetTrays.put(uuid, tray); + } + + @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 new file mode 100644 index 00000000..5f99ea29 --- /dev/null +++ b/core/src/cn/harryh/arkpets/tray/Tray.java @@ -0,0 +1,41 @@ +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()); + } + + 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 new file mode 100644 index 00000000..a2b8ba12 --- /dev/null +++ b/core/src/cn/harryh/arkpets/tray/TrayInstance.java @@ -0,0 +1,104 @@ +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 new file mode 100644 index 00000000..b1bbe767 --- /dev/null +++ b/core/src/cn/harryh/arkpets/tray/TrayManager.java @@ -0,0 +1,91 @@ +package cn.harryh.arkpets.tray; + +import cn.harryh.arkpets.ArkTray; +import cn.harryh.arkpets.process_pool.ProcessPool; +import cn.harryh.arkpets.process_pool.TaskStatus; +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.List; +import java.util.*; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + +import static cn.harryh.arkpets.Const.fontFileRegular; + +public abstract class TrayManager { + protected volatile SystemTray tray; + protected volatile TrayIcon trayIcon; + protected final static ProcessPool processPool = new ProcessPool(); + 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(Stage stage); + + public abstract void addTray(UUID uuid, Tray tray); + + public abstract void removeTray(UUID uuid); + + public abstract Tray getTray(UUID uuid); + + public void shutdown() { + processPool.shutdown(); + } + + public SystemTray getTray() { + return tray; + } + + 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); + } + + public FutureTask submit(Class clazz, java.util.List jvmArgs, List args) { + return processPool.submit(clazz, jvmArgs, args); + } + + public Future submit(Runnable task) { + return processPool.submit(task); + } +} diff --git a/desktop/build.gradle b/desktop/build.gradle index 629338bc..ef42b9b1 100644 --- a/desktop/build.gradle +++ b/desktop/build.gradle @@ -8,6 +8,16 @@ eclipse.project.name = appName + "-desktop" import org.gradle.internal.os.OperatingSystem +processResources { + includeEmptyDirs = false + excludes = [ + "**/models_enemies/**", + "**/models/**", + "**/logs/**", + "models_data.json" + ] +} + // Runs the app without debug. task run(dependsOn: classes, type: JavaExec, group: 'execute') { mainClass = project.mainClassName diff --git a/desktop/src/cn/harryh/arkpets/ArkHomeFX.java b/desktop/src/cn/harryh/arkpets/ArkHomeFX.java index cbd27eec..67b5a286 100644 --- a/desktop/src/cn/harryh/arkpets/ArkHomeFX.java +++ b/desktop/src/cn/harryh/arkpets/ArkHomeFX.java @@ -8,6 +8,8 @@ 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.tray.SystemTrayManager; import cn.harryh.arkpets.utils.FXMLHelper; import cn.harryh.arkpets.utils.FXMLHelper.LoadFXMLResult; import cn.harryh.arkpets.utils.Logger; @@ -49,6 +51,9 @@ public void start(Stage stage) throws Exception { Font.loadFont(getClass().getResourceAsStream(fontFileRegular), Font.getDefault().getSize()); Font.loadFont(getClass().getResourceAsStream(fontFileBold), Font.getDefault().getSize()); + // Start Socket Server + InteriorSocketServer.getInstance().startServer(); + // Load FXML for root node. LoadFXMLResult fxml0 = FXMLHelper.loadFXML(getClass().getResource("/UI/RootModule.fxml")); fxml0.initializeWith(this); @@ -66,6 +71,16 @@ public void start(Stage stage) throws Exception { stage.setTitle(desktopTitle); rootModule.titleText.setText(desktopTitle); + SystemTrayManager.getInstance().listen(stage); + + stage.iconifiedProperty().addListener(((observable, oldValue, newValue) -> { + if (newValue) { + SystemTrayManager.getInstance().hide(stage); + } + })); + + stage.setOnCloseRequest(e -> SystemTrayManager.getInstance().hide(stage)); + // After the stage is shown, do initialize modules. stage.show(); rootModule.popSplashScreen(e -> { @@ -91,6 +106,14 @@ public void start(Stage stage) throws Exception { }, Duration.ZERO, durationFast); } + @Override + public void stop() throws Exception { + super.stop(); + InteriorSocketServer.getInstance().stopServer(); + if (!(Objects.requireNonNull(ArkConfig.getConfig()).separate_arkpet_from_launcher)) + SystemTrayManager.getInstance().shutdown(); + } + public boolean initModelsDataset(boolean popNotice) { return modelsModule.initModelsDataset(popNotice); } diff --git a/desktop/src/cn/harryh/arkpets/DesktopLauncher.java b/desktop/src/cn/harryh/arkpets/DesktopLauncher.java index 68b5a5ae..620483aa 100644 --- a/desktop/src/cn/harryh/arkpets/DesktopLauncher.java +++ b/desktop/src/cn/harryh/arkpets/DesktopLauncher.java @@ -3,21 +3,25 @@ */ package cn.harryh.arkpets; +import cn.harryh.arkpets.tray.SystemTrayManager; import cn.harryh.arkpets.utils.ArgPending; import cn.harryh.arkpets.utils.Logger; import javafx.application.Application; import java.nio.charset.Charset; import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; -import static cn.harryh.arkpets.Const.*; +import static cn.harryh.arkpets.Const.LogConfig; +import static cn.harryh.arkpets.Const.appVersion; /** The entrance of the whole program, also the bootstrap for ArkHomeFX. * @see ArkHomeFX */ public class DesktopLauncher { - public static void main (String[] args) { + public static void main(String[] args) { ArgPending.argCache = args; // Logger Logger.initialize(LogConfig.logDesktopPath, LogConfig.logDesktopMaxKeep); @@ -58,7 +62,12 @@ protected void process(String command, String addition) { }; // Java FX bootstrap - Application.launch(ArkHomeFX.class, args); + Future future = SystemTrayManager.getInstance().submit(() -> Application.launch(ArkHomeFX.class, args)); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } Logger.info("System", "Exited from DesktopLauncher successfully"); System.exit(0); } diff --git a/desktop/src/cn/harryh/arkpets/controllers/RootModule.java b/desktop/src/cn/harryh/arkpets/controllers/RootModule.java index 01192488..a3be23ed 100644 --- a/desktop/src/cn/harryh/arkpets/controllers/RootModule.java +++ b/desktop/src/cn/harryh/arkpets/controllers/RootModule.java @@ -8,6 +8,9 @@ import cn.harryh.arkpets.EmbeddedLauncher; import cn.harryh.arkpets.guitasks.CheckAppUpdateTask; import cn.harryh.arkpets.guitasks.GuiTask; +import cn.harryh.arkpets.process_pool.Status; +import cn.harryh.arkpets.process_pool.TaskStatus; +import cn.harryh.arkpets.tray.SystemTrayManager; import cn.harryh.arkpets.utils.ArgPending; import cn.harryh.arkpets.utils.GuiPrefabs; import cn.harryh.arkpets.utils.JavaProcess; @@ -27,8 +30,9 @@ import javafx.scene.text.Text; import javafx.util.Duration; -import java.io.IOException; import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; import static cn.harryh.arkpets.Const.*; import static cn.harryh.arkpets.utils.GuiPrefabs.DialogUtil; @@ -125,7 +129,7 @@ public void popLoading(EventHandler handler) { public void startArkPetsCore() { Task task = new Task<>() { @Override - protected Boolean call() throws IOException, InterruptedException { + protected Boolean call() throws InterruptedException, ExecutionException { // Update the logging level arg to match the custom value of the Launcher. ArrayList args = new ArrayList<>(Arrays.asList(ArgPending.argCache.clone())); args.remove(LogConfig.errorArg); @@ -134,8 +138,8 @@ protected Boolean call() throws IOException, InterruptedException { args.remove(LogConfig.debugArg); String temp = switch (app.config.logging_level) { case LogConfig.error -> LogConfig.errorArg; - case LogConfig.warn -> LogConfig.warnArg; - case LogConfig.info -> LogConfig.infoArg; + case LogConfig.warn -> LogConfig.warnArg; + case LogConfig.info -> LogConfig.infoArg; case LogConfig.debug -> LogConfig.debugArg; default -> ""; }; @@ -143,14 +147,11 @@ protected Boolean call() throws IOException, InterruptedException { // Start ArkPets core. Logger.info("Launcher", "Launching " + app.config.character_asset); Logger.debug("Launcher", "With args " + args); - int code = JavaProcess.exec( - EmbeddedLauncher.class, true, - List.of(), args - ); + FutureTask future = SystemTrayManager.getInstance().submit(EmbeddedLauncher.class, List.of(), args); // ArkPets core finalized. - if (code != 0) { - Logger.warn("Launcher", "Detected an abnormal finalization of an ArkPets thread (exit code " + code + "). Please check the log file for details."); - lastLaunchFailed = new JavaProcess.UnexpectedExitCodeException(code); + if (Objects.equals(future.get().getStatus(), Status.FAILURE)) { + 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; } Logger.debug("Launcher", "Detected a successful finalization of an ArkPets thread.");