Added server image support.

This commit is contained in:
Andrew Lalis 2021-07-06 22:23:51 +02:00
parent cbcc8b3db1
commit 6df046dfea
12 changed files with 110 additions and 26 deletions

3
.gitignore vendored
View File

@ -4,5 +4,8 @@ core/target/
server/target/ server/target/
server-registry/target/ server-registry/target/
*.iml *.iml
# Server files for testing.
/settings.yaml /settings.yaml
/*.log /*.log
/icon.png

View File

@ -8,6 +8,7 @@ public record PublicServerInfo(
String address, String address,
String description, String description,
String location, String location,
Image icon,
int maxPlayers, int maxPlayers,
int currentPlayers int currentPlayers
) { ) {
@ -18,11 +19,18 @@ public record PublicServerInfo(
panel.add(new JLabel("Address: " + value.address()), BorderLayout.NORTH); 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()); JTextArea descriptionArea = new JTextArea(value.description());
descriptionArea.setEditable(false); descriptionArea.setEditable(false);
descriptionArea.setWrapStyleWord(true); descriptionArea.setWrapStyleWord(true);
descriptionArea.setLineWrap(true); descriptionArea.setLineWrap(true);
panel.add(descriptionArea, BorderLayout.CENTER); content.add(descriptionArea);
panel.add(content, BorderLayout.CENTER);
JPanel bottomPanel = new JPanel(); JPanel bottomPanel = new JPanel();
bottomPanel.add(new JLabel(String.format("Current players: %d / %d", value.currentPlayers(), value.maxPlayers()))); bottomPanel.add(new JLabel(String.format("Current players: %d / %d", value.currentPlayers(), value.maxPlayers())));

View File

@ -2,8 +2,12 @@ package nl.andrewlalis.aos_client.launcher.servers;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import javax.imageio.ImageIO;
import javax.swing.*; import javax.swing.*;
import java.awt.*;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -14,6 +18,7 @@ import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -101,11 +106,17 @@ public class PublicServerListModel extends AbstractListModel<PublicServerInfo> {
this.currentPageItems.clear(); this.currentPageItems.clear();
for (Iterator<JsonNode> it = json.get("contents").elements(); it.hasNext();) { for (Iterator<JsonNode> it = json.get("contents").elements(); it.hasNext();) {
JsonNode node = it.next(); 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( PublicServerInfo info = new PublicServerInfo(
node.get("name").asText(), node.get("name").asText(),
node.get("address").asText(), node.get("address").asText(),
node.get("description").asText(), node.get("description").asText(),
node.get("location").asText(), node.get("location").asText(),
icon,
node.get("maxPlayers").asInt(), node.get("maxPlayers").asInt(),
node.get("currentPlayers").asInt() node.get("currentPlayers").asInt()
); );
@ -143,4 +154,8 @@ public class PublicServerListModel extends AbstractListModel<PublicServerInfo> {
public PublicServerInfo getElementAt(int index) { public PublicServerInfo getElementAt(int index) {
return this.currentPageItems.get(index); return this.currentPageItems.get(index);
} }
public void dispose() {
this.executorService.shutdown();
}
} }

View File

@ -11,9 +11,19 @@ import java.awt.event.MouseEvent;
import java.io.IOException; import java.io.IOException;
public class SearchServersDialog extends JDialog { public class SearchServersDialog extends JDialog {
private final JButton prevButton;
private final JButton nextButton;
private final PublicServerListModel listModel;
public SearchServersDialog(Frame frame, ServerInfoListModel serverInfoListModel) { public SearchServersDialog(Frame frame, ServerInfoListModel serverInfoListModel) {
super(frame, true); super(frame, true);
this.setTitle("Search for Servers"); 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.setContentPane(this.getContent(serverInfoListModel));
this.pack(); this.pack();
this.setLocationRelativeTo(frame); this.setLocationRelativeTo(frame);
@ -22,10 +32,6 @@ public class SearchServersDialog extends JDialog {
private Container getContent(ServerInfoListModel serverInfoListModel) { private Container getContent(ServerInfoListModel serverInfoListModel) {
JPanel panel = new JPanel(new BorderLayout()); JPanel panel = new JPanel(new BorderLayout());
JButton prevButton = new JButton("<");
JButton nextButton = new JButton(">");
PublicServerListModel listModel = new PublicServerListModel(prevButton, nextButton);
JList<PublicServerInfo> serversList = new JList<>(listModel); JList<PublicServerInfo> serversList = new JList<>(listModel);
serversList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); serversList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
serversList.setCellRenderer(PublicServerInfo.cellRenderer()); 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); JOptionPane.showMessageDialog(this, "Could not connect:\n" + e.getMessage(), "Connection Error", JOptionPane.WARNING_MESSAGE);
} }
} }
@Override
public void dispose() {
this.listModel.dispose();
super.dispose();
}
} }

View File

@ -10,12 +10,15 @@ import nl.andrewlalis.aos_server_registry.util.Responses;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -50,7 +53,7 @@ public class ServerInfoServlet extends HttpServlet {
var info = Requests.getBody(req, ServerInfoUpdate.class); var info = Requests.getBody(req, ServerInfoUpdate.class);
try { try {
this.saveNewServer(info); this.saveNewServer(info);
Responses.ok(resp, Map.of("message", "Server info saved.")); Responses.ok(resp, Map.of("message", "Server icon saved."));
} catch (SQLException e) { } catch (SQLException e) {
e.printStackTrace(); e.printStackTrace();
Responses.internalServerError(resp, "Database error."); Responses.internalServerError(resp, "Database error.");
@ -63,11 +66,11 @@ public class ServerInfoServlet extends HttpServlet {
this.updateServerStatus(status, resp); this.updateServerStatus(status, resp);
} }
private List<ServerInfoResponse> getData(int size, int page, String searchQuery, String order, String orderDir) throws SQLException { private List<ServerInfoResponse> getData(int size, int page, String searchQuery, String order, String orderDir) throws SQLException, IOException {
final List<ServerInfoResponse> results = new ArrayList<>(20); final List<ServerInfoResponse> results = new ArrayList<>(20);
var con = DataManager.getInstance().getConnection(); var con = DataManager.getInstance().getConnection();
String selectQuery = """ 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 FROM servers
//CONDITIONS //CONDITIONS
ORDER BY name ORDER BY name
@ -87,14 +90,21 @@ public class ServerInfoServlet extends HttpServlet {
stmt.setInt(index, page * size); stmt.setInt(index, page * size);
ResultSet rs = stmt.executeQuery(); ResultSet rs = stmt.executeQuery();
while (rs.next()) { 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( results.add(new ServerInfoResponse(
rs.getString(1), rs.getString(1),
rs.getString(2), rs.getString(2),
rs.getTimestamp(3).toInstant().atOffset(ZoneOffset.UTC).toString(), rs.getTimestamp(3).toInstant().atOffset(ZoneOffset.UTC).toString(),
rs.getString(4), rs.getString(4),
rs.getString(5), rs.getString(5),
rs.getInt(6), encodedIconImage,
rs.getInt(7) rs.getInt(7),
rs.getInt(8)
)); ));
} }
stmt.close(); stmt.close();
@ -111,30 +121,40 @@ public class ServerInfoServlet extends HttpServlet {
stmt.close(); stmt.close();
if (!exists) { if (!exists) {
PreparedStatement createStmt = con.prepareStatement(""" PreparedStatement createStmt = con.prepareStatement("""
INSERT INTO servers (name, address, description, location, max_players, current_players) INSERT INTO servers (name, address, description, location, icon, max_players, current_players)
VALUES (?, ?, ?, ?, ?, ?); VALUES (?, ?, ?, ?, ?, ?, ?);
"""); """);
createStmt.setString(1, info.name()); createStmt.setString(1, info.name());
createStmt.setString(2, info.address()); createStmt.setString(2, info.address());
createStmt.setString(3, info.description()); createStmt.setString(3, info.description());
createStmt.setString(4, info.location()); createStmt.setString(4, info.location());
createStmt.setInt(5, info.maxPlayers()); InputStream inputStream = null;
createStmt.setInt(6, info.currentPlayers()); 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(); int rowCount = createStmt.executeUpdate();
createStmt.close(); createStmt.close();
if (rowCount != 1) throw new SQLException("Could not insert new server."); if (rowCount != 1) throw new SQLException("Could not insert new server.");
log.info("Registered new server " + info.name() + " @ " + info.address()); log.info("Registered new server " + info.name() + " @ " + info.address());
} else { } else {
PreparedStatement updateStmt = con.prepareStatement(""" 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 = ?; WHERE name = ? AND address = ?;
"""); """);
updateStmt.setString(1, info.description()); updateStmt.setString(1, info.description());
updateStmt.setString(2, info.location()); updateStmt.setString(2, info.location());
updateStmt.setInt(3, info.maxPlayers()); InputStream inputStream = null;
updateStmt.setInt(4, info.currentPlayers()); if (info.icon() != null) {
updateStmt.setString(5, info.name()); inputStream = new ByteArrayInputStream(Base64.getUrlDecoder().decode(info.icon()));
updateStmt.setString(6, info.address()); }
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(); int rowCount = updateStmt.executeUpdate();
updateStmt.close(); updateStmt.close();
if (rowCount != 1) throw new SQLException("Could not update server."); if (rowCount != 1) throw new SQLException("Could not update server.");

View File

@ -6,6 +6,7 @@ public record ServerInfoResponse(
String updatedAt, String updatedAt,
String description, String description,
String location, String location,
String icon,
int maxPlayers, int maxPlayers,
int currentPlayers int currentPlayers
) {} ) {}

View File

@ -5,6 +5,7 @@ public record ServerInfoUpdate (
String address, String address,
String description, String description,
String location, String location,
String icon,
int maxPlayers, int maxPlayers,
int currentPlayers int currentPlayers
) {} ) {}

View File

@ -7,6 +7,7 @@ CREATE TABLE servers (
updated_at TIMESTAMP(0) WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP(0), updated_at TIMESTAMP(0) WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP(0),
description VARCHAR(1024), description VARCHAR(1024),
location VARCHAR(64), location VARCHAR(64),
icon BLOB NULL DEFAULT NULL,
max_players INTEGER NOT NULL, max_players INTEGER NOT NULL,
current_players INTEGER NOT NULL, current_players INTEGER NOT NULL,

View File

@ -2,11 +2,19 @@ package nl.andrewlalis.aos_server;
import com.fasterxml.jackson.databind.ObjectMapper; 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.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.util.Base64;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@ -48,6 +56,7 @@ public class RegistryManager {
data.put("address", this.server.getSettings().getRegistrySettings().getAddress()); data.put("address", this.server.getSettings().getRegistrySettings().getAddress());
data.put("description", this.server.getSettings().getRegistrySettings().getDescription()); data.put("description", this.server.getSettings().getRegistrySettings().getDescription());
data.put("location", this.server.getSettings().getRegistrySettings().getLocation()); data.put("location", this.server.getSettings().getRegistrySettings().getLocation());
data.put("icon", this.getIconData());
data.put("maxPlayers", this.server.getSettings().getMaxPlayers()); data.put("maxPlayers", this.server.getSettings().getMaxPlayers());
data.put("currentPlayers", 0); data.put("currentPlayers", 0);
HttpRequest request = HttpRequest.newBuilder() 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() { public void shutdown() {
this.executorService.shutdown(); this.executorService.shutdown();
try { try {

View File

@ -1,9 +1,5 @@
package nl.andrewlalis.aos_server; 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.geom.Vec2;
import nl.andrewlalis.aos_core.model.*; import nl.andrewlalis.aos_core.model.*;
import nl.andrewlalis.aos_core.model.tools.GunCategory; import nl.andrewlalis.aos_core.model.tools.GunCategory;
@ -26,9 +22,7 @@ import java.net.Socket;
import java.net.SocketException; import java.net.SocketException;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class Server { public class Server {
private final ServerSettings settings; private final ServerSettings settings;

View File

@ -11,6 +11,10 @@ import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
/**
* Utility class that's responsible for loading the settings from their usual
* location,
*/
public class SettingsLoader { public class SettingsLoader {
public static ServerSettings load() throws IOException { public static ServerSettings load() throws IOException {
Path settingsFile = Path.of("settings.yaml"); Path settingsFile = Path.of("settings.yaml");

View File

@ -28,6 +28,8 @@ registry-settings:
description: "A simple testing server for development." description: "A simple testing server for development."
# Location of this server, to help players choose servers near to them. # Location of this server, to help players choose servers near to them.
location: "Earth" 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. # Settings that control player behavior.