From 182a6911a0f039005747aecbda03e38d36ac5293 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 1 Jun 2026 15:39:08 +0200 Subject: [PATCH] fix: EOF ends game loop (ConsoleIO returns null on EOF); settings.json excluded from save slots Co-Authored-By: Claude Opus 4.8 (1M context) --- .../thb/jeanluc/adventure/io/ConsoleIO.java | 7 ++++--- .../jeanluc/adventure/save/SaveService.java | 13 +++++++++++-- .../adventure/io/ConsoleIORenderTest.java | 19 +++++++++++++++++++ .../adventure/save/SaveServiceTest.java | 17 +++++++++++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/ConsoleIO.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/ConsoleIO.java index aaf7c26..ec1afc6 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/ConsoleIO.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/io/ConsoleIO.java @@ -71,10 +71,11 @@ public class ConsoleIO implements GameIO { out.print("> "); out.flush(); try { - String line = in.readLine(); - return line == null ? "" : line; + // null propagates as true EOF (ends the game loop); "" stays "" (blank input re-prompts). + return in.readLine(); } catch (IOException e) { - return ""; + // Treat a stream failure as EOF so input ends cleanly rather than spinning. + return null; } } diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/save/SaveService.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/save/SaveService.java index b5cf690..f812d4f 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/save/SaveService.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/save/SaveService.java @@ -23,6 +23,8 @@ import java.util.stream.Stream; @Slf4j public class SaveService { + private static final String SETTINGS_FILENAME = "settings.json"; + private final Path dir; private final ObjectMapper mapper = new ObjectMapper(); @@ -38,7 +40,11 @@ public class SaveService { /** Slug used for the on-disk filename of a slot name. */ public static String slug(String slotName) { String s = slotName.trim().toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]+", "_"); - return s.isBlank() ? "save" : s; + if (s.isBlank()) { + return "save"; + } + // Never let a user-named save collide with the settings file. + return s.equals("settings") ? "settings_" : s; } /** Writes the session to {@code .json} atomically. */ @@ -71,7 +77,10 @@ public class SaveService { } List out = new ArrayList<>(); try (Stream files = Files.list(dir)) { - for (Path f : files.filter(p -> p.toString().endsWith(".json")).toList()) { + for (Path f : files + .filter(p -> p.toString().endsWith(".json")) + .filter(p -> !p.getFileName().toString().equals(SETTINGS_FILENAME)) + .toList()) { try { SaveData d = mapper.readValue(f.toFile(), SaveData.class); String slug = f.getFileName().toString().replaceFirst("\\.json$", ""); diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/ConsoleIORenderTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/ConsoleIORenderTest.java index 21735b1..19db2e3 100644 --- a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/ConsoleIORenderTest.java +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/io/ConsoleIORenderTest.java @@ -50,6 +50,25 @@ class ConsoleIORenderTest { assertThat(out).contains("Kitchen").contains("turn 7").contains("light: off"); } + @Test + void readLine_returnsNullOnEof() { + ByteArrayOutputStream sink = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(sink, true, StandardCharsets.UTF_8); + ConsoleIO io = new ConsoleIO(new BufferedReader(new StringReader("")), ps, + ConsoleIO.ColorMode.OFF, ConsoleIO.GlyphMode.UNICODE); + assertThat(io.readLine()).isNull(); + } + + @Test + void readLine_returnsEmptyStringForBlankLineThenNullAtEof() { + ByteArrayOutputStream sink = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(sink, true, StandardCharsets.UTF_8); + ConsoleIO io = new ConsoleIO(new BufferedReader(new StringReader("\n")), ps, + ConsoleIO.ColorMode.OFF, ConsoleIO.GlyphMode.UNICODE); + assertThat(io.readLine()).isEqualTo(""); // blank interactive line, re-prompt + assertThat(io.readLine()).isNull(); // stream now exhausted → EOF + } + @Test void asciiGlyphMode_usesPlusDashFrame() { ByteArrayOutputStream sink = new ByteArrayOutputStream(); diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/save/SaveServiceTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/save/SaveServiceTest.java index e321a87..f5aba96 100644 --- a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/save/SaveServiceTest.java +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/save/SaveServiceTest.java @@ -55,6 +55,23 @@ class SaveServiceTest { assertThat(new SaveService(dir).list()).isEmpty(); } + @Test + void listExcludesSettingsFile(@TempDir Path dir) throws Exception { + SaveService svc = new SaveService(dir); + GameSession s = freshSession("Real Save"); + svc.save(s); + Files.writeString(dir.resolve("settings.json"), "{\"color\":\"on\"}"); + + List slots = svc.list(); + assertThat(slots).hasSize(1); + assertThat(slots.getFirst().slotName()).isEqualTo("Real Save"); + } + + @Test + void slugForSettingsDoesNotCollideWithSettingsFile() { + assertThat(SaveService.slug("settings")).isNotEqualTo("settings"); + } + @Test void loadCorruptFileThrowsSaveException(@TempDir Path dir) throws Exception { Files.writeString(dir.resolve("bad.json"), "{ not json");