diff --git a/src/BoolGrid.hpp b/src/BoolGrid.hpp index 4c1bbfe..3701856 100644 --- a/src/BoolGrid.hpp +++ b/src/BoolGrid.hpp @@ -198,6 +198,8 @@ class BoolGrid */ bool SetData(const container_type& data, int width, int height) { + if (width < 0 || height < 0) + return false; if (data.size() != static_cast(width) * static_cast(height)) return false; m_Width = width; diff --git a/src/ConsoleCommands.cpp b/src/ConsoleCommands.cpp index 91dccba..b3e9db7 100644 --- a/src/ConsoleCommands.cpp +++ b/src/ConsoleCommands.cpp @@ -263,6 +263,37 @@ bool Cmd_TimeSet(std::span args, CommandContext& ctx) return true; } +bool Cmd_TimeAdd(std::span args, CommandContext& ctx) +{ + if (ctx.time == nullptr) + { + ctx.out.PrintError("time.add: time manager unavailable"); + return false; + } + if (args.size() != 1) + { + ctx.out.PrintError("time.add: usage 'time.add '"); + return false; + } + float delta = 0.0f; + if (!ParseFloat(args[0], delta)) + { + ctx.out.PrintError("time.add: hours must be a finite number"); + return false; + } + + ctx.time->AdvanceTime(delta); + + char line[96]; + std::snprintf(line, + sizeof(line), + "time.add: %+.2fh -> %.2fh", + static_cast(delta), + static_cast(ctx.time->GetTimeOfDay())); + ctx.out.Print(line); + return true; +} + bool Cmd_TimeFreeze(std::span args, CommandContext& ctx) { if (ctx.time == nullptr) @@ -3258,6 +3289,14 @@ void Console::RegisterDefaultCommands() }, {"ts"}); + m_Registry.Register("time.add", + "time.add - offset time of day (signed, wraps 0..24)", + [makeContext](auto args, Console&) + { + CommandContext ctx = makeContext(); + (void)Cmd_TimeAdd(args, ctx); + }); + m_Registry.Register("time.freeze", "[on|off|toggle] - pause/resume day-night cycle", [makeContext](auto args, Console&) diff --git a/src/ConsoleCommands.hpp b/src/ConsoleCommands.hpp index 2e17b5f..f3c14a9 100644 --- a/src/ConsoleCommands.hpp +++ b/src/ConsoleCommands.hpp @@ -67,6 +67,7 @@ bool Cmd_Teleport(std::span args, CommandContext& ctx); bool Cmd_FlagSet(std::span args, CommandContext& ctx); bool Cmd_FlagGet(std::span args, CommandContext& ctx); bool Cmd_TimeSet(std::span args, CommandContext& ctx); +bool Cmd_TimeAdd(std::span args, CommandContext& ctx); bool Cmd_TimeFreeze(std::span args, CommandContext& ctx); bool Cmd_MapLoad(std::span args, CommandContext& ctx); bool Cmd_StateDump(std::span args, CommandContext& ctx); diff --git a/src/GameStateManager.cpp b/src/GameStateManager.cpp index ebb67e0..6bf973e 100644 --- a/src/GameStateManager.cpp +++ b/src/GameStateManager.cpp @@ -10,10 +10,8 @@ std::vector GameStateManager::GetActiveQuests() const for (const auto& [key, value] : m_Flags) { - // Match keys that start with "accepted_" and end with "_quest". - // Using ends_with() avoids false-matching keys like "accepted_quest_status". - if (key.starts_with(kAcceptedPrefix) && key.ends_with("_quest") && !value.empty() && - value != "false" && value != "0") + if (key.starts_with(kAcceptedPrefix) && !value.empty() && value != "false" && + value != "0") { // Extract quest name (remove "accepted_" prefix) std::string questName = key.substr(kAcceptedPrefix.size()); diff --git a/tests/CollisionMapTests.cpp b/tests/CollisionMapTests.cpp index 86ac03d..83c0cff 100644 --- a/tests/CollisionMapTests.cpp +++ b/tests/CollisionMapTests.cpp @@ -177,6 +177,15 @@ TEST_F(CollisionMapTest, SetData_InvalidSize_Rejected) EXPECT_EQ(map.GetHeight(), 10); } +TEST_F(CollisionMapTest, SetData_NegativeDimensions_Rejected) +{ + std::vector data; + EXPECT_FALSE(map.SetData(data, -1, 0)); + EXPECT_FALSE(map.SetData(data, 0, -1)); + EXPECT_EQ(map.GetWidth(), 10); + EXPECT_EQ(map.GetHeight(), 10); +} + // --- Copy/Move --- TEST_F(CollisionMapTest, CopyConstructor) diff --git a/tests/ConsoleCommandsTests.cpp b/tests/ConsoleCommandsTests.cpp index e31e1b2..3088ea2 100644 --- a/tests/ConsoleCommandsTests.cpp +++ b/tests/ConsoleCommandsTests.cpp @@ -688,6 +688,40 @@ TEST(ConsoleCommandsTests, TimeNextRejectsArgs) EXPECT_FALSE(Cmd_TimeNext(ArgPack({"now"}).span(), ctx)); } +TEST(ConsoleCommandsTests, TimeAddOffsetsCurrentTime) +{ + TimeManager time; + time.SetTime(14.0f); + ConsoleBuffer buf; + CommandContext ctx{buf}; + ctx.time = &time; + + EXPECT_TRUE(Cmd_TimeAdd(ArgPack({"0.5"}).span(), ctx)); + + EXPECT_FLOAT_EQ(time.GetTimeOfDay(), 14.5f); + EXPECT_TRUE(BufferContains(buf, "time.add: +0.50h -> 14.50h")); +} + +TEST(ConsoleCommandsTests, TimeAddRejectsInvalidInputs) +{ + ConsoleBuffer buf; + CommandContext ctx{buf}; + + EXPECT_FALSE(Cmd_TimeAdd(ArgPack({"1"}).span(), ctx)); + EXPECT_TRUE(BufferContains(buf, "time.add: time manager unavailable")); + + TimeManager time; + ctx.time = &time; + buf.Clear(); + + EXPECT_FALSE(Cmd_TimeAdd(ArgPack({}).span(), ctx)); + EXPECT_TRUE(BufferContains(buf, "time.add: usage 'time.add '")); + + buf.Clear(); + EXPECT_FALSE(Cmd_TimeAdd(ArgPack({"soon"}).span(), ctx)); + EXPECT_TRUE(BufferContains(buf, "time.add: hours must be a finite number")); +} + // --------------------------------------------------------------------------- // character.set / character.next (parse paths only - sprite assets may be // missing in the test working dir, so SwitchCharacter success is best-effort) @@ -1710,8 +1744,6 @@ TEST(ConsoleCommandsTests, QuestCompleteSetsFlag) TEST(ConsoleCommandsTests, QuestListShowsActiveAndCompleted) { - // GetActiveQuests requires the "accepted__quest" naming convention, - // so quest names must end in "_quest" for this code path to recognise them. GameStateManager gs; gs.AcceptQuest("a_quest", "do A"); gs.AcceptQuest("b_quest", "do B"); @@ -1725,6 +1757,19 @@ TEST(ConsoleCommandsTests, QuestListShowsActiveAndCompleted) EXPECT_TRUE(BufferContains(buf, "[DONE]")); } +TEST(ConsoleCommandsTests, QuestListShowsQuestGiveWithoutQuestSuffix) +{ + GameStateManager gs; + ConsoleBuffer buf; + CommandContext ctx{buf}; + ctx.gameState = &gs; + + EXPECT_TRUE(Cmd_QuestGive(ArgPack({"ufo", "investigate"}).span(), ctx)); + EXPECT_TRUE(Cmd_QuestList(ArgPack({}).span(), ctx)); + + EXPECT_TRUE(BufferContains(buf, "[ACTIVE] ufo")); +} + // --------------------------------------------------------------------------- // version / renderer.info / mem.stats / config.dump // ---------------------------------------------------------------------------