From 926270eb8f497123b1d09ab1bf42d56bd86702e4 Mon Sep 17 00:00:00 2001 From: Tyler McGurrin Date: Fri, 20 Feb 2026 19:36:29 -0500 Subject: [PATCH 1/3] me when i forget to commit my stuff. --- .gitignore | 6 +- .idea/misc.xml | 2 +- client/build.gradle.kts | 20 ++- client/src/main/java/net/xircon/Main.java | 17 --- .../java/net/xircon/xenon/client/Xenon.java | 143 ++++++++++++++++++ .../java/net/xircon/xenon/client/io/Log.java | 25 +++ .../java/net/xircon/xenon/client/io/Util.java | 58 +++++++ .../xenon/client/networking/Networking.java | 61 ++++++++ .../networking/textclient/TextClient.java | 15 ++ client/src/main/resources/client.properties | 5 + client/src/main/resources/log4j2.properties | 12 ++ server/build.gradle.kts | 19 ++- server/src/main/java/net/xircon/Main.java | 11 -- .../java/net/xircon/xenon/server/Xenon.java | 56 +++++++ .../java/net/xircon/xenon/server/io/Log.java | 25 +++ .../java/net/xircon/xenon/server/io/Util.java | 57 +++++++ .../xenon/server/networking/Networking.java | 21 +++ .../networking/textserver/ClientHandler.java | 32 ++++ .../networking/textserver/TextServer.java | 31 ++++ server/src/main/resources/server.properties | 4 + 20 files changed, 584 insertions(+), 36 deletions(-) delete mode 100644 client/src/main/java/net/xircon/Main.java create mode 100644 client/src/main/java/net/xircon/xenon/client/Xenon.java create mode 100644 client/src/main/java/net/xircon/xenon/client/io/Log.java create mode 100644 client/src/main/java/net/xircon/xenon/client/io/Util.java create mode 100644 client/src/main/java/net/xircon/xenon/client/networking/Networking.java create mode 100644 client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java create mode 100644 client/src/main/resources/client.properties create mode 100644 client/src/main/resources/log4j2.properties delete mode 100644 server/src/main/java/net/xircon/Main.java create mode 100644 server/src/main/java/net/xircon/xenon/server/Xenon.java create mode 100644 server/src/main/java/net/xircon/xenon/server/io/Log.java create mode 100644 server/src/main/java/net/xircon/xenon/server/io/Util.java create mode 100644 server/src/main/java/net/xircon/xenon/server/networking/Networking.java create mode 100644 server/src/main/java/net/xircon/xenon/server/networking/textserver/ClientHandler.java create mode 100644 server/src/main/java/net/xircon/xenon/server/networking/textserver/TextServer.java diff --git a/.gitignore b/.gitignore index 1fac4d5..0890712 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,8 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +## Logs ## +/server/logs/ +/client/logs/ diff --git a/.idea/misc.xml b/.idea/misc.xml index 5cd9a10..5b99cf8 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/client/build.gradle.kts b/client/build.gradle.kts index de765f9..0b45225 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -1,18 +1,32 @@ plugins { id("java") + application } group = "net.xircon" version = "1.0-SNAPSHOT" +val log4jVersion = "2.25.1" + repositories { mavenCentral() } dependencies { - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") + implementation (platform("org.apache.logging.log4j:log4j-bom:$log4jVersion")) + implementation("org.apache.logging.log4j:log4j-core:$log4jVersion") + implementation("org.apache.logging.log4j:log4j-api:$log4jVersion") + implementation("com.formdev:flatlaf:3.7") +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +application { + mainClass = "net.xircon.xenon.client.Xenon" } tasks.test { diff --git a/client/src/main/java/net/xircon/Main.java b/client/src/main/java/net/xircon/Main.java deleted file mode 100644 index aeaa351..0000000 --- a/client/src/main/java/net/xircon/Main.java +++ /dev/null @@ -1,17 +0,0 @@ -package net.xircon; - -//TIP To Run code, press or -// click the icon in the gutter. -public class Main { - public static void main(String[] args) { - //TIP Press with your caret at the highlighted text - // to see how IntelliJ IDEA suggests fixing it. - System.out.printf("Hello and welcome!"); - - for (int i = 1; i <= 5; i++) { - //TIP Press to start debugging your code. We have set one breakpoint - // for you, but you can always add more by pressing . - System.out.println("i = " + i); - } - } -} \ No newline at end of file diff --git a/client/src/main/java/net/xircon/xenon/client/Xenon.java b/client/src/main/java/net/xircon/xenon/client/Xenon.java new file mode 100644 index 0000000..cefe0cd --- /dev/null +++ b/client/src/main/java/net/xircon/xenon/client/Xenon.java @@ -0,0 +1,143 @@ +package net.xircon.xenon.client; + + +import com.formdev.flatlaf.FlatDarkLaf; +import net.xircon.xenon.client.io.Log; +import net.xircon.xenon.client.io.Util; +import net.xircon.xenon.client.networking.Networking; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.net.Socket; +import java.util.Objects; + +public class Xenon implements Runnable { + private Thread mainThread; + public static void main(String[] args) {new Xenon().start();} + private static Socket mainSocket; + + static final String propertiesFile = "client.properties"; + // Properties + public static int PortVoIP = 49190; + public static int PortText = 49180; + public static int Width = 800; + public static int Height = 600; + + private static String nickname = "Test User"; + + public void start() { + mainThread = new Thread(this, "mainThread"); + mainThread.start(); + } + + @Override + public void run() { + Log.info("Starting Xenon Client!"); + // Get Property Values + PortVoIP = Objects.requireNonNull(Util.GetProperties(propertiesFile, "PortVoIP", Util.PROPERTY_TYPE_INT)).intValue; + PortText = Objects.requireNonNull(Util.GetProperties(propertiesFile, "PortText", Util.PROPERTY_TYPE_INT)).intValue; + Width = Objects.requireNonNull(Util.GetProperties(propertiesFile, "Width", Util.PROPERTY_TYPE_INT)).intValue; + Height = Objects.requireNonNull(Util.GetProperties(propertiesFile, "Height", Util.PROPERTY_TYPE_INT)).intValue; + + // Start Java Swing GUI + JFrame mainFrame = new JFrame("Xenon Client"); + mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + mainFrame.setSize(Width, Height); + try { + UIManager.setLookAndFeel( new FlatDarkLaf()); + } catch (UnsupportedLookAndFeelException e) { + Log.err("Java Swing GUI Look and Feel Did Not Correctly Apply! Falling Back to Default!"); + } + + // Creating the MenuBar and adding components + JMenuBar mb = new JMenuBar(); + // Menu 1 + JMenu m1 = new JMenu("File"); + JMenuItem m11 = new JMenuItem("Open"); + JMenuItem m12 = new JMenuItem("Save as"); + mb.add(m1); + m1.add(m11); + m1.add(m12); + // Menu 2 + JMenu m2 = new JMenu("Connections"); + JMenuItem m21 = new JMenuItem(new AbstractAction("Connect To Server") { + @Override + public void actionPerformed(ActionEvent e) { + String IPAddress = JOptionPane.showInputDialog(null, "IP Address Of Server", "127.0.0.1"); + if (IPAddress == null) {return;} + else { + mainSocket = Networking.connectToServer(IPAddress, PortText); + } + } + }); + JMenuItem m22 = new JMenuItem(new AbstractAction("Disconnect From Server") { + @Override + public void actionPerformed(ActionEvent e) { + Networking.disconnectFromServer(mainSocket); + } + }); + JMenuItem m23 = new JMenuItem(new AbstractAction("Change Nickname") { + @Override + public void actionPerformed(ActionEvent e) { + String Nick = JOptionPane.showInputDialog(null, "New Nickname", nickname); + if (Nick == null) {return;} + else { + nickname = Nick; + } + } + }); + mb.add(m2); + m2.add(m21); + m2.add(m22); + m2.add(m23); + // Menu 3 + JMenu m3 = new JMenu("Help"); + JMenuItem m31 = new JMenuItem(new AbstractAction("About") { + @Override + public void actionPerformed(ActionEvent actionEvent) { + JOptionPane.showMessageDialog(null, "Xenon Created By Xircon\nLicenced under the GNU GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.en.html)", + "About Xenon", JOptionPane.INFORMATION_MESSAGE); + } + }); + mb.add(m3); + m3.add(m31); + + + // Creating the panel at bottom and adding components + JPanel panel = new JPanel(); // the panel is not visible in output + panel.setLayout(new BorderLayout()); + JTextField sendField = new JTextField(); // accepts up to 255 characters + sendField.setToolTipText("Enter Message"); + sendField.setPreferredSize(new Dimension(200 ,20 )); + JButton send = new JButton("Send"); + JButton upload = new JButton("Upload"); + panel.add(sendField, BorderLayout.CENTER); + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new BorderLayout()); + buttonPanel.add(send, BorderLayout.WEST); + buttonPanel.add(upload, BorderLayout.EAST); + panel.add(buttonPanel, BorderLayout.EAST); + + // Text Sending Handler + send.addActionListener(e -> { + if (mainSocket == null) {JOptionPane.showMessageDialog(null, "Ensure you are Connected to a Server!", "Error", JOptionPane.ERROR_MESSAGE);} + else { + Log.info("Sending " + sendField.getText()); + Networking.sendMessage(sendField.getText(), nickname, mainSocket); + SwingUtilities.invokeLater(() -> sendField.setText("")); + } + }); + + // Text Area at the Center + JTextArea ta = new JTextArea(); + + //Adding Components to the Frame. + mainFrame.getContentPane().add(BorderLayout.SOUTH, panel); + mainFrame.getContentPane().add(BorderLayout.NORTH, mb); + mainFrame.getContentPane().add(BorderLayout.CENTER, ta); + // Set GUI To Visible + mainFrame.setVisible(true); + } +} \ No newline at end of file diff --git a/client/src/main/java/net/xircon/xenon/client/io/Log.java b/client/src/main/java/net/xircon/xenon/client/io/Log.java new file mode 100644 index 0000000..d64d4e7 --- /dev/null +++ b/client/src/main/java/net/xircon/xenon/client/io/Log.java @@ -0,0 +1,25 @@ +package net.xircon.xenon.client.io; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.swing.*; + +public class Log { + public static final Logger logger = LogManager.getLogger(); + + public static void fatal(String message) { + logger.fatal(message); + JOptionPane.showMessageDialog(null, message, "Fatal Error", JOptionPane.ERROR_MESSAGE); + throw new RuntimeException(message); + } + public static void err(String message) { + logger.error(message); + } + public static void warn(String message) { + logger.warn(message); + } + public static void info(String message) { + logger.info(message); + } +} diff --git a/client/src/main/java/net/xircon/xenon/client/io/Util.java b/client/src/main/java/net/xircon/xenon/client/io/Util.java new file mode 100644 index 0000000..61603a5 --- /dev/null +++ b/client/src/main/java/net/xircon/xenon/client/io/Util.java @@ -0,0 +1,58 @@ +package net.xircon.xenon.client.io; + +import javax.annotation.Nullable; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.Properties; + +public class Util { + public static class Property { + public boolean boolValue = false; + public int intValue = 0; + public String stringValue = ""; + } + + public static final int PROPERTY_TYPE_BOOLEAN = 1; + public static final int PROPERTY_TYPE_INT = 2; + public static final int PROPERTY_TYPE_STRING = 3; + + @Nullable + public static Property GetProperties(String propertiesFile, String propertyName, int propertyType) { + Properties properties = new Properties(); + Property property = new Property(); + if (false); // TODO: Add Checker for file later, and make fallback to resources folder + else try (InputStream propertiesinput = Util.class.getClassLoader().getResourceAsStream(propertiesFile)) { + properties.load(propertiesinput); + if (properties.containsKey(propertyName) && propertyType == PROPERTY_TYPE_BOOLEAN) { // Booleans + String prop = properties.getProperty(propertyName); + property.boolValue = prop.equals("true"); + return property; + } + if (properties.containsKey(propertyName) && propertyType == PROPERTY_TYPE_INT) { // Ints + int prop = Integer.parseInt(properties.getProperty(propertyName)); + if (prop >=0 ) property.intValue=prop; + return property; + } + if (properties.containsKey(propertyName) && propertyType == PROPERTY_TYPE_STRING) { // Strings + String prop = properties.getProperty(propertyName); + if (prop != null ) property.stringValue=prop; + return property; + } + } catch (IOException e) { + Log.warn("[Property Loader] Failed to Read server.properties! Falling Back to Defaults! {" + e.getMessage() + "}"); + } + Log.warn("[Property Loader] Unable to Find Property/Invalid Type!"); + return null; + } + public static String GetExternalIP() throws IOException { + URL whatismyip = new URL("http://checkip.amazonaws.com"); + BufferedReader in = new BufferedReader(new InputStreamReader( + whatismyip.openStream())); + + // Return IP As String + return in.readLine(); + } +} diff --git a/client/src/main/java/net/xircon/xenon/client/networking/Networking.java b/client/src/main/java/net/xircon/xenon/client/networking/Networking.java new file mode 100644 index 0000000..324448b --- /dev/null +++ b/client/src/main/java/net/xircon/xenon/client/networking/Networking.java @@ -0,0 +1,61 @@ +package net.xircon.xenon.client.networking; + +import net.xircon.xenon.client.io.Log; + +import javax.annotation.Nullable; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketTimeoutException; + +public class Networking { + private static final String nameTerminator = "\uffff"; + + @Nullable + public static Socket connectToServer(String Address, int Port) { + try { + Log.info("Connecting To Server " + Address + ":" + Port); + Socket socket = new Socket(); + SocketAddress address = new InetSocketAddress(Address, Port); + socket.connect(address, 5000); + Log.info("Connected to Server!"); + socket.setKeepAlive(true); + return socket; + } catch (SocketTimeoutException e) { + Log.warn("Connection timed out!"); + return null; + } catch (Exception e) { + Log.err("Failed to Connect to Server!"); + return null; + } + } + + public static void disconnectFromServer(Socket socket) { + try { + Log.info("Disconnecting from Server"); + socket.close(); + } catch (IOException e) { + Log.err("Failed to Close Socket!"); + } catch (NullPointerException e) { + Log.warn("Ignoring Disconnect Request, Null Socket!"); + } + } + + public static String sendMessage(String message, String nickname, Socket socket) { + try { + BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); + PrintWriter output = new PrintWriter(socket.getOutputStream(), true); + output.println(nickname + nameTerminator + message); + String messageFromServer = input.readLine(); + Log.info("Server Message: " + messageFromServer); + return messageFromServer; + } catch (IOException e) { + Log.warn("Could not Read/Send Message!"); + return null; + } + } +} diff --git a/client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java b/client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java new file mode 100644 index 0000000..37de8c0 --- /dev/null +++ b/client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java @@ -0,0 +1,15 @@ +package net.xircon.xenon.client.networking.textclient; + +import java.net.Socket; + +public class TextClient implements Runnable { + + public TextClient(Socket serverSocket) { + + } + + @Override + public void run() { + + } +} diff --git a/client/src/main/resources/client.properties b/client/src/main/resources/client.properties new file mode 100644 index 0000000..4068efe --- /dev/null +++ b/client/src/main/resources/client.properties @@ -0,0 +1,5 @@ +# Client Properties +PortText=49180 +PortVoIP=49190 +Width=960 +Height=600 \ No newline at end of file diff --git a/client/src/main/resources/log4j2.properties b/client/src/main/resources/log4j2.properties new file mode 100644 index 0000000..485380b --- /dev/null +++ b/client/src/main/resources/log4j2.properties @@ -0,0 +1,12 @@ +appender.console.type=Console +appender.console.name=STDOUT +appender.console.layout.type=PatternLayout +appender.console.layout.pattern=[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%t] %msg%n +appender.file.type=File +appender.file.name=LOGFILE +appender.file.fileName=logs/technitium.log +appender.file.layout.type=PatternLayout +appender.file.layout.pattern=[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%t] %msg%n +appender.file.filter.threshold.type = ThresholdFilter +appender.file.filter.threshold.level = info +rootLogger=debug, STDOUT, LOGFILE \ No newline at end of file diff --git a/server/build.gradle.kts b/server/build.gradle.kts index de765f9..53cfc8f 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -1,18 +1,31 @@ plugins { id("java") + application } group = "net.xircon" version = "1.0-SNAPSHOT" +val log4jVersion = "2.25.1" + repositories { mavenCentral() } dependencies { - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") + implementation (platform("org.apache.logging.log4j:log4j-bom:$log4jVersion")) + implementation("org.apache.logging.log4j:log4j-core:$log4jVersion") + implementation("org.apache.logging.log4j:log4j-api:$log4jVersion") +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +application { + mainClass = "net.xircon.xenon.server.Xenon" } tasks.test { diff --git a/server/src/main/java/net/xircon/Main.java b/server/src/main/java/net/xircon/Main.java deleted file mode 100644 index 80eff76..0000000 --- a/server/src/main/java/net/xircon/Main.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.xircon; - -public class Main { - public static void main(String[] args) { - System.out.println() - - for (int i = 1; i <= 5; i++) { - System.out.println("i = " + i); - } - } -} diff --git a/server/src/main/java/net/xircon/xenon/server/Xenon.java b/server/src/main/java/net/xircon/xenon/server/Xenon.java new file mode 100644 index 0000000..09a1189 --- /dev/null +++ b/server/src/main/java/net/xircon/xenon/server/Xenon.java @@ -0,0 +1,56 @@ +package net.xircon.xenon.server; + +import net.xircon.xenon.server.io.*; +import net.xircon.xenon.server.networking.Networking; +import net.xircon.xenon.server.networking.textserver.TextServer; + +import java.io.IOException; +import java.net.*; +import java.util.Objects; + +//import java.net. + +public class Xenon implements Runnable { + private Thread serverThread; + public static void main(String[] args) {new Xenon().start();} + + private ServerSocket textServerSocket; + + private static final String propertiesFile = "server.properties"; + // Sever Properties + public static String ServerName = "Xenon Test Server"; + public static int PortVoIP = 49190; + public static int PortText = 49180; + + public void start() { + serverThread = new Thread(this, "serverThread"); + serverThread.start(); + } + + @Override + public void run() { + Log.info("Starting Xenon Server!"); + + // Get Property Values + PortVoIP = Objects.requireNonNull(Util.GetProperties(propertiesFile, "PortVoIP", Util.PROPERTY_TYPE_INT)).intValue; + PortText = Objects.requireNonNull(Util.GetProperties(propertiesFile, "PortText", Util.PROPERTY_TYPE_INT)).intValue; + ServerName = Objects.requireNonNull(Util.GetProperties(propertiesFile, "ServerName", Util.PROPERTY_TYPE_STRING)).stringValue; + + Log.info("Server is on Port " + PortVoIP + " For VoIP and Port " + PortText + " For Text."); + // Print External IP and Handle IOException + try { + Log.info("Server's External IP is " + Util.GetExternalIP()); + } catch (IOException e) { + Log.err("IOException Whilst Fetching External IP! {" + e + "}"); + } + Log.info("Server Name is " + '"' + ServerName + '"'); + textServerSocket = Networking.createTextServer(PortText); + TextServer textServer = new TextServer(textServerSocket); + Thread textServerThread = new Thread(textServer); + textServerThread.start(); + } + + + + +} diff --git a/server/src/main/java/net/xircon/xenon/server/io/Log.java b/server/src/main/java/net/xircon/xenon/server/io/Log.java new file mode 100644 index 0000000..98d5b7e --- /dev/null +++ b/server/src/main/java/net/xircon/xenon/server/io/Log.java @@ -0,0 +1,25 @@ +package net.xircon.xenon.server.io; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.swing.*; + +public class Log { + public static final Logger logger = LogManager.getLogger(); + + public static void fatal(String message) { + logger.fatal(message); + JOptionPane.showMessageDialog(null, message, "Fatal Error", JOptionPane.ERROR_MESSAGE); + throw new RuntimeException(message); + } + public static void err(String message) { + logger.error(message); + } + public static void warn(String message) { + logger.warn(message); + } + public static void info(String message) { + logger.info(message); + } +} diff --git a/server/src/main/java/net/xircon/xenon/server/io/Util.java b/server/src/main/java/net/xircon/xenon/server/io/Util.java new file mode 100644 index 0000000..d74a249 --- /dev/null +++ b/server/src/main/java/net/xircon/xenon/server/io/Util.java @@ -0,0 +1,57 @@ +package net.xircon.xenon.server.io; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import java.net.*; +import java.io.*; + +public class Util { + public static class Property { + public boolean boolValue = false; + public int intValue = 0; + public String stringValue = ""; + } + + public static final int PROPERTY_TYPE_BOOLEAN = 1; + public static final int PROPERTY_TYPE_INT = 2; + public static final int PROPERTY_TYPE_STRING = 3; + + @Nullable + public static Property GetProperties(String propertiesFile, String propertyName, int propertyType) { + Properties properties = new Properties(); + Property property = new Property(); + if (false); // TODO: Add Checker for file later, and make fallback to resources folder + else try (InputStream propertiesinput = Util.class.getClassLoader().getResourceAsStream(propertiesFile)) { + properties.load(propertiesinput); + if (properties.containsKey(propertyName) && propertyType == PROPERTY_TYPE_BOOLEAN) { // Booleans + String prop = properties.getProperty(propertyName); + property.boolValue = prop.equals("true"); + return property; + } + if (properties.containsKey(propertyName) && propertyType == PROPERTY_TYPE_INT) { // Ints + int prop = Integer.parseInt(properties.getProperty(propertyName)); + if (prop >=0 ) property.intValue=prop; + return property; + } + if (properties.containsKey(propertyName) && propertyType == PROPERTY_TYPE_STRING) { // Strings + String prop = properties.getProperty(propertyName); + if (prop != null ) property.stringValue=prop; + return property; + } + } catch (IOException e) { + Log.warn("[Property Loader] Failed to Read server.properties! Falling Back to Defaults! {" + e.getMessage() + "}"); + } + Log.warn("[Property Loader] Unable to Find Property/Invalid Type!"); + return null; + } + public static String GetExternalIP() throws IOException { + URL whatismyip = new URL("http://checkip.amazonaws.com"); + BufferedReader in = new BufferedReader(new InputStreamReader( + whatismyip.openStream())); + + // Return IP As String + return in.readLine(); + } +} diff --git a/server/src/main/java/net/xircon/xenon/server/networking/Networking.java b/server/src/main/java/net/xircon/xenon/server/networking/Networking.java new file mode 100644 index 0000000..1e50d1b --- /dev/null +++ b/server/src/main/java/net/xircon/xenon/server/networking/Networking.java @@ -0,0 +1,21 @@ +package net.xircon.xenon.server.networking; + +import net.xircon.xenon.server.io.Log; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.net.ServerSocket; + +public class Networking { + @Nullable + public static ServerSocket createTextServer(int Port) { + try { + ServerSocket serverSocket = new ServerSocket(Port); + Log.info("Text Server Waiting For Clients"); + return serverSocket; + } catch (IOException e) { + Log.info("Failed to Create Text Server!"); + return null; + } + } +} diff --git a/server/src/main/java/net/xircon/xenon/server/networking/textserver/ClientHandler.java b/server/src/main/java/net/xircon/xenon/server/networking/textserver/ClientHandler.java new file mode 100644 index 0000000..ed69636 --- /dev/null +++ b/server/src/main/java/net/xircon/xenon/server/networking/textserver/ClientHandler.java @@ -0,0 +1,32 @@ +package net.xircon.xenon.server.networking.textserver; + +import net.xircon.xenon.server.io.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Socket; + +public class ClientHandler implements Runnable { + private Socket clientSocket; + + public ClientHandler(Socket clientSocket) { + this.clientSocket = clientSocket; + } + + @Override + public void run() { + try { + BufferedReader input = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); + PrintWriter output = output = new PrintWriter(clientSocket.getOutputStream(), true); + String message = input.readLine(); + String namedMessage[] = message.split("\uffff", -1); + Log.info("<" + namedMessage[0] + "> " + namedMessage[1]); + output.println("Server Received Message!"); + clientSocket.setKeepAlive(true); + } catch (IOException e) { + Log.warn("Could not Read Message From Client"); + } + } +} diff --git a/server/src/main/java/net/xircon/xenon/server/networking/textserver/TextServer.java b/server/src/main/java/net/xircon/xenon/server/networking/textserver/TextServer.java new file mode 100644 index 0000000..326147a --- /dev/null +++ b/server/src/main/java/net/xircon/xenon/server/networking/textserver/TextServer.java @@ -0,0 +1,31 @@ +package net.xircon.xenon.server.networking.textserver; + +import net.xircon.xenon.server.io.Log; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +public class TextServer implements Runnable { + private ServerSocket mainServerSocket; + + public TextServer(ServerSocket serverSocket) { + this.mainServerSocket = serverSocket; + } + + @Override + public void run() { + while (true) { + try { + Socket clientSocket = mainServerSocket.accept(); + Log.info("Client Connected: " + clientSocket); + ClientHandler clientHandler = new ClientHandler(clientSocket); + Thread thread = new Thread(clientHandler); + thread.start(); + } catch (IOException e) { + Log.err("Text Server Did not correctly Accept a Client!"); + } + + } + } +} diff --git a/server/src/main/resources/server.properties b/server/src/main/resources/server.properties index e69de29..b61e188 100644 --- a/server/src/main/resources/server.properties +++ b/server/src/main/resources/server.properties @@ -0,0 +1,4 @@ +# Server.properties +PortText=49180 +PortVoIP=49190 +ServerName=Xircon's Test Server \ No newline at end of file From db7579ca491a6f7c1f6e48a872fa7a783820fb8c Mon Sep 17 00:00:00 2001 From: Tyler McGurrin Date: Sat, 21 Feb 2026 00:00:21 -0500 Subject: [PATCH 2/3] Greatly Overhaul GUI and get server in a usable state --- .../java/net/xircon/xenon/client/Xenon.java | 183 +++++++++++++++--- .../xenon/client/io/CustomOutputStream.java | 23 +++ .../java/net/xircon/xenon/client/io/Log.java | 2 + .../java/net/xircon/xenon/client/io/Util.java | 18 +- .../xenon/client/networking/Networking.java | 16 +- .../networking/textclient/TextClient.java | 24 ++- client/src/main/resources/xenon.jpg | Bin 0 -> 11591 bytes client/src/main/resources/xenon.png | Bin 0 -> 23693 bytes 8 files changed, 219 insertions(+), 47 deletions(-) create mode 100644 client/src/main/java/net/xircon/xenon/client/io/CustomOutputStream.java create mode 100644 client/src/main/resources/xenon.jpg create mode 100644 client/src/main/resources/xenon.png diff --git a/client/src/main/java/net/xircon/xenon/client/Xenon.java b/client/src/main/java/net/xircon/xenon/client/Xenon.java index cefe0cd..dd4c678 100644 --- a/client/src/main/java/net/xircon/xenon/client/Xenon.java +++ b/client/src/main/java/net/xircon/xenon/client/Xenon.java @@ -2,21 +2,25 @@ package net.xircon.xenon.client; import com.formdev.flatlaf.FlatDarkLaf; +import net.xircon.xenon.client.io.CustomOutputStream; import net.xircon.xenon.client.io.Log; import net.xircon.xenon.client.io.Util; import net.xircon.xenon.client.networking.Networking; +import net.xircon.xenon.client.networking.textclient.TextClient; +import org.jspecify.annotations.NonNull; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; +import java.io.PrintStream; import java.net.Socket; import java.util.Objects; public class Xenon implements Runnable { - private Thread mainThread; public static void main(String[] args) {new Xenon().start();} private static Socket mainSocket; + public Color buttonColour = new Color(55, 90, 129); + public Image xenonIcon; static final String propertiesFile = "client.properties"; // Properties @@ -28,12 +32,39 @@ public class Xenon implements Runnable { private static String nickname = "Test User"; public void start() { - mainThread = new Thread(this, "mainThread"); + Thread mainThread = new Thread(this, "mainThread"); mainThread.start(); } @Override public void run() { + // Load Icon + xenonIcon = Util.loadIcon("xenon.png"); + + // Set Swing LaF before all Swing Stuff gets init + try { + UIManager.setLookAndFeel( new FlatDarkLaf()); + } catch (UnsupportedLookAndFeelException e) { + Log.err("Java Swing GUI Look and Feel Did Not Correctly Apply! Falling Back to Default!"); + } + // Start Swing Logger ASAP + // Disable this If the entire thing dies :) + JFrame logFrame = new JFrame("Xenon Log"); + logFrame.setIconImage(xenonIcon); + // logFrame.setLayout(new BorderLayout()); + logFrame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); + logFrame.setSize(640,480); + JTextArea logText = new JTextArea(); + logText.setAutoscrolls(true); + logText.setLineWrap(true); + logText.setEditable(false); + JScrollPane logScrollPane = new JScrollPane(logText); + logFrame.add(logScrollPane); + PrintStream systemOutput = new PrintStream(new CustomOutputStream(logText)); + System.setOut(systemOutput); + System.setErr(systemOutput); + + Log.info("Starting Xenon Client!"); // Get Property Values PortVoIP = Objects.requireNonNull(Util.GetProperties(propertiesFile, "PortVoIP", Util.PROPERTY_TYPE_INT)).intValue; @@ -43,21 +74,17 @@ public class Xenon implements Runnable { // Start Java Swing GUI JFrame mainFrame = new JFrame("Xenon Client"); + mainFrame.setIconImage(xenonIcon); mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); mainFrame.setSize(Width, Height); - try { - UIManager.setLookAndFeel( new FlatDarkLaf()); - } catch (UnsupportedLookAndFeelException e) { - Log.err("Java Swing GUI Look and Feel Did Not Correctly Apply! Falling Back to Default!"); - } // Creating the MenuBar and adding components - JMenuBar mb = new JMenuBar(); + JMenuBar menubar = new JMenuBar(); // Menu 1 JMenu m1 = new JMenu("File"); JMenuItem m11 = new JMenuItem("Open"); JMenuItem m12 = new JMenuItem("Save as"); - mb.add(m1); + menubar.add(m1); m1.add(m11); m1.add(m12); // Menu 2 @@ -66,8 +93,7 @@ public class Xenon implements Runnable { @Override public void actionPerformed(ActionEvent e) { String IPAddress = JOptionPane.showInputDialog(null, "IP Address Of Server", "127.0.0.1"); - if (IPAddress == null) {return;} - else { + if (IPAddress != null) { mainSocket = Networking.connectToServer(IPAddress, PortText); } } @@ -82,27 +108,53 @@ public class Xenon implements Runnable { @Override public void actionPerformed(ActionEvent e) { String Nick = JOptionPane.showInputDialog(null, "New Nickname", nickname); - if (Nick == null) {return;} - else { + if (Nick != null) { nickname = Nick; } } }); - mb.add(m2); + menubar.add(m2); m2.add(m21); m2.add(m22); m2.add(m23); + // Menu 3 - JMenu m3 = new JMenu("Help"); - JMenuItem m31 = new JMenuItem(new AbstractAction("About") { + JMenu m3 = new JMenu("Edit"); + // Preferences Menu + JFrame preferences = preferencesJFrame(); + JMenuItem m31 = new JMenuItem(new AbstractAction("Preferences") { + @Override + public void actionPerformed(ActionEvent e) { + preferences.setVisible(true); + } + }); + + + menubar.add(m3); + m3.add(m31); + + //Menu 4 + JMenu m4 = new JMenu("View"); + JMenuItem m41 = new JMenuItem(new AbstractAction("Log") { + @Override + public void actionPerformed(ActionEvent e) { + logFrame.setVisible(true); + } + }); + menubar.add(m4); + m4.add(m41); + + // Menu 5 + JMenu m5 = new JMenu("Help"); + JMenuItem m51 = new JMenuItem(new AbstractAction("About") { @Override public void actionPerformed(ActionEvent actionEvent) { JOptionPane.showMessageDialog(null, "Xenon Created By Xircon\nLicenced under the GNU GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.en.html)", "About Xenon", JOptionPane.INFORMATION_MESSAGE); } }); - mb.add(m3); - m3.add(m31); + menubar.add(m5); + m5.add(m51); // Creating the panel at bottom and adding components @@ -112,7 +164,9 @@ public class Xenon implements Runnable { sendField.setToolTipText("Enter Message"); sendField.setPreferredSize(new Dimension(200 ,20 )); JButton send = new JButton("Send"); + send.setBackground(buttonColour); JButton upload = new JButton("Upload"); + upload.setBackground(buttonColour); panel.add(sendField, BorderLayout.CENTER); JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new BorderLayout()); @@ -125,19 +179,98 @@ public class Xenon implements Runnable { if (mainSocket == null) {JOptionPane.showMessageDialog(null, "Ensure you are Connected to a Server!", "Error", JOptionPane.ERROR_MESSAGE);} else { Log.info("Sending " + sendField.getText()); - Networking.sendMessage(sendField.getText(), nickname, mainSocket); + Thread textClient = new Thread(new TextClient(mainSocket, nickname, sendField.getText())); + textClient.start(); SwingUtilities.invokeLater(() -> sendField.setText("")); } }); - // Text Area at the Center - JTextArea ta = new JTextArea(); + // Central Panel With Text Stuffs + JPanel centrePanel = new JPanel(); + JTextArea textarea = new JTextArea(); + textarea.setAutoscrolls(true); + textarea.setLineWrap(true); + textarea.setEditable(false); + textarea.setBackground(new Color(70,73,75)); + centrePanel.setLayout(new BorderLayout()); + centrePanel.add(panel, BorderLayout.SOUTH); + centrePanel.add(textarea, BorderLayout.CENTER); + + // Side Panel + JPanel sidePanel = sideJPanel(); //Adding Components to the Frame. - mainFrame.getContentPane().add(BorderLayout.SOUTH, panel); - mainFrame.getContentPane().add(BorderLayout.NORTH, mb); - mainFrame.getContentPane().add(BorderLayout.CENTER, ta); + mainFrame.getContentPane().add(BorderLayout.NORTH, menubar); + mainFrame.getContentPane().add(BorderLayout.CENTER, centrePanel); + mainFrame.getContentPane().add(BorderLayout.WEST, sidePanel); // Set GUI To Visible mainFrame.setVisible(true); } + + private @NonNull JPanel sideJPanel() { + JPanel sidePanel = new JPanel(new BorderLayout()); + sidePanel.setBackground(new Color(60,63,65)); + JPanel bottomSidePanel = new JPanel(new BorderLayout()); + // Mute Button + JToggleButton muteButton = muteJToggleButton(); + + // Deaf Button + JToggleButton deafButton = deafJToggleButton(); + + bottomSidePanel.add(muteButton, BorderLayout.WEST); + bottomSidePanel.add(deafButton, BorderLayout.CENTER); + sidePanel.add(bottomSidePanel, BorderLayout.SOUTH); + return sidePanel; + } + + private @NonNull JToggleButton deafJToggleButton() { + JToggleButton deafButton = new JToggleButton("Deafen"); + deafButton.setBackground(buttonColour); + deafButton.setForeground(Color.WHITE); + deafButton.setFocusPainted(false); + deafButton.setBorderPainted(false); + deafButton.addActionListener(e -> { + if (deafButton.isSelected()) { + deafButton.setBackground(Color.RED); // Change to RED + } else { + deafButton.setBackground(buttonColour); + } + }); + return deafButton; + } + + private @NonNull JToggleButton muteJToggleButton() { + JToggleButton muteButton = new JToggleButton("Mute"); + muteButton.setBackground(buttonColour); + muteButton.setForeground(Color.WHITE); + muteButton.setFocusPainted(false); + muteButton.setBorderPainted(false); + muteButton.addActionListener(e -> { + if (muteButton.isSelected()) { + muteButton.setBackground(Color.RED); // Change to RED + } else { + muteButton.setBackground(buttonColour); + } + }); + return muteButton; + } + + private @NonNull JFrame preferencesJFrame() { + JFrame preferences = new JFrame("Preferences"); + preferences.setSize(800,600); + preferences.setIconImage(xenonIcon); + preferences.setLayout(new BorderLayout()); + preferences.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); + JPanel preferencesSidePanel = new JPanel(new BorderLayout()); + JButton preferencesGeneralButton = new JButton(new AbstractAction("General") { + @Override + public void actionPerformed(ActionEvent e) { + + } + }); + preferencesSidePanel.add(preferencesGeneralButton); + preferences.add(preferencesSidePanel, BorderLayout.WEST); + + return preferences; + } } \ No newline at end of file diff --git a/client/src/main/java/net/xircon/xenon/client/io/CustomOutputStream.java b/client/src/main/java/net/xircon/xenon/client/io/CustomOutputStream.java new file mode 100644 index 0000000..73c6147 --- /dev/null +++ b/client/src/main/java/net/xircon/xenon/client/io/CustomOutputStream.java @@ -0,0 +1,23 @@ +package net.xircon.xenon.client.io; + +import javax.swing.*; +import java.io.IOException; +import java.io.OutputStream; + +public class CustomOutputStream extends OutputStream { + private JTextArea textArea; + + public CustomOutputStream(JTextArea textArea) { + this.textArea = textArea; + } + + @Override + public void write(int b) throws IOException { + // redirects data to the text area + textArea.append(String.valueOf((char)b)); + // scrolls the text area to the end of data + textArea.setCaretPosition(textArea.getDocument().getLength()); + // keeps the textArea up to date + textArea.update(textArea.getGraphics()); + } +} \ No newline at end of file diff --git a/client/src/main/java/net/xircon/xenon/client/io/Log.java b/client/src/main/java/net/xircon/xenon/client/io/Log.java index d64d4e7..51684d0 100644 --- a/client/src/main/java/net/xircon/xenon/client/io/Log.java +++ b/client/src/main/java/net/xircon/xenon/client/io/Log.java @@ -2,6 +2,7 @@ package net.xircon.xenon.client.io; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Appender; import javax.swing.*; @@ -23,3 +24,4 @@ public class Log { logger.info(message); } } + diff --git a/client/src/main/java/net/xircon/xenon/client/io/Util.java b/client/src/main/java/net/xircon/xenon/client/io/Util.java index 61603a5..2beb2e8 100644 --- a/client/src/main/java/net/xircon/xenon/client/io/Util.java +++ b/client/src/main/java/net/xircon/xenon/client/io/Util.java @@ -1,11 +1,11 @@ package net.xircon.xenon.client.io; import javax.annotation.Nullable; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; import java.net.URL; +import java.util.Objects; import java.util.Properties; public class Util { @@ -47,6 +47,7 @@ public class Util { Log.warn("[Property Loader] Unable to Find Property/Invalid Type!"); return null; } + public static String GetExternalIP() throws IOException { URL whatismyip = new URL("http://checkip.amazonaws.com"); BufferedReader in = new BufferedReader(new InputStreamReader( @@ -55,4 +56,13 @@ public class Util { // Return IP As String return in.readLine(); } + + @Nullable + public static BufferedImage loadIcon(String path) { + try { + return ImageIO.read(Objects.requireNonNull(Util.class.getClassLoader().getResource(path))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/client/src/main/java/net/xircon/xenon/client/networking/Networking.java b/client/src/main/java/net/xircon/xenon/client/networking/Networking.java index 324448b..eb5d468 100644 --- a/client/src/main/java/net/xircon/xenon/client/networking/Networking.java +++ b/client/src/main/java/net/xircon/xenon/client/networking/Networking.java @@ -13,7 +13,7 @@ import java.net.SocketAddress; import java.net.SocketTimeoutException; public class Networking { - private static final String nameTerminator = "\uffff"; + @Nullable public static Socket connectToServer(String Address, int Port) { @@ -44,18 +44,4 @@ public class Networking { Log.warn("Ignoring Disconnect Request, Null Socket!"); } } - - public static String sendMessage(String message, String nickname, Socket socket) { - try { - BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); - PrintWriter output = new PrintWriter(socket.getOutputStream(), true); - output.println(nickname + nameTerminator + message); - String messageFromServer = input.readLine(); - Log.info("Server Message: " + messageFromServer); - return messageFromServer; - } catch (IOException e) { - Log.warn("Could not Read/Send Message!"); - return null; - } - } } diff --git a/client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java b/client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java index 37de8c0..736d8f1 100644 --- a/client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java +++ b/client/src/main/java/net/xircon/xenon/client/networking/textclient/TextClient.java @@ -1,15 +1,33 @@ package net.xircon.xenon.client.networking.textclient; +import net.xircon.xenon.client.io.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; import java.net.Socket; public class TextClient implements Runnable { + private final String nameTerminator = "\uffff"; + private String nickname; + private String message; + private Socket socket; - public TextClient(Socket serverSocket) { - + public TextClient(Socket serverSocket, String nickname, String message) { + this.socket = serverSocket; this.message = message; this.nickname = nickname; } @Override public void run() { - + try { + BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream())); + PrintWriter output = new PrintWriter(socket.getOutputStream(), true); + output.println(nickname + nameTerminator + message); + String messageFromServer = input.readLine(); + Log.info("Server Message: " + messageFromServer); + } catch (IOException e) { + Log.warn("Could not Read/Send Message!"); + } } } diff --git a/client/src/main/resources/xenon.jpg b/client/src/main/resources/xenon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..92d9f0d7ffc0b2e1f614b7dd373773fc832848b6 GIT binary patch literal 11591 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 Al>h($ literal 0 HcmV?d00001 diff --git a/client/src/main/resources/xenon.png b/client/src/main/resources/xenon.png new file mode 100644 index 0000000000000000000000000000000000000000..31f2922408a90015e03cdab95ce8134e256f4ce3 GIT binary patch literal 23693 zcmV)7K*zs{P)o`vxH=%Xwsd3u6fhM>n#+bojN{q3Pg+U`AA%y1X&i5P7 z+~4o_zVBXZpYMG4UI|m@kDRMd=bU}^UVE+ePQQ0pYd6wg%xm%&bKC!T*XbE(`~^(O zPL?c@#vs$IkdIdEp!@Kf zi$}#OM!4KYCHW;&X0|fs(#x1pS5~~%0Qq-P`|PxHj=`0`c>m9y>*>4B+a=)@JJ1Tj z4%eih)cDgAo25m(J;qc43#19!5fuU{FKjg{h{-DEqkIVn~<w&nmB?&5yX$Py=ccs<$dsbk0-_Alo!_krHvK{b56~JvetN>Kp2@^lMr0aA6@Z=6jbf={Ng+{R5w4SCId+Mxzhh8 z)xrio!Bo;FVA%?X?Y3U>I=3UP{4++dHlsJ$G{ji!Ix__IGwX8^=SLsozV<#131gnv zk6zmTjxxaHZ(jLJ8a5QE0V<*C{I5xsDdHGFi>_-ZnsnoCQ&Sle=vy!h-uc=7u^m%? z`sTAMXy)fztNg8*S~wxTODz|KWS2x+wx#xQfX(xM{!iyS77(PvxnaArwGOOfn1S~V z%)!1^w%JLYSt+x$vC%Xpu(1Y%#YpUkI_+#zGQ5LYpRJv=^&Mk?$=|x_pVX24l(7lq z937n{;l>f}&gqNGLf{1vro1xCW}pa#8N_HlAybIkez%s*Hk$^hG((tB-MW-APITR~ zFm+^&m6E5v)HF}9-L!9CPLiEdZ>Q7*!5|?hCF`JFXI#62j23NYUGtq{fXUBY^-p8| zZDK1)&jYMLAt<0BP!(5F?`k-ZJC29cNYZs)3`*keuh0aVyG+=aqfp)6u4#%OtZN}* zgi35@1yzgzX88JrAXn#RMySn6wYp>ab7z3b-@E32&3>^akR#8wpDMGG(ZEJNe=+z< zSgY%bxy>^qyRX5CwuMyGutZ>tED?65NxK-q8wo<&S2%6Xw7R8Be}K)Px8eXAW5fhw zFh1?h7oiD^;w8W=BZ1bhP&C%hB5<`!!o&U>(E}k=^XKdvn4ko%aV|B$ImZEtx!?0+ zfV}dAY;ks9#tD?Yt%hC5z_iHkQUP{V)68}C%J(x#fCn%4jPE&4;1Vt378k?+H~Y_X z$MF4I@cr#t<{La>f#1nM<{E}%#~7@&I5X~9U1{2|@WCrjrt)^IZ$>bVf$nf<7K?er z_0uz|vFdz_IaoXXnao=yiCC4A6)yZm8y@9SqOJpmD?1#9J;;DHn1dRfG(S~ zEV-H5uJS_FljJoUE5mDy5O|`}5QO>o5pe>y=+-Zn+6o6$o`HEG;gMJNTp|~8mo-i0 z)nt*LqE}Y_VP5P6Hy=Jn?tE$t_VaF+xokMIb1;pKzXXMGpRKfBIjPgk^jyAA{^51M zR!97r6WY9@J2SpluAS}{HX*;8Mll|3G@Mr{eVUw>{>3ZkM2#SPE_4rx(K{l8ah;@` zs^8wD-RN)i9&N{YexsI;qjI-+H{7H8^Ye@^@_m|q|Gg&G`dMfrKQU&b!Y#oEIM-Nm z_cb;@^_R`d?$@_#0w(|ci+;VrHo~Luw)Hy`qvZfA_30cRQc6uIX?H@7^?~Zk zTq1st&s6OU`sR$C+k8q3Ml9t=A*a;f`^eF|#p_Jc#?;bk-y(u)TcKss9t~)SF@L8i z@F=3p1mJ&oCW>Ab|IDm4t5mYQLz)YXL9s*ylXa{DWt=y5$)kpBQ`fm@I9B1P=y_NY z%udM2p#Yg^NBT%6(%7LX6A-Ln_bH&*Uc9;I@yXBM_`mOxbdAApSZaZ3?ye*8xF+k% zN@dr&S=SQLz9hjVLicxR3MphfzZ`5SjlrO`Q3j}R>5ukHDU8pOeBRsttRH~{Tzl;y z{k!I*CZ(pm%1ENdCU&(Kf1gFby7Yq))Xr=b@O&}Ays#0PwopW?18j(H*Ew>qK1=*BRIP0h=gc0mm>l*_7arCbzGC zO&LrQmaE)Oehe?j(SPBk7yt}YS?TtISl*YZNe!RJ^oO+@kJTmC>hEKS?is?44eJbM z_sSjzC*IoS6xV^AnH7!M&xyWTV_6zY!kM51XIiV~4eYk`0YW1nm0Ki36>Ss%xoX;{ z;`uPZx0f3 zcrZvvcbyS5;gZn3{diLa6`4d5wJOPV7#4AZ%ndc|Asts)6hF&0M%SJLOh;{-VZ z5OD)zrjiw`O&go?6V8lP>?N4f^CQ#rVoBI@27$AX>NIBs4dRBgzGokLhG_Z)H71!M z!lrsH-@Z%4!$br#Sb7(9{hVw{lMw+SsZ zf!&Mco#yUE*wq-~7R9Fvh6?UYyxVpl0`~7^LFHm}kGysaMsOs$bEbNN42yz!GJ)la#stgFQ)ivYN=u#1jFE%axtZ{bBJ>(cycC8KK#Eji z;!=cTB24+`JM+8u%jeoD;giomml=oZxELx!ErYo~@4N%Nab`6DKg znGtwhfUY26NKUjBwpbr(SCs~Vu*60jTn|UFq<3bJ{VZC2$7K+r^zbL zamg_ZY0_hO#n0nyd4({9;fdO0PidE2^T7LgJ)vWPs-8gokVI6Ro?``e?_f0MAk6q> zy;ibLXoB2t;~<;oCgOl6&#D0??|<2cOp*)aI?IR8gU`}jqVKq-N=i&=jI;e7>a@l@ z4gp;>vubcjDc!g{p>+^iNt8>gy#@2QidD-zM!T*PYLaN4h!MepG!9Jp9zx*lW_29y zZ?TTR@2gYAymg7+r5V(CjWU&>+J}Iw-hVd(&IM;>fXV;zvfs@^os!^PK@4)HB6zM& zLnAmgn3mu`{1L@7rZ0Fo?5Bgdz2XExqP^+~ETCuhHCGNvZbNUR?&@4QEh**ow z^Lje5rv{k(%F91oxiTfOclp;bCDRz7FbSavb}@}>mT8R=GIsVjfs-R^%Pax4fDv_f zJfmnc}!EOiV$fAiPU|TxTS3m=+98GsL4sKZ7ZahVfgppEZw}uF$IF zW;11WhYU;V({JqshB34dH@Eiq+KT}u|Jy5mPbY*#+C&t*@7kcW>AOJOSs0)qZiRsB z7@@MOg?fEH^#n1FX$Gk337n7>q7LPb@#dPvVhTg*ihfmAxSHcqYw!lfd^&10%m7oA z+9M7T7{dk;J$3Q6?^Pr)utZ6x!aS&Az;5sDG!X?jws#yLT)ddTkQieFYR0-PjpNn( zw2hxK;(NsbTqbj_ecJdJH!?|>5d(1Mtz6VRb=B4K!O_Q9NsxupKXGk9!~o<+*Zu#H zi1HqmVa#h3=;O1Vpviau>kBw<()|%7boFj($VtODyVnHgHHo!_2Xc}qNJ!#GJwaCb z##}}p&QDQK;QcG>38vk^$!~nuA4rK!+uWM^a^gxu1U9ItLXFgITLws#L31+9QvK8S z+1vKJbLEp4{8kMvB7v3a=fr3Aml@mR22KKFYd04zF`1WlYXap63iY0~xwR1(M;L*z zf-p@cQKGwjbWFmGT^RnNFS|jws}e^D%1jcYhNcKDQ;k0aHoT%F4Ny97liz&h$94&F zOjP8Zs=T~kI+KVlfyN-#q#PyxZdt7STil~GBeU?r?%yNmLoTqPDEF^P$a(+T@uHqU zlIVSnfm=U4i|3A^&Z{zde$7-p!LErb@5A0mMI)wIzE@Zd%v^j9Y+~=x7aP|uQyao0 ze#q@q?Zy0u(E4}wgRjs4lizsN$HkG^6EUz+{Z$b>#+rIDO)IbvuwQTA9i)oSn!G?_LsWf98Wfs`iB*B-V!g4GoWB4oGIj|H9x zFle*w|0g3QC(Atl69v92k>m5G1QeQM_u)+Q=<$fj9qIlETlg$mH^;>kSml?%u*SZKyHc(m!FWmDkr z%P8$uNpvC)j~jf@fMsfUR(CM)s1&%d{pM{71ENN-dz22e66K*L_@ zr35-9_^Xl*K{K~MasKpskkpQc>*WYJ^7gg13~U;Lwth~(Sb@YB!Pay?D=((F$n{d( zCAaVQsnS4xudtD^J}H_~iiZ92ikz{`2776+GKxZ~bxmXn*=RdYg{ssN0-L54^pd$D zKt&BzZBEP>M7pZ#S7n-v0n0_GqOFrX3qnL5tc8jJrYH8?h#1ppy{|Jz4mwP?9p5XZ zwVSRwrL=i}Kwa6YpBEXySiL;Ui!ck| zE%OtK&!Amc#zyPh0#lE`;51QVU)5Z$ZmxMCKL6HdtAx&80NJs=vN(FYCj*pjV2paV z>u1eaLL&xlDbR3R>Z#TG1lr(DR+thkUv2QFQmKJ4T+(ejn5?zpny5wOcXB&50$)-7 z746F<^M6c}+7>QRzMXr}^!rCCxfPXipt+wy`NBBBL#!#(IC2R%2i62W$Yu&e_47sn zG#MoFZED7zt6=S(0#U(Ofv1E#INxwDU2BZf&^v94U1~p)S8Bmtr4g|Hn8rA(2GGS* z>N#r%^D}3ekg$kZigrs7k3?PnUJ|~S1W)%Vlg>U>x~T$oyYGK4^#rASM$2(8E9xZk zJimGZ^#~JX%s}b4o{JIi10x4ySDF!vO85n_MuFz10YRyv877Q|!;19~lK#LGfm>`8 zH3pb9sii4U_X2!5QwRiAg$QN?b&gA{>R)9m4Jn6>tlf)}LUrmsH90gScG||Xh_jMl zPTN^*e=uj%9mb|fHV${2)IZZ)_M#1r{R!-Nlf@LgAN8{nii9^o4d$XIkp7VxXLAt( z_N2GBPfZX69m=2J{Fqcb2y~1b%qAC)%)$0Km-vBwv_5;`iS{RO8u?ttCRCmcak;mO zVZN$3z_gD!3vlh}gBIXy`keSa>$o{m_Bz}B&-8nFkF)i=hAU#N!CCHK!va*tl~%C? zay?Gq(<+9i{f2Z)pOYiXc!EuSR3)=b&Ie`QoLB-x{s|3K_X+pgTC^+j{KfY`7IugD z%i1)K6&heKl}a0?F^cm{LhAUy(Wn1jr7^4R*UU}KnjpfIZd!bhl;j%mC;^m=3IkX_ z!nQ|$Y17re%Jx}1p*dpEHB6xXm>q1iC_PlLf!WPHw2-k&#w58D6Tk zxxw$;&c-yVy}TDL@fpX|G=QJG=UZKRcH7iojpmI#|_HFM;88pSfHVUy?XBHbl#S5ks~ zdDI^mQ~ryX`d(;1`V<%;Rw89}ZPFLtvj#C+ZHq{`ddOA<$>#G1pJZBSilneY3CL6v z=nF-q(5V6P3PNLxaP3@ZNMxO7TGgOl_FQ%YSSu8@Xkln3;tZMyGo+?za%w9A$e{!5 z&L}`NBvZ*vRTW%L%q&UXQ!V(UY7z@9kSBX;;=Q4>X$E$)Cb0!xs59Umn#j*D8DP&b zlvpG_hn=s+G<6>zw{{ULT|afs5O-dYOd{oq;;J80b7rfN=@y!a662F01wd^#GXADG z0nTNxD3*N%fyD$eixK+4FI6Vd3s|c-!5Q4Za+3F+xa@8P60N)dzbK=B=Q{6PZaV$~6K{tKezE1{<8K2AF2fU07jCbHw=(BAu=$sIH&V0JGv0 z%&65R&}PKu&%8s6+|8~hs57|O#F-%POE3Zm&2 zY6jpaV$ht_LR7;`wn%kMxNWjXR85L-?`koIn9*#Mn4~$MHhVj1*czvO7p|s)*rN}Wj>vAmV2rege+N?gs)cm@1usqQ)@Zi5kPowo4lrvCAZq&^~)cX(hu=CBp26<{pM=6rFT5zcibLD=i(vKAO!@ zEP?Yz=QNUNwZY&tF~9(to_$#8wxIz!sXOXog2$NN?MjaOkWgmMwnwMaxZRT~8<18~ zjRAai``M+snZ+OzNFWIVM#bkG^uyPy!#vnb3&;JiJDLZMW z(@Ucz{KB?&b};y1+G#;cwIL}jvtW`Bpde@te6IQ2FzdG`Aq7l*R-8*`%GzYnMg< zflbkYjAUD>Jt_foSGuv4x(m=U%!@a`jNM6^pYNve4rP{P>!*eG9J)2aIooJuJ9Qd! zV4y+jG#AobyOnmp2xcQ`hcWTM2n`Id&3kxAmVGys%08GqjZK%y6Na?Cl3$Kq}f2 z12kdhI&+L^gF{BWwAJ6jR8qqFnA;uW5&Y|8Z`t8w2NHojW@%>#z`#77yxx-sN_QtF zTN9xG2h@+EeingK5QDXHAQXqwZnOPi8e*bM;`<_WA8T6)27AMGDEtMSvJHZRIXh?* zc%eNWU=rG?i#9v30YgqQ+HCR<4A2@H?^CYp zGWPr+s#>CObXyG80#k6j0z|I7CIE<)2Eb_~s#X=djaL|2{X_4-Oai!(KBfd4rE62L zTcYjfU9ui=0+(<*%bs^&uC#;F2=x}{@g53BQSHxo-a(|erNwxzm`Ybw-4z31N>XnG zs|r;*p5Q?uOuwyN0ZUR08%zhAC`CLT zg%D%_W#*`+VWZVLypkp)dgPJ8!LTP1^@n|*&@Os4mv(mAX?+W_Gr%{PfzI5L?D`P> zwk$bSqM_p!kv$k?)Pz21L!-3VrcGw0{M{2F6DbC7+l_gD-PLriP$tvk`D7f)v`hZb za~!!=&?f3sgOR2P%lBOYm`bGsV_W-Cc&^w%)J%gn8uOB ziS|4f#Bbro-EIeM19fBaD^BGZ3x21b56imr0nVeTm1K(-w zr+HxOxZH|B$~ZyRE(Y-3JO)trxuuS+C$u_vG8h=LfPzqOv0 z<`<;qd$6T#FaZ^3qYc6W=(RfSL@3aNq>Txn-~tYI*f`X`IIS+}L1^{aIZf>5-!l+` zxk0Ba=Eg~YO@)e9Y``aF2LP17kMZ$&cqt>eEWq!n2jX`Y8j$?WG-+?9$AATX_x1-< zvxn=4hH)vCwkM$OEJg{LvXzJ&=e+K1dd^<cUzYZH>~x{9KyA z^Bwj^X?}4bZD}7ts#LE^u3GU*BK<~YA#)oJm)2vCVY0< zDf9kwq=1mz2&?&4g35?YO`|oAifh=hGA|+q*tfh0f(~RmIl;1ZRL9h^2%nE(JF z07*naR8pXBPLjYD%>~YBH|K!R=hJXVGvQXSQ1jJ16d!iw0VqJ27@8oeeQE$|7mJ*c ztHx<<-%2{Z^$esRS_+IgBDnMZ?XX|?(q|tB*!Fb1jt_1#39Mj0`CLFt&N9fv8|DtR zAm#0}WHz*TaqL!w(}r+BAfBG2%g?_$Ew+}@#^xw3HkZ@F(qh`)20o#((vvCb7$B?O z+^~(mZW1jtVc<=e$Vg-wl}8m9;W1+odj;xmO~@GtZNW$4R3-;4 zi2)Xu=F_Qd_|B9qs0&IM9X(aMKr0LZ1U1tbKyM7L04VFscQ84O-Wf7hx7^;ukk9lz zelx&IyGV({Y+^F36oA99s+i#j?%fjJVF(9MMT}9jTC_c|-8@$T5B|B@RtIbaQ?*45 z8;a1j(`$&?9}c%PNj9bV=_FRF0gVGc;r8miyN1QOId#ki%oinU0#0RY`t;^WXc`=wo1|w?KMm(}6AZw70J)q~0q=l} zBd5>?Y0MPNVA{(##WQoE31oSM`R&FuscHai;r)aJvf zyC0Uvr78$rz6S&B^)5sTDO3oU2))IG4LS=@21gy=`}l)@ztTuYPCc3Kd*Dv^CL8Dn zz}pDK!ANQSprg=pVguUF5stxJfJvGFf|OzXVItZXPiqK9W%!VP1h<@5X%YY9aGeH} zbUs}ZcS}bnPo=Ltd`~*rKb}T&!?Xbgm>|c%aAG;WKo4=O+(NcIFBXaNSQLX&=Tz58 zFPQ)tW{5wUCOeM;8%J%Qz($Zl>Hb_Vl2_D+v8?L-TlS*4BiC};pQpqIDGwD%if!osq zkKHRZUA{_{KdYl9gXmoD;!Ot7JM#iyfHvNly@QXwGu{C4?@13H`?`ov;%YUIQ?k$0 zHFXoN8-S`qIwtsI#+rCgDP-XZ`8Cc8M9DTR#0vF~;UvW)Lg^K_u1Bn)+32T7Pdt(y zefD9+Nyc6Dkw9x!dAVu{W2w5S`FaU=m`4&VgV4OZeTE=u(Doux?Qcp`P&3RDK)-*f z4cLx>z(u57j{^(=0M5@ZVmcPn%UZnD*ip~xMgGBFe!Yv9ZVqSK&Q?CL$hJw^=BbT-pd>yM;+ z9{&oqGdfWTsQ?dq?g~n-Qy4e2W4uo&YoE=nGdbnhIsSl)q5YfH^Eowan?WKwKw51o#>^t)&XSW7%(vgV0Mn^RH_|=5mDrRVd5BwX<%~E$;B~T(T!nR zU0qGvo%Qqp2;aSYl79CyAClQ&4gmT9LlET742h2`OLM8eF%simz4qetf{U+D^P{EI zZOo_f&=`)H#0dy2)1U$nFai3}!hKa8gdB&k4(bQi5mWfalMkhD4IYMzxrshP+JQ$l z8>iC(OyzOJ9`1a2koLdy0NlZSY3Jks7y~*4#9&-Qu3LWqpu7M9LZ2Nc)q+eOKWXZ* zW8v}{?X-D=dspVvQOoc*iw+o5Sv>(=W2J!P|65G~nM`m{YYcflOU>;(6TY17lm_S^ zYu11S6AFwlG40k|x?u5AUX?uNJNO}GEI^y?GtAb@N2~_pWIpA0T=MhZL>hf|LoF1lSgm1Su*V8uq z`PB=S(uY6)L6H7r+D4#7wN;QvyS>Qh=<+ zmlb|cWjh0CvH%^C-%t%c>l&yPkTlrp_EPiUa%!JkPG8%(8w3IF00Z!~EzckcjNWzs zo$1zV4j>{Aa8QBvN6ewq&qlC?1tJGl4yISV^t;o=oy*b) zKJUV0PRT7Y2xIJ2rKiWI(j9kyF+FwUNey~=AEIMB$j|{ekvrvEm+l~$0D6uZfwsSc zSn|f^>9n{HW~i~5)|;nOf2p7Ddg#mPt4HrsO~Me-Kx3R(OH6l;vx#IBv{o0y-=EG) zFMh#I>5B8ON=pdX%%hTM5fMwKeOruYK-y`Z+VO`ZQfmwrfytYnp9j!G`WT+je0K>^ zztwc%p^MVhmt4a%6N*RRX3}f$s4O$p%u7DC{Zu+~uBYE`VGaD@#Q zm@S_MhCD_li0xa1^gRYpn?paZYJ*B-%H=-0H)eg17a+96n{~mtMBq@B113>+M(M(R z7o~T6|2ttRSHR_*4oC(PG<+#-$&@kv{@{}zNt5|eTJN7$%$~rh$+$fq&&tjzMjaVA zhArtrhT+15cHwZL)N68JlpZ*KU;6adKdEQiQuT}2#Y=LM@j`1Q9auV? zUVPC_82hs2Ea-d?&k#cB>m)t#^b?APvZi5(b|Umm2FOG(q6hKK-HX5Mx;-CHqICV9 z>-7}_beVBu5D1kwZN%3}U(%+9n0mLjn6A6v#`KmO-YO=5TtRJ)5j_MT5U%D^sE?<| zQKt*H67nRni>a3>B-?E>Em~P z6pH^O@B{s9x}iq>@Of>=EjG>U#7esMz)k5Tm%j}7<4{T)NM@m@`{ve^73?-p;e^TN z`yBr84WD@&On^Uoy11cqu!d>|8UT`AWIGp|E9pg--;^#saJj%D>QTH@jS6%un0ab) zLUnyw)r&t{N{KQ1no7o1!Zow#V%;atj4{OsGf=z6U-L)&FNJayjJL101g2a+5;AeATFkcZhPwU z>Ha70O|3P^Lk9&VsNm%fv_+IaUK>oHzzG#KL7HP%=gUPb1^F-a7^Rzn=c&>z_~ep1Ko`$PP;OnDccOb)VfAV;79jPS;*` zL%MkXl_(5ZfXRaNqZF)x^lcAG91@}fvj%OfG;$l&&%_*U#5!2hKs6nbN=`G|%=!cz zM?j%HMD}yLkuKVIX*z%HBAGyi2S7Mg82ewFg(`Hha5)9X3)?MHr(HivLQx0Q12*QO zdYH=N2Nf2Gc!GK+Jgaa4j{_ zftB-Quc2<#{sIyQvgH7@s|S`n4nep|=C*&b-kx~qfNGfuf)MB@%mc?rjk3V(Ut`&V zMff+$rMD6o<_rb{XisN?YJVQMj23$|(wPfIJXFb^hBXb?YGZXm&604oI4iic7a)v# zmBBh=00t^BA#};c^ab$}pQ5vU`sUsJlCZ{BD_wK(b?J>ay-7h4M$txnoDW_YppvK$ z6yKa5r-w%mrjOnB`>C-4SwOUmm_i;bG)jD_69=G|2zA$XfDq(!4 z7a}v{K`EIJWnPoNTF5(^nSGW*Hik<@|~x$TiZNuT?( z&!ueyQ80&aFJUT#`so%<($eYz^8ClrQyY(`Pu=lJNoWu955EoS1s!4-A3+?tY6lh$ znA$Vht>!k=u_n%4A;tQL)bYod23I)|Yw)u?9DO$cs=koW5nJ&9i~=)~JU3MUg{lY= z6470Ct;Y(jNAA%Wq;%loJylyC_^60%;TyFn9@@i=i{E0HiReJ4>uTR&l+yJ1&2qFf-^&w<@*xM6>V<6>5 z`uzQ$OHXe;fyfzlq=eJZf`?)19h27`AmVlc;p9IA=zAO(;;5LQ0gg>r>9a6dLEYg} zI^4Z5U3Kv_QuhqdDv-;7jo0@ZEsd#Svcv>TDI15tS-^+*?hHt&l|)D?a?DQYkVB#XJ3o@|@oy@8oAP?KN0T^>S_BXCU+n^a`; zlH(60Qw2=bSmdS`UKu?W@x@mt3p5~QiL=St|bb|bodo?SC2>{)1X#VB6Aw|6Jr@b)2^_U3ukASn(Is` zzSI!qdD^Pi!Qh#$-|IK)HHYCUj}u}*&Fm4SXd4wXtt9{-*v&oyMBVm68m_~ZZ!V;p zFS;eY^`$?8DBNKz&N`p~%A{|Zx>K7+6d-zV=j-XSfBJ9X8m4rl|D+^+`Lc!dxx4-_ z{pq7$P@Ml*4<%plKsFF+?m~@R*uF^dhL^9sGTnT|E$QGq{CmB~koJj@K~u^Maax`) zPZTE}E+_xcqQb!ho?h!DqPzOE08#ko7Mvn(AYR}*nSo4a%w=(pQ1De;-U%OGL}l;8 zu2(sEYe&CZ?9EE?E5^uq|EdkQ@zX>s^ZlX4YV`y&FoDGivP+s7z>6BBTo^jl1X*>- zW*?O=OP2TF>F=cZ4q}0=eW|?z7jbJT-FoF~(sy6}I!yDtVFG4OS)hZB|q^kp>IBw0zNgdicbH=_6nIH`uB68CCd<8DW9>;N&hL>b8x@+6&SP(;IGnV>&o@ zI1M*B`33+RSP#Gx4FOy252LUwkB*6u-wlFW@`OcuWEGaUF&JeLL0%WpL8*7wBGNDf zhfGV$6!5#5ka&^d>MzFX{YaPrX#rE|(-JJL#h_84v7V0x&@~nFJ@<%W#Q@QH+80fR z2QWAhT$I6x99QRZPC3#pw8Y#9arP~QVMhnk>tFo+>B`m%p|GI^m=Yy8rW=Rr_>W}x z$ekYs9(WR2#UtsrKlU$RQW)h&5ho>jQM@K~%6QxY{(avq-=8jDyE1hc+FkD}W3R## zdL6zxUDa?bdEz^0LV`vsJgLRp1wbV5xl+|yFe)an`~cltNrg;-Nu|=Wp05b$x5b;G zW$MJ|w`^zYM3^SLNqz>xb&YklNMMALGiaRI{uDllb1LQyZR5b#RtAxeWJ@fRX(Xu!PDnspGfZDjXz5bo)*FXI$O3W~Yir|1W!A|ca zTIxfxSJQ_+_5Yy4XFl~>JL&l72!cI;`UH;5#1i}~d)G;S<>fz-t~hjU+B!Z&QfV1M znRz8?(%^`aSRHr?rYupQ(m|3KIRq{qZK7b61n(2CU`g2!E+GjzW;TzYkEQ~yE_3W; z7%id3gt3Hrg!KVqFf4;O%=e91=ZN{xVl?2E0T#qsfXDrLZO>Mq0v8%dq)#qMMrWpc zGKmFi$newx?rmwV*>1hKMg-9L#VX|Dd$oF4X3$E-Y1*{%80TqEA&gJU(D|+<<~(kh zP`<)5*cfG&;@-J;qaQU9YqS?%5qMp?e^`_M$hX>o0jl z>K|>$_wH@&zzw`89q&V1pc@T<%@!hkoZ10AjjtWQJ3a91H$@7YL>ri`^ySy^@jT}5 zU*m8e1t4-R#VCB|MSQmj zQcEzlU>b(S9XYgZxqhAJ#UhGLh({P&vgEPtDrVne$2Bbm1&xd?ecUn%jxSU*U<1y$ z#pgxfxLO-%b7jccwSXF-iiL#O!|?nZcEt4qeCi=;<0+j7Py=v2Qp4Z-!Z)Q)J@8>% zha3T}gDbC40jq4R8*GaVU|Xdyj5^xDqE3+i#se=+ues@Usdr+SE<5k4^yDK?r+E~6 z(4H%TM+ZYwZN(6j&f6aP6AT&Wh-`L;c&8*#3a5|9cMd4l43vu9GWO=@vu22M#Y`Xb zo`L+Mklefp%Xac!Hu;84LSImrJa$!qYlf@iTqAz&fqkcjQ24A$D_hmC?b>4JnAT8j zK_&I%S*rrFq8!eF;U{gJjYCmSz;N{cItX@7Gq zz4E44r^k*xnC?CHMX2OWX$%{y1@-*9<_|Vv)TFeI^F5{@CQlh+~&OPAU~tWDEsD-G-82 zWXx$eXuymj&589l>3lmLwH_625C)<43E?nxU(!T;0_M!BKSN)}Q-V3e9=fX55cTVDG%#D|sx1ZW`2o_i?`!}-F*-;&v!YVP;Ay7Z+}p04G!Vcf+(!WG z+2XrGz$zoR->2Vo6xZ;tr+4TXe;;abi0ug!JH>=l2h^Z*DXk_9MVYer$@E{u`Aza> zyK52uo6g@)$MVgWcV_c@RW%3Yy$f%kTt86u%>>dJV6XS-!ITWc&!R@?L(!Er0}#bZG66fd~UO6mEM*y_S?Do74wG3?DbxXg+P%1xruVKFDs#tKxk@$V#!%{oz11(%>vjsxt~29KYU37EzM*&boQDyXnsLbRnBiY>U&)I;|_ zkhV5YVdzxdtR3)~#z)jnR&ek%GJyBoduMv}OK!!^Boc|p%`V@vw({+w}6{*CG-qjZwj%m4#N7%Mu@)e$y% z^U6_B{aa3gL|_ogx3;6}>BKi)G*_EU7UtB5!vfK%-O_cO=R1m*s;9=SyNU)j`SRZ# zG}>}7USR<}nMr=4DHNVC#;F+KOcS*iNTjQ@ys`$VMZ^z57dCwA)QPmTuakcN_y1oN ziD>^H(~kCHXdGi;L6KqAUix=m`y_VvYNuD;@T#-6^-l5HY+b6I$TP79OGnHwR43kquWmQoNzC7xF(`WAdc)IY? z3)1;>m!uOrr_}uN7rAQxe@F_q zhw;y1ilP?YV4SlF!KBch$x$$mz5CMj%FdL(GsDnIm(E|2P98g%EKcR(pE7Q&?_*GN17~pvQ8+stRWl_b<@tRCht>`sOivwqHXXQRE&cmXekOfm^B(+y z&89#k?(wAET*w6U*r>MQ=CL4T4*LrYv2cRU)(D$Ve&`ebGX2;Oz9(IN@P+C0F%lRB zoCy33SZ_}$XKA;ZV0f9~m{xlC5B{}usD*k!yjai@M32v`ZjM(lK`^uhfS3NVb^CI&arG9+erJD*RXhI0c$~QxqN9NEf(Oh2v_}5?OY)hiJ{_}U>5V{ z%&yxjo2#smY5Z~}ay8K6!)|xdevEd{iUFb%(@zeOl+muBh4m437&(6-eFnMx+aLb| z@_Q#hBJ3x~Ze#ud>amoZ>j_w{1>T0nSXo|4>l??#=~!ijg_n|VimfO$qWtu?SW$G6iH8Q0!{W%1JqQ0FS4pUGCM!3Tl;`&3>13rPZ z`~T+7-$7Z|)7rv~0UK;DpnOX2I7+Gz?78Z~8*#Lo9(v-NsR!IZ0uE65JBD@`Y;qH- zak}HN+ta?=&r9F;vbP|#ifP=!?}!7i(`^)VHgQahMS|o1oe?&KLj3?`fnnVa7>vi; z&`iwybwN;zjobN+;=9)36;Vz_E9m+aL9oJD$W|+o5t9o6=pkFmis})nh`S(u2L-3X zC*E$|eYWIN6qZC$iW!4Kvq(fkjKhS~zh@z@Uzfl6ClCBgw;$)#FhFj!Xx`OR5I-VG zQY}d446v#61}y&n{jq-z`F$E%{xq}#7KNZ?XvDOn9Y>Y{mo83!^)){Q*RGR(>jS@u zbDvfDCkw$)y^Ug2oR7jvs@~566kc(`73sS3Uku{6kt@Pt6a2q7=58JYe1ev$6x61Y zEZ3&G)P4rc_JM2!Y=}W~-GOYVB-v+{k|Km`^&dHaY+=;w3(_3eQn1L8Xl7g+b4eCA z(NJm`)lzL}s3uOd+Db09HxYpw1t?7k7lCDWP9z#;Ezq{_YWUB)8gG{rd5 zAmw$wyISmh5|j<6EUba<3sW zXCp`~;yEG$`~;Z)1L>y$RUE40U;i`d^6u5?iutS3dw%G>&<85JlwSY<94ARcK~x6l zqNa^1eFuf5l+zK|=;NRJaC+i6k~|AYMy*hxB4yY>;i)3>W_n1P2G%uF)i$}0;nX{| z7qx~;!Tr6(JrJw!z!)H0mm6h*kaKu%Bbn1hHE$QDhwbq@$4V@x_8D+gF{iy#^R&76 z1G7E`UK*%C)fj{KDK&tkU-LRUU^FWZaQ0(+m!O?3Mi8t&llZ|Y5yc9out@;g#bCF3 z*ev3%f0w@a=x0g3w9|m0U&TyNuc#Uvwf9H^zx~FaNY@{Fd0N^ykmgP;rK?x3O7D2Z z5960*(A!`LcZ4BvxB_?{+tD0NkEK5W9yp4k(S8#fKw<3xh2LU&xCbMJRQoK+X6+Hc zM)?=V_+I|mj-z&L$mQUS5c+m7W8YAUTg}=Z*Cf*X;5>RX+?ocFPy6F?auW9Gy%TFJ7F! zeC)P#{u3xNS;W-CTaX@9CXvM<9)-h79EIH(AmR!nUy?=5-vT4`xW|#gcm)s?C^Ash z)VArOh+Hn>^-z}#^^Tp8vCn~lNHZq{aNK*(CRS(YQ88_}xIOGwxYOHCEriJ}&||7w z_FfANaZ9g4l5yDO2`Jj(0M9am)qVqhz6~l6k!1$Um${s=KY;(@8N}au@|VPOVgOeA zs8~W2z`J(pWPvERkjG}ts0s$W#d`NP_Q-h#bv#E?^Sn{|pMLEZV4XLh4SL$!M;yhK z!&s}o0^%Qrm;TdleQ#Pu=~$Z&biha#P$N#Y-`{Gczx+KvnjSy#RJ!lPJ=)Zn6Van= zA`$TL&SQ$f{odWblU{V)HFz!neuEpigq6ITJCHyu*gz!$mUqF0#9loMokamXRt%X< z=2kZ>XD5*iNgLI;T`VLBc!6My|M!@T;b#U`!pKwpysp3)ESSi^V6{yv2af)CxS)nw z#h1-3Vt+%e93X#H3}$^G@vDt3swi2k-M39OB>m#es8gupliduFf7d6)33xs0A?dkr zC)m0xD;=bPt8PBr8(Yw6v2W}hgvsJwy~j6Cq*J3)Y2p02^gsV!zob<_+(e0+HcEo< zH$)!bQ0EYo`)Ydk54;0tyd@+myFR-0stRVof z7h%^?Sn|Z_$GG2uMF0_bQg{{ldAc_AG}!On5F0VTEEy<6{QwrQa-TyMyC_rV{+6Qe zTAF6t;Az}kuC6_c?K20j2|!*9s!0&HguQV8<>|-Y_8v(h%StEQl9ZZg2U^FY6emugx`@P=3udJlEw1hX^U)=1 zqI8`lYvgWfAntO;RZH_t?EV2CvqdJvYTvehW@wqap)lSy0#fJxgYh?Dk;{zlJYN2$ z&)hh>sPVJ7eYkD;y|n*9puND|tly`Pl|-filCTO`8n`TfpT71t9{Yug_%#Nw6Cd!U`Z`p7-$V_*J=q>)gHL*~lZ(e^}A&jiUUFa7TH>KDHj>0Ks* zG0|WqF2!PdoUrI*3%+yjWFx)s!W+_CU;cKrN!Zx+aH)Jf++YW5{kc@N4K;7_PY$5P z8>B~{c~nf=Sp+U%KeR{$<)2`LIqclgWKk#+vXz5@rW3HNTXpB_4TfBLNt{~GN6QAj`5{$oEKK8<72r}T2I;9R6Tk=PA8syHa&LYo3Q67 z=p^DMr2#@GDk-8%g}MW6T5P7O1v4->)=K-A&QAvpVEGuckxV|4LbztE{n0H(V1UVN zzG}P}gH%oi48u(lWGxwbhb0};Hs z;>e7oGiWl*mRKKnGygsnBKBXaQAB$&j$e8B@9!c$4^)95K1e|-Opzl3Cl+2n1m^&x z5UG3eDzgXdlXbln*7u^qYQK=Om>U2pn{bhvXtYHmZ8DQVg{i|6r) zUyQ`5aRq!j+@S;e{a%Lh@1u`BimITuVnfI$(!EE&EIZx?VMg5K$wAUU1)NSxf_HT9 z;oi5joy_X~<#aKYakJ)sf`km0nDr51YGdCb)%ej2@soy#|4|zeOBknXzG?}hhz2@# z2oPDG*W{DTxV0TqxfB}Cot{&Kn2EH2Zw#%_uKNH12t5$QO`>)^INL_)Lp7DY*LOok zyQ_&S{=l#Af9SuPNqmu|*1Gc9w6hczpmOZj*Kcj5BRfx|fB)rAr>~v58{9eq5ImXM zi@32ok49SS9MrBHKl=Kg#O6|$$mN^wFQ&O6ZLq0B`riUt0$Jo}D~90aTnFY4BB20A z+L}-Ae*1f%Jr3EHGz5zd9-FeYtP2Fue031nmm5jkee{m>r3b&5j_y2-ZDz0{7+9r= zWoW}W?DC21B)6RFBRa{_?g4f%;a|UlZQ?OLPE=Ik#Jl;}1|G}z_#DSg7+I`J0Bwky z=FTea%zw|pAIt4e+vrf=R$y-b?1^PY_*e@l3Q|jZZ)0hk9XFF*#~L}{RHO0K7hd_k zpMnWcK}YBfaW(=g*yUrZy@8!UZ%ZG${Ug8urxfaiPp2n|5cn-&&DRJf;O)1(BfajW zZ%E61EG6HW!ydx>(-z|W+(d#LKGr6XRvBn6rma5WL#j?5Kama|+7D&IqK|R9;*uAn zyS{O^B7UYeF%bljj1-dZKzJsZxRQMVHH6#98?T=@mCif=Ft&3C)8O1Tw!5K+GU5)W z@FvIzLp)e&fZe`^%wKSqGu9fCDD0d1$N&$Qk#ik^ztg*CE`p_JyM6}*=61FU-?D<5 z`F^_4QL$?tmNC3#6{ID|xzHHq=__1XdnqPRnG*l0t_YmivFn#xxv~|H@hrg0O^zq-uGOnjpflJii!Q^t21a7v} z7ul>ICy)S=bI@G=(b$77LXB&h>#&2;EMV&As=4NrJ6(@*)wkn6cBH-iWXVgb6 zr0k#=bPa0)_NDuuxF1u+U4RiN0#THvg;injh7YW2aXmhlt|8#FIayDSKl(_z6vZT~ z2UgNaxU5{$$PFj>5X=EI!vH&vhzXzpxC6-qi%RqiBr*gSe&F{Jj*YvuBPutvHSMh2eUdsZuxf?NA#oAJgvkUV>^Wa8b%wRUzYM5l>Nf{yODJJkUgApQ) zRAGY9@md#Ytrx`5QlXM&k2FVE__9D@qviCr?89R}Zg%_;;oce)PAog6~N! z>{wXAgCf>3fl%A43;Ph5nP7va8`4Xzx;Y(Qx)2X=L|fRre;He^@yR0Gpn(KaFH|bU z!cKP5Vh*hR(~2H0D2458Vi_1Ya<#jdUUbPz)9a2OO`m)4qsSXzA0aF%SzKzRt!*m| zU&7AaEFCAa_OYWVD^HF9lzjG%KTLPDZo_&7JZk{k+^uh-#t<*=3lI;M76dg*q^E&Y zd6gMrQu^rA7=e2RthV=~rtuYNv4fN}!6}%04xlOWCqV(#{w~ux*e&xjU;MY}u|9Uo)OKwk zJT_$PsQwT92#^OkO@HauA4=C>_>#1QsvSnam;s~2GmEC^AST6vN)bsjG%P0Ic?=(O zS29{RZXW|>z*Rz5?7t?pUN%loAAKm@ck)ZBzT3gmO)GS(M|~0D+vX1~Y%DQEFzA63 z_sJVk>DVUs4-Z39hF%QS#{v+GUC(|=s?=j6QTdL|W70}*EnTyHEjGK{ucc_*Z>hzY z2(Z)+qLSQvy8@fmfLRCXw{ReFq5FQ94@)+O!dzX(Np| z!J}EYh1;RI3(}iVUVh{gPhh+DCzNfZm(rO>`Pl}fK86Xtz`W}T87vFu9k}L?>fbDH zERYI!$`mS(G97K@C1-vG;mflyYOnx+E_OuWrrM(&7)T&N?x-@MiB$aFJrL=CYCpc? zi(GCq20{=eY@hLcwiS=7GrfQD{{QNni7$rX|M-j`v5x#}?(37%XTR`iW#%>j{J44Z z${HWbX3;oqocPlH-<7`q6>mvv<3m9FsEWb5IKIXJTs`dZPsGg_0`H)Z2)u!gHfap7 zX)aWc8b?p|MUVf=)oap^|K*=dOV}A?j7Z)}V;?}R^f8^x2?W*u3_xuMF2|r66UB!>&mk)`Mor)kVCmQk4&Q+LbF~rU{#Y+S>C>IL zUog!Q`(P6)wu=QxL13RA1%%uf6L)ZD(C77s8*mhGo+QOlEtR*4HLaM{dtM+?6208n zBm4i5Z4aV}*j`Rvg`10ZdF*{({m-8>@p*xzgPAr(VuO!@XIy{tDB}K@$aMr+dLTMM z+8nZT!&A+4%MG_8&i_4W4Mmyg_kwljd5d<-J__%kdz{NSbgzRZ#*K&Zv zCIDCCybd14#?2%~Xk(~oBtpL2tC;2jQh&*TtI`wCK9b(~=69uE|JUzN$FUBP0Xo(A z!N!vOrUMB7EQ2KQr$rpF@d8v-zbsFq7SV>W72E5AJzfC<@t15WF z2UoIygf$e3{N;*d3k>C($er^j<1G6xW|G;cFWA*(XhunEd3}xa5AXd?pBM2f2AIHB zO;Gd1Cxvo%)&qG0$Vbd^ zv9@-~<5uicbQlPcIssf@X@)`g2zO!vJbX`*#0{zXGzAQ;f*DED4$%JO)Hof)a|q_~ zd}BrppXfazca;)O(Ao$3=hiaUop)o}c^2j74eS<f-quF^q__26N-EzP_(NEZ2gWse|snv888ya$R6YJ zgdNz?q{9l{zHR162Uok9s!ks_ntE4_49yt&t2y!C<<_EaBW##O{KF@vY;7S7jqoHl zjAWtFh-=`Sp_E{Ag7cU(gjW}ZODK}Nnh1upbL(pjNFTQR_wISGamlywvSbkH<4^Eb zdXrE0Ab^q&Z5ce>Mui_lTnvx$DH$2Su${1h(VtntjGDT0L>!eZR`%p$(u8@wf%<`9 zQ4yPf{64qbnGEUP-a5j{jWtO2n$pk=x3V@s+rud2>wYq~vjZcI!2~oP9h8(c5m%-J z>d`M#?<(>vuQ*@GG6nt(a%gsSxbjHs$C&E`n%sHEL>pud-o zakV-0fP4s*r;8(2QO)s`kUd`>5ChoqAw~e{UYRRsPvHx+?0ScMr`^Qu&(`M~*G9*t z-%Dngc^)U#5wJ;srEj%iB;6y@jjk!Rhx2+BLqm5K5i%abU8LIYU}<^>JA5<4Hs4-S zJ-`;9dBoyPe^Q8+TSGi)+BoL^2$4cxaimZ)j>fxKhRfpcRvwSv*`briPATX_;1^rO z3cr8>;-L{Xt}Li$n=6|$JHH+<%4Gg;@BZn0((|Q0u5%4wt5^y3$yj=S(D6N-Z!wa7#bC`H>c~+IU(`ZEr-iOMT~Y3d^RM#}Zy#A8QG?J)J$= znLCn0#ba^CJSu>&L5T`f_|#6NZMjL3Dmn-*&4#cvEW#jCw_YG>C0#U$OlEP|e(&O) zXAtPdLYXL`XT>zV0x|?OgIZFWeK!*X^JkSG)XWTEOpv3M|IM9$?azhyY@`yVQVLbQ zsh1Z*Nc=Qi)gU+)dt_SnZ)1Sdu1a}Ma5axfxP4OOD$=e>wRvf@)*cvxco~8B$}(xx z4t!_KU7yFH`p@(z$74`9>vaGOQ0F%wH^S)XxL_c9!uBw zjD+$#H;c9hOzeK=^OL#lJc(_qm8~S4gy*q--_{Dom@CqrVa{gx_vM^@JH-D7*Z*pC zp?a4FV8xCVF-Xw~pjjoUGT=7*@CxjSUtZ&fLoIhx+;CR|J(omAo)lK(khzHZNtvj= zvDGwsFthEms>x)^)wX?PpSil2<_hjAwv_3kRDFp0KNKo~2p}fH+bj!b31*AQApR5h zVvNIH116&I(hgl@NrkI`0n48%?C5^w}=VM(uy_*WZ@7v zzQ^j_yf)B!+DL!v&wk84JkQtv@YSE0%>V*lKKbUXlK7M;CRJEHYkSWzHb)OY&k9zz zZL;>l0CBf7{T_DRsNihiIRj}4-pJE+iXbv!BtISiPrW1KpvUOS0=BQ4gXb{7mhD`$ z(nh*k&FUZ1(N?{qa%uenO~SHbnF0e}<+~m}ElgzIftGU>@3#9Fg43))00VHVczOb4 z0{-zqR5S;r>6@MtV}gwG%kOsFe*YVHyzAQ{{$4yI4ZzJ9H-;zSXRoBqjnh~bg)Lu{ zf1^6>=-B`xR0PbUN=Hx9fU4xWeN_+n>f7hb1WF5Q@-3LFg$!asV|3;RcA!cZ)&S`G zC$$d|>kQ{b(mr~ITs&O4CW}5X>06t4)-axF$RLWPxNRRk*4>F+c>c~ss#0d2 zg8x{Mx_{w35EQl=nZgAya$}o*c^}CoY6{B;YTsaXogr$Fcm(3g`=z9d8K{ILa{68! zmj_aO7iaQh>*<*#IMjRnJYBQHx?Vgvi;Tf%01Q!#aZ%&av^-uxF$i<8oCh1=w0}E|YWMox0!~!D z^yg_#xg?D~zXe{#DDxd4lGmDtQNlBbo;mS2!o{fh>%fP{wlduLVu0cg?nt>ZcOdog z6wd{?fLw-#3ss_}{`t}^KHE?amu4NG_5*N`rs7OP`{i=-AW$6Vdz@$7w0r#1V~zQl zjswvAY1MV974)FTJfDC4_8upPaO(11{lV ziXGV4<_1;@Av8P(jlpeDxINt2{Ms}9ZXr;Mk4hu(UFw@bC#Ya%g`gJu*r|Mo%p+?M zhhT~!mWOq)(1ZI=%^?yvz>1+B%FtR3m?xliZYIx%MCr+35^dWxSH)*E2MYeU7^^}_ zvu?zdqiJ#YNE%r=TVO;3r{)@F!Q9K`4J{D@oP#-a?O?y>n99$7@$KhK{9nHLgQCOF z-TC8jn{#@%f)IpC8{9HpWm?!97n?OS0i}0N@1%=YF2Ux|2$*t%CN84DB=>=C5lGXk zEi5owJ5757(>=1RoZ)fWS|YOuk($rX3?a(Kw005poX;y{ogp`Iz(4FE0+i2y3+ynq zoToI+AqTL9{e}6I6M8UY4ok!sprQvM+b##v;uyB&YJkNYt&3xNFLDJD)g$l9h!y(_ zRloUQtNg)?>y6ngzL32H^9ugU+kWVriO&=N_E+Q~Jm;6C&gddhsz3n5($c>9C9G;i z;m0b#-$^J$tjyuF_YiK=dioYrZ)2Y5m_)Iy+n%o$5bHPyRE4ewTC`vSgqmC6>K%kw z;ahK_B#nzzhnw7e6m1#=G|4_4DKb8BCuU=&`2jQPMaTw>1q|kM}=U;y-7q7Z+GM z7(l9vWz6$@R2mmHKnomu>X^cy=XWmBZrGcs)Y0NgSDRch%)E@{;CBx+w8EJpfd*^5 zv^fA3t$||l++YBYnBf*Nm>@pmS0PsfWEP-x7O|5!sxV|K1~6YOFzrSUPb|gO_3Jbp z&}ygGpOIOUQr99;`H}7hy4r_Z%Z)`yV1|q<0!#gy=BWto=Ak-IDl1FxER8oSG(L@w wN+$Vsi1~c3HTL_Dum8|y;5&l&XAScI1GmPxG((yQ4FCWD07*qoM6N<$f Date: Sat, 21 Feb 2026 00:09:28 -0500 Subject: [PATCH 3/3] Remove Unneeded stuff from gradle --- build.gradle.kts | 20 -------------------- settings.gradle.kts | 3 +-- 2 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 build.gradle.kts diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index de765f9..0000000 --- a/build.gradle.kts +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id("java") -} - -group = "net.xircon" -version = "1.0-SNAPSHOT" - -repositories { - mavenCentral() -} - -dependencies { - testImplementation(platform("org.junit:junit-bom:5.10.0")) - testImplementation("org.junit.jupiter:junit-jupiter") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") -} - -tasks.test { - useJUnitPlatform() -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f07be67..85d6ef4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,3 @@ rootProject.name = "Xenon" include("server") -include("client") -include("server") \ No newline at end of file +include("client") \ No newline at end of file