From 6eabebff0085fe80f41aef4ac2346cf32436536e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Mon, 1 Jun 2026 17:28:32 +0200 Subject: [PATCH] fix(combinations): guard null response; reject self-combination at load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Combinations.tryUse: only write response when non-null and non-blank, preventing NPE/IllegalArgument for effects-only recipes. - CombinationFactory.fromDto: throw WorldLoadException when a == b, since item singletons make a self-combination impossible at runtime. - Tests: +effectsOnlyRecipeAppliesEffectsWithoutProduceOrResponse (CombinationsTest 7→8), +rejectsSelfCombination (CombinationFactoryTest 4→5). Co-Authored-By: Claude Sonnet 4.6 --- .../thb/jeanluc/adventure/game/Combinations.java | 4 +++- .../adventure/loader/CombinationFactory.java | 4 ++++ .../jeanluc/adventure/game/CombinationsTest.java | 14 ++++++++++++++ .../adventure/loader/CombinationFactoryTest.java | 8 ++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Combinations.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Combinations.java index d34108a..01f5ed1 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Combinations.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/game/Combinations.java @@ -47,7 +47,9 @@ public final class Combinations { } } Effects.applyAll(c.effects(), ctx); - ctx.getIo().write(c.response()); + if (c.response() != null && !c.response().isBlank()) { + ctx.getIo().write(c.response()); + } } private static boolean present(GameContext ctx, String id) { diff --git a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/CombinationFactory.java b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/CombinationFactory.java index 81234b4..296b4bd 100644 --- a/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/CombinationFactory.java +++ b/Semesterprojekt/src/main/java/thb/jeanluc/adventure/loader/CombinationFactory.java @@ -18,6 +18,10 @@ public final class CombinationFactory { public static Combination fromDto(CombinationDto dto, Map items) { requireItem(items, dto.a(), "a"); requireItem(items, dto.b(), "b"); + if (dto.a().equals(dto.b())) { + throw new WorldLoadException( + "Combination cannot combine an item with itself: '" + dto.a() + "'"); + } if (dto.consume() != null) { for (String id : dto.consume()) { requireItem(items, id, "consume"); diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/CombinationsTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/CombinationsTest.java index 16fb1e6..910446e 100644 --- a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/CombinationsTest.java +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/game/CombinationsTest.java @@ -123,6 +123,20 @@ class CombinationsTest { assertThat(ctx.getPlayer().hasItem("matches")).isTrue(); // not consumed } + @Test + void effectsOnlyRecipeAppliesEffectsWithoutProduceOrResponse() { + Combination effectsOnly = new Combination("matches", "shovel", + java.util.List.of(), java.util.List.of(), null, + java.util.List.of(new Effect(Effect.Type.SET_FLAG, "rite_done")), + null, null); + GameContext ctx = ctx(effectsOnly); + give(ctx, "matches"); + give(ctx, "shovel"); + Combinations.tryUse(ctx, "matches", "shovel"); + assertThat(ctx.getState().isSet("rite_done")).isTrue(); + assertThat(out(ctx)).doesNotContain("null"); + } + @Test void metRequiresAppliesEffectsAndProduces() { Combination gated = new Combination("matches", "torch", diff --git a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/CombinationFactoryTest.java b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/CombinationFactoryTest.java index 61cd55c..afa1723 100644 --- a/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/CombinationFactoryTest.java +++ b/Semesterprojekt/src/test/java/thb/jeanluc/adventure/loader/CombinationFactoryTest.java @@ -58,4 +58,12 @@ class CombinationFactoryTest { .isInstanceOf(WorldLoadException.class) .hasMessageContaining("wraith"); } + + @Test + void rejectsSelfCombination() { + CombinationDto dto = new CombinationDto("torch", "torch", null, null, null, null, "x", null); + assertThatThrownBy(() -> CombinationFactory.fromDto(dto, items())) + .isInstanceOf(WorldLoadException.class) + .hasMessageContaining("itself"); + } }