Skip to content

Commit aaa46a4

Browse files
committed
Make towners initialization more dynamic
1 parent bb6ed0a commit aaa46a4

File tree

7 files changed

+209
-88
lines changed

7 files changed

+209
-88
lines changed

Source/lua/modules/towners.cpp

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "lua/modules/towners.hpp"
22

33
#include <optional>
4+
#include <unordered_map>
45
#include <utility>
56

67
#include <sol/sol.hpp>
@@ -13,20 +14,21 @@
1314
namespace devilution {
1415
namespace {
1516

16-
const char *const TownerTableNames[NUM_TOWNER_TYPES] {
17-
"griswold",
18-
"pepin",
19-
"deadguy",
20-
"ogden",
21-
"cain",
22-
"farnham",
23-
"adria",
24-
"gillian",
25-
"wirt",
26-
"cow",
27-
"lester",
28-
"celia",
29-
"nut",
17+
// Map from towner type enum to Lua table name
18+
const std::unordered_map<_talker_id, const char *> TownerTableNames = {
19+
{ TOWN_SMITH, "griswold" },
20+
{ TOWN_HEALER, "pepin" },
21+
{ TOWN_DEADGUY, "deadguy" },
22+
{ TOWN_TAVERN, "ogden" },
23+
{ TOWN_STORY, "cain" },
24+
{ TOWN_DRUNK, "farnham" },
25+
{ TOWN_WITCH, "adria" },
26+
{ TOWN_BMAID, "gillian" },
27+
{ TOWN_PEGBOY, "wirt" },
28+
{ TOWN_COW, "cow" },
29+
{ TOWN_FARMER, "lester" },
30+
{ TOWN_GIRL, "celia" },
31+
{ TOWN_COWFARM, "nut" },
3032
};
3133

3234
void PopulateTownerTable(_talker_id townerId, sol::table &out)
@@ -44,10 +46,15 @@ void PopulateTownerTable(_talker_id townerId, sol::table &out)
4446
sol::table LuaTownersModule(sol::state_view &lua)
4547
{
4648
sol::table table = lua.create_table();
47-
for (uint8_t townerId = TOWN_SMITH; townerId < NUM_TOWNER_TYPES; ++townerId) {
49+
// Iterate over all towner types found in TSV data
50+
for (const auto &[townerId, name] : TownerLongNames) {
51+
auto tableNameIt = TownerTableNames.find(townerId);
52+
if (tableNameIt == TownerTableNames.end())
53+
continue; // Skip if no table name mapping
54+
4855
sol::table townerTable = lua.create_table();
49-
PopulateTownerTable(static_cast<_talker_id>(townerId), townerTable);
50-
LuaSetDoc(table, TownerTableNames[townerId], /*signature=*/"", TownerLongNames[townerId], std::move(townerTable));
56+
PopulateTownerTable(townerId, townerTable);
57+
LuaSetDoc(table, tableNameIt->second, /*signature=*/"", name.c_str(), std::move(townerTable));
5158
}
5259
return table;
5360
}

Source/msg.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1953,7 +1953,7 @@ size_t OnTalkXY(const TCmdLocParam1 &message, Player &player)
19531953
const Point position { message.x, message.y };
19541954
const uint16_t townerIdx = Swap16LE(message.wParam1);
19551955

1956-
if (gbBufferMsgs != 1 && player.isOnActiveLevel() && InDungeonBounds(position) && townerIdx < NUM_TOWNERS) {
1956+
if (gbBufferMsgs != 1 && player.isOnActiveLevel() && InDungeonBounds(position) && townerIdx < GetNumTowners()) {
19571957
MakePlrPath(player, position, false);
19581958
player.destAction = ACTION_TALK;
19591959
player.destParam1 = townerIdx;

Source/townerdat.cpp

Lines changed: 26 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
namespace devilution {
2121

2222
std::vector<TownerDataEntry> TownersDataEntries;
23-
std::vector<std::array<_speech_id, MAXQUESTS>> TownerQuestDialogTable;
23+
std::unordered_map<_talker_id, std::array<_speech_id, MAXQUESTS>> TownerQuestDialogTable;
2424

2525
namespace {
2626

@@ -135,12 +135,8 @@ void LoadQuestDialogFromFile()
135135
const std::string_view filename = "txtdata\\towners\\quest_dialog.tsv";
136136
DataFile dataFile = DataFile::loadOrDie(filename);
137137

138-
// Initialize table with TEXT_NONE
138+
// Initialize table (will be populated as we read rows)
139139
TownerQuestDialogTable.clear();
140-
TownerQuestDialogTable.resize(NUM_TOWNER_TYPES);
141-
for (auto &row : TownerQuestDialogTable) {
142-
row.fill(TEXT_NONE);
143-
}
144140

145141
// Parse header to find which quest columns exist
146142
DataFileRecord headerRecord = *dataFile.begin();
@@ -155,21 +151,19 @@ void LoadQuestDialogFromFile()
155151
dataFile.skipHeaderOrDie(filename);
156152

157153
// Find the towner_type column index
158-
auto townerTypeColIt = columnMap.find("towner_type");
159-
if (townerTypeColIt == columnMap.end()) {
154+
if (!columnMap.contains("towner_type")) {
160155
return; // Invalid file format
161156
}
162-
unsigned townerTypeColIndex = townerTypeColIt->second;
157+
unsigned townerTypeColIndex = columnMap["towner_type"];
163158

164159
// Build quest column index map
165160
std::unordered_map<quest_id, unsigned> questColumnMap;
166161
for (quest_id quest : magic_enum::enum_values<quest_id>()) {
167162
if (quest == Q_INVALID || quest >= MAXQUESTS) continue;
168163

169-
auto questName = magic_enum::enum_name(quest);
170-
auto questColIt = columnMap.find(std::string(questName));
171-
if (questColIt != columnMap.end()) {
172-
questColumnMap[quest] = questColIt->second;
164+
auto questName = std::string(magic_enum::enum_name(quest));
165+
if (columnMap.contains(questName)) {
166+
questColumnMap[quest] = columnMap[questName];
173167
}
174168
}
175169

@@ -182,31 +176,30 @@ void LoadQuestDialogFromFile()
182176
}
183177

184178
// Read towner_type
185-
auto townerTypeFieldIt = fields.find(townerTypeColIndex);
186-
if (townerTypeFieldIt == fields.end()) {
179+
if (!fields.contains(townerTypeColIndex)) {
187180
continue; // Invalid row
188181
}
189182

190-
auto townerTypeResult = ParseEnum<_talker_id>(townerTypeFieldIt->second);
183+
auto townerTypeResult = ParseEnum<_talker_id>(fields[townerTypeColIndex]);
191184
if (!townerTypeResult.has_value()) {
192185
continue; // Invalid towner type
193186
}
194187
_talker_id townerType = townerTypeResult.value();
195188

196-
if (static_cast<size_t>(townerType) >= TownerQuestDialogTable.size()) {
197-
continue;
189+
// Initialize row if it doesn't exist, then get reference
190+
auto [it, inserted] = TownerQuestDialogTable.try_emplace(townerType);
191+
if (inserted) {
192+
it->second.fill(TEXT_NONE);
198193
}
199-
200-
auto &dialogRow = TownerQuestDialogTable[static_cast<size_t>(townerType)];
194+
auto &dialogRow = it->second;
201195

202196
// Read quest columns that exist in this file
203197
for (const auto &[quest, colIndex] : questColumnMap) {
204-
auto fieldIt = fields.find(colIndex);
205-
if (fieldIt == fields.end()) {
198+
if (!fields.contains(colIndex)) {
206199
continue; // Column missing in this row
207200
}
208201

209-
auto speechResult = ParseSpeechId(fieldIt->second);
202+
auto speechResult = ParseSpeechId(fields[colIndex]);
210203
if (speechResult.has_value()) {
211204
dialogRow[quest] = speechResult.value();
212205
}
@@ -224,24 +217,27 @@ void LoadTownerData()
224217

225218
_speech_id GetTownerQuestDialog(_talker_id type, quest_id quest)
226219
{
227-
if (static_cast<size_t>(type) >= TownerQuestDialogTable.size()) {
220+
if (quest < 0 || quest >= MAXQUESTS) {
228221
return TEXT_NONE;
229222
}
230-
if (quest < 0 || quest >= MAXQUESTS) {
223+
auto it = TownerQuestDialogTable.find(type);
224+
if (it == TownerQuestDialogTable.end()) {
231225
return TEXT_NONE;
232226
}
233-
return TownerQuestDialogTable[static_cast<size_t>(type)][quest];
227+
return it->second[quest];
234228
}
235229

236230
void SetTownerQuestDialog(_talker_id type, quest_id quest, _speech_id speech)
237231
{
238-
if (static_cast<size_t>(type) >= TownerQuestDialogTable.size()) {
239-
return;
240-
}
241232
if (quest < 0 || quest >= MAXQUESTS) {
242233
return;
243234
}
244-
TownerQuestDialogTable[static_cast<size_t>(type)][quest] = speech;
235+
// Initialize row if it doesn't exist
236+
auto [it, inserted] = TownerQuestDialogTable.try_emplace(type);
237+
if (inserted) {
238+
it->second.fill(TEXT_NONE);
239+
}
240+
it->second[quest] = speech;
245241
}
246242

247243
} // namespace devilution

Source/townerdat.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include <cstdint>
99
#include <string>
10+
#include <unordered_map>
1011
#include <vector>
1112

1213
#include "engine/direction.hpp"
@@ -21,7 +22,7 @@ namespace devilution {
2122
* @brief Data for a single towner entry loaded from TSV.
2223
*/
2324
struct TownerDataEntry {
24-
_talker_id type;
25+
_talker_id type; // Parsed from TSV using magic_enum
2526
std::string name;
2627
Point position;
2728
Direction direction;
@@ -37,7 +38,7 @@ struct TownerDataEntry {
3738
extern std::vector<TownerDataEntry> TownersDataEntries;
3839

3940
/** Contains the quest dialog table loaded from TSV. Indexed by [towner_type][quest_id]. */
40-
extern std::vector<std::array<_speech_id, MAXQUESTS>> TownerQuestDialogTable;
41+
extern std::unordered_map<_talker_id, std::array<_speech_id, MAXQUESTS>> TownerQuestDialogTable;
4142

4243
/**
4344
* @brief Loads towner data from TSV files.

Source/towners.cpp

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <algorithm>
44
#include <cstdint>
5+
#include <unordered_map>
56

67
#include "cursor.h"
78
#include "engine/clx_sprite.hpp"
@@ -50,7 +51,7 @@ struct TownerData {
5051
*
5152
* Populated during InitTowners() from the TownersData array.
5253
*/
53-
const TownerData *TownerBehaviors[NUM_TOWNER_TYPES];
54+
std::unordered_map<_talker_id, const TownerData *> TownerBehaviors;
5455

5556
/**
5657
* @brief Default towner initialization using TSV data.
@@ -88,7 +89,8 @@ void NewTownerAnim(Towner &towner, ClxSpriteList sprites, uint8_t numFrames, int
8889
void InitTownerInfo(Towner &towner, const TownerData &townerData, const TownerDataEntry &entry)
8990
{
9091
towner._ttype = townerData.type;
91-
towner.name = _(TownerLongNames[townerData.type]);
92+
auto nameIt = TownerLongNames.find(townerData.type);
93+
towner.name = nameIt != TownerLongNames.end() ? _(nameIt->second.c_str()) : std::string_view(entry.name);
9294
towner.position = entry.position;
9395
towner.talk = townerData.talk;
9496

@@ -99,14 +101,6 @@ void InitTownerInfo(Towner &towner, const TownerData &townerData, const TownerDa
99101
}
100102
}
101103

102-
void InitTownerInfo(int16_t i, const TownerData &townerData, const TownerDataEntry &entry)
103-
{
104-
// It's necessary to assign this before invoking townerData.init()
105-
// specifically for the cows that need to read this value to fill adjacent tiles
106-
dMonster[entry.position.x][entry.position.y] = i + 1;
107-
InitTownerInfo(Towners[i], townerData, entry);
108-
}
109-
110104
void LoadTownerAnimations(Towner &towner, const char *path, int frames, int delay)
111105
{
112106
towner.ownedAnim = std::nullopt;
@@ -704,23 +698,19 @@ const TownerData TownersData[] = {
704698

705699
} // namespace
706700

707-
Towner Towners[NUM_TOWNERS];
708-
709-
const char *const TownerLongNames[NUM_TOWNER_TYPES] {
710-
N_("Griswold the Blacksmith"),
711-
N_("Pepin the Healer"),
712-
N_("Wounded Townsman"),
713-
N_("Ogden the Tavern owner"),
714-
N_("Cain the Elder"),
715-
N_("Farnham the Drunk"),
716-
N_("Adria the Witch"),
717-
N_("Gillian the Barmaid"),
718-
N_("Wirt the Peg-legged boy"),
719-
N_("Cow"),
720-
N_("Lester the farmer"),
721-
N_("Celia"),
722-
N_("Complete Nut")
723-
};
701+
std::vector<Towner> Towners;
702+
703+
std::unordered_map<_talker_id, std::string> TownerLongNames;
704+
705+
size_t GetNumTownerTypes()
706+
{
707+
return TownerLongNames.size();
708+
}
709+
710+
size_t GetNumTowners()
711+
{
712+
return Towners.size();
713+
}
724714

725715
bool IsTownerPresent(_talker_id npc)
726716
{
@@ -756,23 +746,36 @@ void InitTowners()
756746
TownerAnimOrderStorage.clear();
757747

758748
// Build lookup table for towner behaviors
759-
std::fill(std::begin(TownerBehaviors), std::end(TownerBehaviors), nullptr);
749+
TownerBehaviors.clear();
760750
for (const auto &behavior : TownersData) {
761751
TownerBehaviors[behavior.type] = &behavior;
762752
}
763753

754+
// Build TownerLongNames from TSV data (first occurrence of each type wins)
755+
TownerLongNames.clear();
756+
for (const auto &entry : TownersDataEntries) {
757+
TownerLongNames.try_emplace(entry.type, entry.name);
758+
}
759+
764760
CowSprites.emplace(LoadCelSheet("towners\\animals\\cow", 128));
765761

762+
Towners.clear();
763+
Towners.reserve(TownersDataEntries.size());
766764
int16_t i = 0;
767765
for (const auto &entry : TownersDataEntries) {
768766
if (!IsTownerPresent(entry.type))
769767
continue;
770768

771-
const TownerData *behavior = TownerBehaviors[entry.type];
772-
if (behavior == nullptr)
769+
auto behaviorIt = TownerBehaviors.find(entry.type);
770+
if (behaviorIt == TownerBehaviors.end() || behaviorIt->second == nullptr)
773771
continue;
774772

775-
InitTownerInfo(i, *behavior, entry);
773+
// It's necessary to assign this before invoking townerData.init()
774+
// specifically for the cows that need to read this value to fill adjacent tiles
775+
dMonster[entry.position.x][entry.position.y] = i + 1;
776+
777+
Towners.emplace_back();
778+
InitTownerInfo(Towners.back(), *behaviorIt->second, entry);
776779
i++;
777780
}
778781
}
@@ -852,7 +855,7 @@ bool DebugTalkToTowner(_talker_id type)
852855
{
853856
if (!IsTownerPresent(type))
854857
return false;
855-
// Cows have special init logic that isn't compatible with this debug function
858+
// cows have an init function that differs from the rest and isn't compatible with this code, skip them :(
856859
if (type == TOWN_COW)
857860
return false;
858861

0 commit comments

Comments
 (0)