Merge branch 'feat/highscore-server-storage' into 'main'
Feat: Add GET_HIGHSCORES and CLEAR_HIGHSCORES server commands (Issue #110) See merge request cs108-fs26/Gruppe-13!135
This commit was merged in pull request #291.
This commit is contained in:
@@ -27,6 +27,12 @@ import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_message_count.Get
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.get_next_message.GetNextMessageRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores.ClearHighscoresHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores.ClearHighscoresParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores.ClearHighscoresRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores.GetHighscoresRequest;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.app.commands.list_users.ListUsersRequest;
|
||||
@@ -249,6 +255,18 @@ public class ServerApp {
|
||||
new GetGameStateHandler(
|
||||
responseDispatcher, context.lobbyManager(), userRegistry));
|
||||
|
||||
parserDispatcher.register("GET_HIGHSCORES", new GetHighscoresParser());
|
||||
commandRouter.register(
|
||||
GetHighscoresRequest.class,
|
||||
(CommandHandler<GetHighscoresRequest>)
|
||||
new GetHighscoresHandler(responseDispatcher));
|
||||
|
||||
parserDispatcher.register("CLEAR_HIGHSCORES", new ClearHighscoresParser());
|
||||
commandRouter.register(
|
||||
ClearHighscoresRequest.class,
|
||||
(CommandHandler<ClearHighscoresRequest>)
|
||||
new ClearHighscoresHandler(responseDispatcher));
|
||||
|
||||
// BET registration
|
||||
parserDispatcher.register("BET", new PlayerBetParser());
|
||||
commandRouter.register(
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.OkResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
|
||||
/** Handler for CLEAR_HIGHSCORES. */
|
||||
public class ClearHighscoresHandler extends CommandHandler<ClearHighscoresRequest> {
|
||||
private final HighscoreService highscoreService;
|
||||
|
||||
public ClearHighscoresHandler(ResponseDispatcher responseDispatcher) {
|
||||
super(responseDispatcher);
|
||||
this.highscoreService = HighscoreService.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(ClearHighscoresRequest request) {
|
||||
highscoreService.clearAll();
|
||||
responseDispatcher.dispatch(new OkResponse(request.getContext()));
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
|
||||
/** Parser for CLEAR_HIGHSCORES. */
|
||||
public class ClearHighscoresParser implements CommandParser<ClearHighscoresRequest> {
|
||||
@Override
|
||||
public ClearHighscoresRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
return new ClearHighscoresRequest(primitiveRequest.context());
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.clear_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/** Request object for CLEAR_HIGHSCORES. */
|
||||
public class ClearHighscoresRequest extends Request {
|
||||
public ClearHighscoresRequest(RequestContext context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore.HighscoreService;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.execution.CommandHandler;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.dispatcher.ResponseDispatcher;
|
||||
|
||||
/** Handler for GET_HIGHSCORES. */
|
||||
public class GetHighscoresHandler extends CommandHandler<GetHighscoresRequest> {
|
||||
private final HighscoreService highscoreService;
|
||||
|
||||
public GetHighscoresHandler(ResponseDispatcher responseDispatcher) {
|
||||
super(responseDispatcher);
|
||||
this.highscoreService = HighscoreService.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(GetHighscoresRequest request) {
|
||||
responseDispatcher.dispatch(
|
||||
new GetHighscoresResponse(
|
||||
request.getContext(), highscoreService.readFormattedEntries()));
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.command.parsing.CommandParser;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.PrimitiveRequest;
|
||||
|
||||
/** Parser for GET_HIGHSCORES. */
|
||||
public class GetHighscoresParser implements CommandParser<GetHighscoresRequest> {
|
||||
@Override
|
||||
public GetHighscoresRequest parse(PrimitiveRequest primitiveRequest) {
|
||||
return new GetHighscoresRequest(primitiveRequest.context());
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.Request;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
|
||||
/** Request object for GET_HIGHSCORES. */
|
||||
public class GetHighscoresRequest extends Request {
|
||||
public GetHighscoresRequest(RequestContext context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.app.commands.highscore.get_highscores;
|
||||
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.request.RequestContext;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.SuccessResponse;
|
||||
import ch.unibas.dmi.dbis.cs108.casono.server.network.protocol.response.builder.ResponseBody;
|
||||
import java.util.List;
|
||||
|
||||
/** Success response containing repeated HIGHSCORE parameters. */
|
||||
public class GetHighscoresResponse extends SuccessResponse {
|
||||
public GetHighscoresResponse(RequestContext context, List<String> entries) {
|
||||
super(
|
||||
context,
|
||||
ResponseBody.builder()
|
||||
.block(
|
||||
"HIGHSCORES",
|
||||
block -> {
|
||||
for (String entry : entries) {
|
||||
block.param("HIGHSCORE", entry);
|
||||
}
|
||||
})
|
||||
.build());
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package ch.unibas.dmi.dbis.cs108.casono.server.domain.highscore;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Stores and reads persistent highscore entries in timestamp + winner format. */
|
||||
public final class HighscoreService {
|
||||
|
||||
private static final HighscoreService INSTANCE = new HighscoreService();
|
||||
private static final DateTimeFormatter DISPLAY_FORMATTER =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
|
||||
private static final int MAX_RETURNED_ENTRIES = 100;
|
||||
private static final String LINE_SEPARATOR = "\t";
|
||||
|
||||
private final Path storagePath;
|
||||
|
||||
private HighscoreService() {
|
||||
this.storagePath =
|
||||
Path.of(System.getProperty("user.home"), ".casono", "highscores", "winners.log");
|
||||
}
|
||||
|
||||
public static HighscoreService getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public synchronized void appendWinner(String winnerName) {
|
||||
if (winnerName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String sanitized = sanitizeName(winnerName);
|
||||
if (sanitized.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String line = Instant.now() + LINE_SEPARATOR + sanitized + System.lineSeparator();
|
||||
|
||||
try {
|
||||
Files.createDirectories(storagePath.getParent());
|
||||
Files.writeString(
|
||||
storagePath,
|
||||
line,
|
||||
StandardCharsets.UTF_8,
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND);
|
||||
} catch (IOException ignored) {
|
||||
// Highscore persistence must never break the game state response path.
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized List<String> readFormattedEntries() {
|
||||
if (!Files.exists(storagePath)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> lines;
|
||||
try {
|
||||
lines = Files.readAllLines(storagePath, StandardCharsets.UTF_8);
|
||||
} catch (IOException ignored) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> formatted = new ArrayList<>();
|
||||
|
||||
for (String raw : lines) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] parts = raw.split(LINE_SEPARATOR, 2);
|
||||
if (parts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Instant ts = Instant.parse(parts[0].trim());
|
||||
String display = DISPLAY_FORMATTER.format(ts) + " | " + parts[1].trim();
|
||||
formatted.add(display);
|
||||
} catch (Exception ignored) {
|
||||
// Skip malformed lines and keep all valid entries.
|
||||
}
|
||||
}
|
||||
|
||||
int from = Math.max(0, formatted.size() - MAX_RETURNED_ENTRIES);
|
||||
return new ArrayList<>(formatted.subList(from, formatted.size()));
|
||||
}
|
||||
|
||||
public synchronized void clearAll() {
|
||||
try {
|
||||
Files.deleteIfExists(storagePath);
|
||||
} catch (IOException ignored) {
|
||||
// Clearing highscores must not break request handling.
|
||||
}
|
||||
}
|
||||
|
||||
private static String sanitizeName(String winnerName) {
|
||||
return winnerName.trim().replace("\n", " ").replace("\r", " ").replace("\t", " ");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user