diff --git a/.gitignore b/.gitignore index e440c55..f3e147d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,8 @@ core/target/ server/target/ server-registry/target/ *.iml + +# Server files for testing. /settings.yaml -/*.log \ No newline at end of file +/*.log +/icon.png diff --git a/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/PublicServerInfo.java b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/PublicServerInfo.java index 24fc638..6d3f05e 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/PublicServerInfo.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/PublicServerInfo.java @@ -8,6 +8,7 @@ public record PublicServerInfo( String address, String description, String location, + Image icon, int maxPlayers, int currentPlayers ) { @@ -18,11 +19,18 @@ public record PublicServerInfo( panel.add(new JLabel("Address: " + value.address()), BorderLayout.NORTH); + JPanel content = new JPanel(); + if (value.icon() != null) { + JLabel iconLabel = new JLabel(new ImageIcon(value.icon())); + content.add(iconLabel); + } + JTextArea descriptionArea = new JTextArea(value.description()); descriptionArea.setEditable(false); descriptionArea.setWrapStyleWord(true); descriptionArea.setLineWrap(true); - panel.add(descriptionArea, BorderLayout.CENTER); + content.add(descriptionArea); + panel.add(content, BorderLayout.CENTER); JPanel bottomPanel = new JPanel(); bottomPanel.add(new JLabel(String.format("Current players: %d / %d", value.currentPlayers(), value.maxPlayers()))); diff --git a/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/PublicServerListModel.java b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/PublicServerListModel.java index 4411a17..76eea6a 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/PublicServerListModel.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/PublicServerListModel.java @@ -2,8 +2,12 @@ package nl.andrewlalis.aos_client.launcher.servers; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.JsonNodeType; +import javax.imageio.ImageIO; import javax.swing.*; +import java.awt.*; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -14,6 +18,7 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; +import java.util.Base64; import java.util.Iterator; import java.util.List; import java.util.concurrent.Executors; @@ -101,11 +106,17 @@ public class PublicServerListModel extends AbstractListModel { this.currentPageItems.clear(); for (Iterator it = json.get("contents").elements(); it.hasNext();) { JsonNode node = it.next(); + Image icon = null; + JsonNode iconNode = node.get("icon"); + if (iconNode != null && iconNode.getNodeType() == JsonNodeType.STRING) { + icon = ImageIO.read(new ByteArrayInputStream(Base64.getUrlDecoder().decode(iconNode.textValue()))); + } PublicServerInfo info = new PublicServerInfo( node.get("name").asText(), node.get("address").asText(), node.get("description").asText(), node.get("location").asText(), + icon, node.get("maxPlayers").asInt(), node.get("currentPlayers").asInt() ); @@ -143,4 +154,8 @@ public class PublicServerListModel extends AbstractListModel { public PublicServerInfo getElementAt(int index) { return this.currentPageItems.get(index); } + + public void dispose() { + this.executorService.shutdown(); + } } diff --git a/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/SearchServersDialog.java b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/SearchServersDialog.java index f0699d9..a757b11 100644 --- a/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/SearchServersDialog.java +++ b/client/src/main/java/nl/andrewlalis/aos_client/launcher/servers/SearchServersDialog.java @@ -11,9 +11,19 @@ import java.awt.event.MouseEvent; import java.io.IOException; public class SearchServersDialog extends JDialog { + private final JButton prevButton; + private final JButton nextButton; + private final PublicServerListModel listModel; + public SearchServersDialog(Frame frame, ServerInfoListModel serverInfoListModel) { super(frame, true); this.setTitle("Search for Servers"); + this.setDefaultCloseOperation(DISPOSE_ON_CLOSE); + + this.prevButton = new JButton("<"); + this.nextButton = new JButton(">"); + this.listModel = new PublicServerListModel(prevButton, nextButton); + this.setContentPane(this.getContent(serverInfoListModel)); this.pack(); this.setLocationRelativeTo(frame); @@ -22,10 +32,6 @@ public class SearchServersDialog extends JDialog { private Container getContent(ServerInfoListModel serverInfoListModel) { JPanel panel = new JPanel(new BorderLayout()); - JButton prevButton = new JButton("<"); - JButton nextButton = new JButton(">"); - - PublicServerListModel listModel = new PublicServerListModel(prevButton, nextButton); JList serversList = new JList<>(listModel); serversList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); serversList.setCellRenderer(PublicServerInfo.cellRenderer()); @@ -96,4 +102,10 @@ public class SearchServersDialog extends JDialog { JOptionPane.showMessageDialog(this, "Could not connect:\n" + e.getMessage(), "Connection Error", JOptionPane.WARNING_MESSAGE); } } + + @Override + public void dispose() { + this.listModel.dispose(); + super.dispose(); + } } diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/ServerInfoServlet.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/ServerInfoServlet.java index 2ec6b12..06e86f2 100644 --- a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/ServerInfoServlet.java +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/ServerInfoServlet.java @@ -10,12 +10,15 @@ import nl.andrewlalis.aos_server_registry.util.Responses; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.Map; import java.util.logging.Logger; @@ -50,7 +53,7 @@ public class ServerInfoServlet extends HttpServlet { var info = Requests.getBody(req, ServerInfoUpdate.class); try { this.saveNewServer(info); - Responses.ok(resp, Map.of("message", "Server info saved.")); + Responses.ok(resp, Map.of("message", "Server icon saved.")); } catch (SQLException e) { e.printStackTrace(); Responses.internalServerError(resp, "Database error."); @@ -63,11 +66,11 @@ public class ServerInfoServlet extends HttpServlet { this.updateServerStatus(status, resp); } - private List getData(int size, int page, String searchQuery, String order, String orderDir) throws SQLException { + private List getData(int size, int page, String searchQuery, String order, String orderDir) throws SQLException, IOException { final List results = new ArrayList<>(20); var con = DataManager.getInstance().getConnection(); String selectQuery = """ - SELECT name, address, updated_at, description, location, max_players, current_players + SELECT name, address, updated_at, description, location, icon, max_players, current_players FROM servers //CONDITIONS ORDER BY name @@ -87,14 +90,21 @@ public class ServerInfoServlet extends HttpServlet { stmt.setInt(index, page * size); ResultSet rs = stmt.executeQuery(); while (rs.next()) { + // Attempt to load the server's icon, if it is not null. + InputStream iconInputStream = rs.getBinaryStream(6); + String encodedIconImage = null; + if (iconInputStream != null) { + encodedIconImage = Base64.getUrlEncoder().encodeToString(iconInputStream.readAllBytes()); + } results.add(new ServerInfoResponse( rs.getString(1), rs.getString(2), rs.getTimestamp(3).toInstant().atOffset(ZoneOffset.UTC).toString(), rs.getString(4), rs.getString(5), - rs.getInt(6), - rs.getInt(7) + encodedIconImage, + rs.getInt(7), + rs.getInt(8) )); } stmt.close(); @@ -111,30 +121,40 @@ public class ServerInfoServlet extends HttpServlet { stmt.close(); if (!exists) { PreparedStatement createStmt = con.prepareStatement(""" - INSERT INTO servers (name, address, description, location, max_players, current_players) - VALUES (?, ?, ?, ?, ?, ?); + INSERT INTO servers (name, address, description, location, icon, max_players, current_players) + VALUES (?, ?, ?, ?, ?, ?, ?); """); createStmt.setString(1, info.name()); createStmt.setString(2, info.address()); createStmt.setString(3, info.description()); createStmt.setString(4, info.location()); - createStmt.setInt(5, info.maxPlayers()); - createStmt.setInt(6, info.currentPlayers()); + InputStream inputStream = null; + if (info.icon() != null) { + inputStream = new ByteArrayInputStream(Base64.getUrlDecoder().decode(info.icon())); + } + createStmt.setBinaryStream(5, inputStream); + createStmt.setInt(6, info.maxPlayers()); + createStmt.setInt(7, info.currentPlayers()); int rowCount = createStmt.executeUpdate(); createStmt.close(); if (rowCount != 1) throw new SQLException("Could not insert new server."); log.info("Registered new server " + info.name() + " @ " + info.address()); } else { PreparedStatement updateStmt = con.prepareStatement(""" - UPDATE servers SET description = ?, location = ?, max_players = ?, current_players = ? + UPDATE servers SET description = ?, location = ?, icon = ?, max_players = ?, current_players = ? WHERE name = ? AND address = ?; """); updateStmt.setString(1, info.description()); updateStmt.setString(2, info.location()); - updateStmt.setInt(3, info.maxPlayers()); - updateStmt.setInt(4, info.currentPlayers()); - updateStmt.setString(5, info.name()); - updateStmt.setString(6, info.address()); + InputStream inputStream = null; + if (info.icon() != null) { + inputStream = new ByteArrayInputStream(Base64.getUrlDecoder().decode(info.icon())); + } + updateStmt.setBinaryStream(3, inputStream); + updateStmt.setInt(4, info.maxPlayers()); + updateStmt.setInt(5, info.currentPlayers()); + updateStmt.setString(6, info.name()); + updateStmt.setString(7, info.address()); int rowCount = updateStmt.executeUpdate(); updateStmt.close(); if (rowCount != 1) throw new SQLException("Could not update server."); diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoResponse.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoResponse.java index eb27412..6fd82d9 100644 --- a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoResponse.java +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoResponse.java @@ -6,6 +6,7 @@ public record ServerInfoResponse( String updatedAt, String description, String location, + String icon, int maxPlayers, int currentPlayers ) {} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoUpdate.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoUpdate.java index 7dce69f..0c0af2c 100644 --- a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoUpdate.java +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoUpdate.java @@ -5,6 +5,7 @@ public record ServerInfoUpdate ( String address, String description, String location, + String icon, int maxPlayers, int currentPlayers ) {} diff --git a/server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/schema.sql b/server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/schema.sql index 7abbfbe..8e237a7 100644 --- a/server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/schema.sql +++ b/server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/schema.sql @@ -7,6 +7,7 @@ CREATE TABLE servers ( updated_at TIMESTAMP(0) WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP(0), description VARCHAR(1024), location VARCHAR(64), + icon BLOB NULL DEFAULT NULL, max_players INTEGER NOT NULL, current_players INTEGER NOT NULL, diff --git a/server/src/main/java/nl/andrewlalis/aos_server/RegistryManager.java b/server/src/main/java/nl/andrewlalis/aos_server/RegistryManager.java index 021999a..8d94b8b 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/RegistryManager.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/RegistryManager.java @@ -2,11 +2,19 @@ package nl.andrewlalis.aos_server; import com.fasterxml.jackson.databind.ObjectMapper; +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; +import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Executors; @@ -48,6 +56,7 @@ public class RegistryManager { data.put("address", this.server.getSettings().getRegistrySettings().getAddress()); data.put("description", this.server.getSettings().getRegistrySettings().getDescription()); data.put("location", this.server.getSettings().getRegistrySettings().getLocation()); + data.put("icon", this.getIconData()); data.put("maxPlayers", this.server.getSettings().getMaxPlayers()); data.put("currentPlayers", 0); HttpRequest request = HttpRequest.newBuilder() @@ -84,6 +93,20 @@ public class RegistryManager { } } + private String getIconData() throws IOException { + Path iconFile = Path.of("icon.png"); + if (Files.exists(iconFile)) { + byte[] imageBytes = Files.readAllBytes(iconFile); + BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageBytes)); + if (image.getWidth() == 64 && image.getHeight() == 64) { + return Base64.getUrlEncoder().encodeToString(imageBytes); + } else { + System.err.println("icon.png must be 64 x 64."); + } + } + return null; + } + public void shutdown() { this.executorService.shutdown(); try { diff --git a/server/src/main/java/nl/andrewlalis/aos_server/Server.java b/server/src/main/java/nl/andrewlalis/aos_server/Server.java index 7ca0d7f..30690a9 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/Server.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/Server.java @@ -1,9 +1,5 @@ package nl.andrewlalis.aos_server; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import nl.andrewlalis.aos_core.geom.Vec2; import nl.andrewlalis.aos_core.model.*; import nl.andrewlalis.aos_core.model.tools.GunCategory; @@ -26,9 +22,7 @@ import java.net.Socket; import java.net.SocketException; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.TimeUnit; public class Server { private final ServerSettings settings; diff --git a/server/src/main/java/nl/andrewlalis/aos_server/settings/SettingsLoader.java b/server/src/main/java/nl/andrewlalis/aos_server/settings/SettingsLoader.java index be42436..594370d 100644 --- a/server/src/main/java/nl/andrewlalis/aos_server/settings/SettingsLoader.java +++ b/server/src/main/java/nl/andrewlalis/aos_server/settings/SettingsLoader.java @@ -11,6 +11,10 @@ import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +/** + * Utility class that's responsible for loading the settings from their usual + * location, + */ public class SettingsLoader { public static ServerSettings load() throws IOException { Path settingsFile = Path.of("settings.yaml"); diff --git a/server/src/main/resources/default_settings.yaml b/server/src/main/resources/default_settings.yaml index 8edecc7..c601f57 100644 --- a/server/src/main/resources/default_settings.yaml +++ b/server/src/main/resources/default_settings.yaml @@ -28,6 +28,8 @@ registry-settings: description: "A simple testing server for development." # Location of this server, to help players choose servers near to them. location: "Earth" + # Note: To set an icon for this server, add an "icon.png" image to the server's directory (where the settings are). + # The icon MUST be 64x64 pixels in size. # Settings that control player behavior.