#include #ifdef USE_SDL3 #include #else #include #endif #include "engine/assets.hpp" #include "tables/townerdat.hpp" #include "towners.h" #include "utils/paths.h" namespace devilution { namespace { void SetTestAssetsPath() { const std::string assetsPath = paths::BasePath() + "/assets/"; paths::SetAssetsPath(assetsPath); } void InitializeSDL() { #ifdef USE_SDL3 if (!SDL_Init(SDL_INIT_EVENTS)) { // SDL_Init returns 0 on success in SDL3 return; } #elif !defined(USE_SDL1) if (SDL_Init(SDL_INIT_EVENTS) >= 0) { return; } #else if (SDL_Init(0) >= 0) { return; } #endif // If we get here, SDL initialization failed // In tests, we'll continue anyway as file operations might still work } /** * @brief Helper to find a towner data entry by type. */ const TownerDataEntry *FindTownerDataByType(_talker_id type) { for (const auto &entry : TownersDataEntries) { if (entry.type == type) { return &entry; } } return nullptr; } } // namespace TEST(TownerDat, LoadTownerData) { InitializeSDL(); SetTestAssetsPath(); LoadTownerData(); // Verify we loaded the expected number of towners from assets ASSERT_GE(TownersDataEntries.size(), 4u) << "Should load at least 4 towners from assets"; // Check Griswold (TOWN_SMITH) const TownerDataEntry *smith = FindTownerDataByType(TOWN_SMITH); ASSERT_NE(smith, nullptr) << "Should find TOWN_SMITH data"; EXPECT_EQ(smith->type, TOWN_SMITH); EXPECT_EQ(smith->name, "Griswold the Blacksmith"); EXPECT_EQ(smith->position.x, 62); EXPECT_EQ(smith->position.y, 63); EXPECT_EQ(smith->direction, Direction::SouthWest); EXPECT_EQ(smith->animWidth, 96); EXPECT_EQ(smith->animPath, "towners\\smith\\smithn"); EXPECT_EQ(smith->animFrames, 16); EXPECT_EQ(smith->animDelay, 3); EXPECT_EQ(smith->gossipTexts.size(), 11u); EXPECT_EQ(smith->gossipTexts[0], TEXT_GRISWOLD2); EXPECT_EQ(smith->gossipTexts[10], TEXT_GRISWOLD13); ASSERT_GE(smith->animOrder.size(), 4u); EXPECT_EQ(smith->animOrder[0], 4); EXPECT_EQ(smith->animOrder[3], 7); // Check Pepin (TOWN_HEALER) const TownerDataEntry *healer = FindTownerDataByType(TOWN_HEALER); ASSERT_NE(healer, nullptr) << "Should find TOWN_HEALER data"; EXPECT_EQ(healer->type, TOWN_HEALER); EXPECT_EQ(healer->name, "Pepin the Healer"); EXPECT_EQ(healer->position.x, 55); EXPECT_EQ(healer->position.y, 79); EXPECT_EQ(healer->direction, Direction::SouthEast); EXPECT_EQ(healer->animFrames, 20); EXPECT_EQ(healer->gossipTexts.size(), 9u); ASSERT_GE(healer->animOrder.size(), 3u); // Check Dead Guy (TOWN_DEADGUY) - has empty gossip texts and animOrder const TownerDataEntry *deadguy = FindTownerDataByType(TOWN_DEADGUY); ASSERT_NE(deadguy, nullptr) << "Should find TOWN_DEADGUY data"; EXPECT_EQ(deadguy->type, TOWN_DEADGUY); EXPECT_EQ(deadguy->name, "Wounded Townsman"); EXPECT_EQ(deadguy->direction, Direction::North); EXPECT_TRUE(deadguy->gossipTexts.empty()) << "Dead guy should have no gossip texts"; EXPECT_TRUE(deadguy->animOrder.empty()) << "Dead guy should have no custom anim order"; // Check Cow (TOWN_COW) - has empty animPath but animFrames and animDelay are set const TownerDataEntry *cow = FindTownerDataByType(TOWN_COW); ASSERT_NE(cow, nullptr) << "Should find TOWN_COW data"; EXPECT_EQ(cow->type, TOWN_COW); EXPECT_EQ(cow->name, "Cow"); EXPECT_EQ(cow->position.x, 58); EXPECT_EQ(cow->position.y, 16); EXPECT_EQ(cow->direction, Direction::SouthWest); EXPECT_EQ(cow->animWidth, 128); EXPECT_TRUE(cow->animPath.empty()) << "Cow should have empty animPath"; EXPECT_EQ(cow->animFrames, 12); EXPECT_EQ(cow->animDelay, 3); EXPECT_TRUE(cow->gossipTexts.empty()) << "Cow should have no gossip texts"; EXPECT_TRUE(cow->animOrder.empty()) << "Cow should have no custom anim order"; } TEST(TownerDat, LoadQuestDialogTable) { InitializeSDL(); SetTestAssetsPath(); LoadTownerData(); // Check Smith quest dialogs EXPECT_EQ(GetTownerQuestDialog(TOWN_SMITH, Q_BUTCHER), TEXT_BUTCH5); EXPECT_EQ(GetTownerQuestDialog(TOWN_SMITH, Q_LTBANNER), TEXT_BANNER6); EXPECT_EQ(GetTownerQuestDialog(TOWN_SMITH, Q_SKELKING), TEXT_KING7); EXPECT_EQ(GetTownerQuestDialog(TOWN_SMITH, Q_ROCK), TEXT_INFRA6); // Check Healer quest dialogs EXPECT_EQ(GetTownerQuestDialog(TOWN_HEALER, Q_BUTCHER), TEXT_BUTCH3); EXPECT_EQ(GetTownerQuestDialog(TOWN_HEALER, Q_LTBANNER), TEXT_BANNER4); EXPECT_EQ(GetTownerQuestDialog(TOWN_HEALER, Q_SKELKING), TEXT_KING5); // Check Dead guy quest dialogs EXPECT_EQ(GetTownerQuestDialog(TOWN_DEADGUY, Q_BUTCHER), TEXT_NONE); EXPECT_EQ(GetTownerQuestDialog(TOWN_DEADGUY, Q_LTBANNER), TEXT_NONE); } TEST(TownerDat, SetTownerQuestDialog) { InitializeSDL(); SetTestAssetsPath(); LoadTownerData(); // Verify initial value from assets EXPECT_EQ(GetTownerQuestDialog(TOWN_SMITH, Q_MUSHROOM), TEXT_MUSH6); // Modify it SetTownerQuestDialog(TOWN_SMITH, Q_MUSHROOM, TEXT_MUSH1); // Verify it changed EXPECT_EQ(GetTownerQuestDialog(TOWN_SMITH, Q_MUSHROOM), TEXT_MUSH1); // Reset to original value for other tests SetTownerQuestDialog(TOWN_SMITH, Q_MUSHROOM, TEXT_MUSH6); } TEST(TownerDat, GetQuestDialogInvalidType) { InitializeSDL(); SetTestAssetsPath(); LoadTownerData(); // Invalid towner type should return TEXT_NONE // Use a value that's guaranteed to be invalid (beyond enum range) _talker_id invalidType = static_cast<_talker_id>(255); _speech_id result = GetTownerQuestDialog(invalidType, Q_BUTCHER); EXPECT_EQ(result, TEXT_NONE) << "Should return TEXT_NONE for invalid towner type"; } TEST(TownerDat, GetQuestDialogInvalidQuest) { InitializeSDL(); SetTestAssetsPath(); LoadTownerData(); // Invalid quest ID should return TEXT_NONE _speech_id result = GetTownerQuestDialog(TOWN_SMITH, static_cast(-1)); EXPECT_EQ(result, TEXT_NONE) << "Should return TEXT_NONE for invalid quest ID"; result = GetTownerQuestDialog(TOWN_SMITH, static_cast(MAXQUESTS)); EXPECT_EQ(result, TEXT_NONE) << "Should return TEXT_NONE for out-of-range quest ID"; } TEST(TownerDat, TownerLongNamesPopulated) { InitializeSDL(); SetTestAssetsPath(); LoadTownerData(); // Build TownerLongNames as InitTowners() does TownerLongNames.clear(); for (const auto &entry : TownersDataEntries) { TownerLongNames.try_emplace(entry.type, entry.name); } // Verify TownerLongNames is populated correctly EXPECT_FALSE(TownerLongNames.empty()) << "TownerLongNames should not be empty after loading"; // Check specific entries auto smithIt = TownerLongNames.find(TOWN_SMITH); ASSERT_NE(smithIt, TownerLongNames.end()) << "Should find TOWN_SMITH in TownerLongNames"; EXPECT_EQ(smithIt->second, "Griswold the Blacksmith"); auto healerIt = TownerLongNames.find(TOWN_HEALER); ASSERT_NE(healerIt, TownerLongNames.end()) << "Should find TOWN_HEALER in TownerLongNames"; EXPECT_EQ(healerIt->second, "Pepin the Healer"); } TEST(TownerDat, GetNumTownerTypes) { InitializeSDL(); SetTestAssetsPath(); LoadTownerData(); // Build TownerLongNames as InitTowners() does TownerLongNames.clear(); for (const auto &entry : TownersDataEntries) { TownerLongNames.try_emplace(entry.type, entry.name); } // GetNumTownerTypes should return the number of unique towner types size_t numTypes = GetNumTownerTypes(); EXPECT_GT(numTypes, 0u) << "Should have at least one towner type"; EXPECT_EQ(numTypes, TownerLongNames.size()) << "GetNumTownerTypes should match TownerLongNames size"; } TEST(TownerDat, MultipleCowsOnlyOneType) { InitializeSDL(); SetTestAssetsPath(); LoadTownerData(); // Count how many TOWN_COW entries exist in the data size_t cowCount = 0; for (const auto &entry : TownersDataEntries) { if (entry.type == TOWN_COW) { cowCount++; } } // There should be multiple cows but only one type entry EXPECT_GT(cowCount, 1u) << "TSV should have multiple cow entries"; // Build TownerLongNames TownerLongNames.clear(); for (const auto &entry : TownersDataEntries) { TownerLongNames.try_emplace(entry.type, entry.name); } // But only one entry in TownerLongNames for TOWN_COW auto cowIt = TownerLongNames.find(TOWN_COW); ASSERT_NE(cowIt, TownerLongNames.end()) << "Should find TOWN_COW in TownerLongNames"; EXPECT_EQ(cowIt->second, "Cow"); } TEST(TownerDat, QuestDialogOptionalColumns) { InitializeSDL(); SetTestAssetsPath(); LoadTownerData(); // Verify that missing quest columns default to TEXT_NONE // Q_FARMER, Q_GIRL, Q_DEFILER, Q_NAKRUL, Q_CORNSTN, Q_JERSEY may not be in base TSV // but the code should handle them gracefully _speech_id result = GetTownerQuestDialog(TOWN_SMITH, Q_FARMER); // Should be TEXT_NONE since TOWN_SMITH doesn't have farmer quest dialog EXPECT_EQ(result, TEXT_NONE) << "Should return TEXT_NONE for unused quest columns"; } } // namespace devilution