Skip to content

Commit 754a2fa

Browse files
committed
Adjust and clean up
1 parent a4561e1 commit 754a2fa

File tree

4 files changed

+132
-42
lines changed

4 files changed

+132
-42
lines changed

Source/lua/lua_global.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,8 @@ void LuaShutdown()
334334
#ifdef _DEBUG
335335
LuaReplShutdown();
336336
#endif
337+
// Must clear before destroying the Lua state: registered callbacks
338+
// capture sol::function handles that reference CurrentLuaState.
337339
ClearTownerDialogOptions();
338340
CurrentLuaState = std::nullopt;
339341
}

Source/stores.cpp

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77

88
#include <algorithm>
99
#include <cstdint>
10+
#include <optional>
1011
#include <string>
1112
#include <string_view>
12-
#include <unordered_map>
13+
#include <utility>
1314
#include <vector>
1415

1516
#include <fmt/format.h>
@@ -35,6 +36,7 @@
3536
#include "towners.h"
3637
#include "utils/format_int.hpp"
3738
#include "utils/language.h"
39+
#include "utils/log.hpp"
3840
#include "utils/str_cat.hpp"
3941
#include "utils/utf8.hpp"
4042

@@ -71,41 +73,69 @@ TalkID OldActiveStore;
7173
/** Temporary item used to hold the item being traded */
7274
Item TempItem;
7375

74-
std::unordered_map<std::string, std::vector<TownerDialogOption>> ExtraTownerOptions;
76+
std::vector<std::pair<std::string, std::vector<TownerDialogOption>>> ExtraTownerOptions;
7577

7678
const char *TownerNameForTalkID(TalkID s)
7779
{
78-
const auto lookup = [](const _talker_id id) -> const char * {
79-
auto it = TownerShortNames.find(id);
80-
return it != TownerShortNames.end() ? it->second : nullptr;
81-
};
8280
switch (s) {
83-
case TalkID::Smith: return lookup(TOWN_SMITH);
84-
case TalkID::Witch: return lookup(TOWN_WITCH);
85-
case TalkID::Boy: return lookup(TOWN_PEGBOY);
86-
case TalkID::Healer: return lookup(TOWN_HEALER);
87-
case TalkID::Storyteller: return lookup(TOWN_STORY);
88-
case TalkID::Tavern: return lookup(TOWN_TAVERN);
89-
case TalkID::Drunk: return lookup(TOWN_DRUNK);
90-
case TalkID::Barmaid: return lookup(TOWN_BMAID);
81+
case TalkID::Smith: return "griswold";
82+
case TalkID::Witch: return "adria";
83+
case TalkID::Boy: return "wirt";
84+
case TalkID::Healer: return "pepin";
85+
case TalkID::Storyteller: return "cain";
86+
case TalkID::Tavern: return "ogden";
87+
case TalkID::Drunk: return "farnham";
88+
case TalkID::Barmaid: return "gillian";
9189
default: return nullptr;
9290
}
9391
}
9492

93+
/** Finds the entry for a towner in ExtraTownerOptions, or nullptr if none. */
94+
static std::vector<TownerDialogOption> *FindExtraTownerOptions(std::string_view townerName)
95+
{
96+
for (auto &[name, opts] : ExtraTownerOptions) {
97+
if (name == townerName)
98+
return &opts;
99+
}
100+
return nullptr;
101+
}
102+
95103
void RegisterTownerDialogOption(std::string_view townerName,
96104
std::function<std::string()> getLabel,
97105
std::function<void()> onSelect)
98106
{
99-
ExtraTownerOptions[std::string(townerName)].push_back({ std::move(getLabel), std::move(onSelect) });
107+
// Validate that the towner name is known.
108+
bool found = false;
109+
for (const auto &[id, shortName] : TownerShortNames) {
110+
if (shortName == townerName) {
111+
found = true;
112+
break;
113+
}
114+
}
115+
if (!found) {
116+
LogWarn("RegisterTownerDialogOption: unknown towner name \"{}\"", townerName);
117+
}
118+
119+
if (auto *opts = FindExtraTownerOptions(townerName); opts != nullptr) {
120+
opts->push_back({ std::move(getLabel), std::move(onSelect) });
121+
} else {
122+
std::vector<TownerDialogOption> newOpts;
123+
newOpts.push_back({ std::move(getLabel), std::move(onSelect) });
124+
ExtraTownerOptions.emplace_back(std::string(townerName), std::move(newOpts));
125+
}
100126
}
101127

102-
/** Maps dialog line number to ExtraTownerOptions index for options visible in the current dialog session */
103-
static std::unordered_map<int, size_t> CurrentExtraOptionIndices;
128+
/**
129+
* Maps dialog line number to ExtraTownerOptions vector index for
130+
* options visible in the current dialog session.
131+
* Indexed by text line number (0..NumStoreLines-1).
132+
*/
133+
static std::optional<size_t> CurrentExtraOptionIndices[NumStoreLines];
104134

105135
void ClearTownerDialogOptions()
106136
{
107137
ExtraTownerOptions.clear();
108-
CurrentExtraOptionIndices.clear();
138+
std::fill(std::begin(CurrentExtraOptionIndices), std::end(CurrentExtraOptionIndices), std::nullopt);
109139
}
110140

111141
namespace {
@@ -2345,19 +2375,33 @@ void StartStore(TalkID s)
23452375
break;
23462376
}
23472377

2348-
CurrentExtraOptionIndices.clear();
2378+
std::fill(std::begin(CurrentExtraOptionIndices), std::end(CurrentExtraOptionIndices), std::nullopt);
23492379
if (const char *extraTownerName = TownerNameForTalkID(s); extraTownerName != nullptr) {
2350-
if (auto extraIt = ExtraTownerOptions.find(extraTownerName); extraIt != ExtraTownerOptions.end()) {
2380+
if (auto *extraOpts = FindExtraTownerOptions(extraTownerName); extraOpts != nullptr) {
2381+
// Find the last selectable line (the "leave"/"say goodbye" option).
2382+
int lastSelectableLine = -1;
2383+
for (int i = NumStoreLines - 1; i >= 0; --i) {
2384+
if (TextLine[i].isSelectable()) {
2385+
lastSelectableLine = i;
2386+
break;
2387+
}
2388+
}
2389+
2390+
// Insert extra options into empty even-numbered lines before the leave option.
23512391
size_t optIdx = 0;
2352-
for (int line = 14; line < 18 && optIdx < extraIt->second.size(); line += 2) {
2353-
if (TextLine[line].hasText()) break;
2354-
std::string label = extraIt->second[optIdx].getLabel();
2392+
for (int line = 10; line < lastSelectableLine && optIdx < extraOpts->size(); line += 2) {
2393+
if (TextLine[line].hasText()) continue;
2394+
std::string label = (*extraOpts)[optIdx].getLabel();
23552395
if (!label.empty()) {
23562396
AddSText(0, line, label, UiFlags::ColorWhite | UiFlags::AlignCenter, true);
23572397
CurrentExtraOptionIndices[line] = optIdx;
23582398
}
23592399
++optIdx;
23602400
}
2401+
if (optIdx < extraOpts->size()) {
2402+
LogWarn("Towner \"{}\" dialog: {} extra option(s) could not be placed (no empty lines)",
2403+
extraTownerName, extraOpts->size() - optIdx);
2404+
}
23612405
}
23622406
}
23632407

@@ -2626,10 +2670,15 @@ void StoreEnter()
26262670

26272671
PlaySFX(SfxID::MenuSelect);
26282672

2629-
if (auto extraOptIt = CurrentExtraOptionIndices.find(CurrentTextLine); extraOptIt != CurrentExtraOptionIndices.end()) {
2673+
if (CurrentTextLine >= 0 && CurrentTextLine < NumStoreLines && CurrentExtraOptionIndices[CurrentTextLine].has_value()) {
2674+
size_t optIdx = *CurrentExtraOptionIndices[CurrentTextLine];
26302675
if (const char *townerName = TownerNameForTalkID(ActiveStore); townerName != nullptr) {
2631-
if (auto it = ExtraTownerOptions.find(townerName); it != ExtraTownerOptions.end() && extraOptIt->second < it->second.size()) {
2632-
it->second[extraOptIt->second].onSelect();
2676+
if (auto *extraOpts = FindExtraTownerOptions(townerName); extraOpts != nullptr && optIdx < extraOpts->size()) {
2677+
ActiveStore = TalkID::None;
2678+
(*extraOpts)[optIdx].onSelect();
2679+
// If onSelect() set ActiveStore (e.g. to open a sub-dialog), preserve it.
2680+
// Otherwise it stays TalkID::None (dialog closed).
2681+
return;
26332682
}
26342683
}
26352684
ActiveStore = TalkID::None;

Source/stores.h

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#include <optional>
1111
#include <string>
1212
#include <string_view>
13-
#include <unordered_map>
13+
#include <utility>
1414
#include <vector>
1515

1616
#include "DiabloUI/ui_flags.hpp"
@@ -117,17 +117,29 @@ struct TownerDialogOption {
117117
};
118118

119119
/** Extra dialog options injected by mods, keyed by towner short name. */
120-
extern DVL_API_FOR_TEST std::unordered_map<std::string, std::vector<TownerDialogOption>> ExtraTownerOptions;
120+
extern DVL_API_FOR_TEST std::vector<std::pair<std::string, std::vector<TownerDialogOption>>> ExtraTownerOptions;
121121

122122
/**
123123
* @brief Returns the towner short name for a top-level TalkID, or nullptr if not a towner store.
124+
*
125+
* Only maps top-level store entries (Smith, Witch, Boy, Healer, Storyteller, Tavern, Drunk, Barmaid).
126+
* Sub-pages (SmithBuy, WitchSell, etc.) return nullptr.
124127
*/
125128
DVL_API_FOR_TEST const char *TownerNameForTalkID(TalkID s);
126129

127130
/**
128131
* @brief Registers a dynamic dialog option for a towner's talk menu.
129132
*
130-
* @param townerName Short name of the towner (e.g. "farnham").
133+
* Options are inserted into empty even-numbered lines before the towner's "leave" option.
134+
* If no empty lines are available the option is silently skipped for that dialog session
135+
* and a warning is logged.
136+
*
137+
* When the player selects a mod option, onSelect() is called. By default the dialog is
138+
* closed afterwards. If onSelect() sets ActiveStore to a value other than TalkID::None,
139+
* that value is preserved (e.g. to open a sub-dialog).
140+
*
141+
* @param townerName Short name of the towner (e.g. "farnham"). Must match one of the
142+
* names in TownerShortNames; a warning is logged for unknown names.
131143
* @param getLabel Called when the dialog is built; return a non-empty string to show the
132144
* option, or an empty string to hide it.
133145
* @param onSelect Called when the player chooses this option.
@@ -136,7 +148,12 @@ void RegisterTownerDialogOption(std::string_view townerName,
136148
std::function<std::string()> getLabel,
137149
std::function<void()> onSelect);
138150

139-
/** Clears all mod-registered towner dialog options (e.g. before mod reload / Lua shutdown). */
151+
/**
152+
* @brief Clears all mod-registered towner dialog options.
153+
*
154+
* Must be called before the Lua state is destroyed, since registered callbacks
155+
* capture sol::function handles that reference the Lua state.
156+
*/
140157
void ClearTownerDialogOptions();
141158

142159
void AddStoreHoldRepair(Item *itm, int8_t i);

test/stores_test.cpp

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ using namespace devilution;
66

77
namespace {
88

9+
/** Helper to find the options vector for a towner by short name, or nullptr. */
10+
std::vector<TownerDialogOption> *FindOptions(std::string_view name)
11+
{
12+
for (auto &[key, opts] : ExtraTownerOptions) {
13+
if (key == name)
14+
return &opts;
15+
}
16+
return nullptr;
17+
}
18+
919
TEST(Stores, AddStoreHoldRepair_magic)
1020
{
1121
devilution::Item *item;
@@ -113,8 +123,10 @@ TEST(Stores, RegisterTownerDialogOption_afterClearDoesNotAccumulate)
113123
ClearTownerDialogOptions();
114124
RegisterTownerDialogOption("farnham", []() { return std::string("Second"); }, []() {});
115125

116-
ASSERT_EQ(ExtraTownerOptions.at("farnham").size(), 1u);
117-
EXPECT_EQ(ExtraTownerOptions.at("farnham")[0].getLabel(), "Second");
126+
auto *opts = FindOptions("farnham");
127+
ASSERT_NE(opts, nullptr);
128+
ASSERT_EQ(opts->size(), 1u);
129+
EXPECT_EQ((*opts)[0].getLabel(), "Second");
118130

119131
ClearTownerDialogOptions();
120132
}
@@ -125,9 +137,10 @@ TEST(Stores, RegisterTownerDialogOption_storesOption)
125137

126138
RegisterTownerDialogOption("farnham", []() { return std::string("Go to Tiny Town"); }, []() {});
127139

128-
ASSERT_EQ(ExtraTownerOptions.count("farnham"), 1u);
129-
ASSERT_EQ(ExtraTownerOptions.at("farnham").size(), 1u);
130-
EXPECT_EQ(ExtraTownerOptions.at("farnham")[0].getLabel(), "Go to Tiny Town");
140+
auto *opts = FindOptions("farnham");
141+
ASSERT_NE(opts, nullptr);
142+
ASSERT_EQ(opts->size(), 1u);
143+
EXPECT_EQ((*opts)[0].getLabel(), "Go to Tiny Town");
131144

132145
ClearTownerDialogOptions();
133146
}
@@ -139,7 +152,10 @@ TEST(Stores, RegisterTownerDialogOption_callsOnSelect)
139152
bool called = false;
140153
RegisterTownerDialogOption("farnham", []() { return std::string("Travel"); }, [&called]() { called = true; });
141154

142-
ExtraTownerOptions.at("farnham")[0].onSelect();
155+
auto *opts = FindOptions("farnham");
156+
ASSERT_NE(opts, nullptr);
157+
ASSERT_EQ(opts->size(), 1u);
158+
(*opts)[0].onSelect();
143159
EXPECT_TRUE(called);
144160

145161
ClearTownerDialogOptions();
@@ -151,8 +167,10 @@ TEST(Stores, RegisterTownerDialogOption_emptyLabelHidesOption)
151167

152168
RegisterTownerDialogOption("farnham", []() { return std::string(""); }, []() {});
153169

154-
ASSERT_EQ(ExtraTownerOptions.at("farnham").size(), 1u);
155-
EXPECT_TRUE(ExtraTownerOptions.at("farnham")[0].getLabel().empty());
170+
auto *opts = FindOptions("farnham");
171+
ASSERT_NE(opts, nullptr);
172+
ASSERT_EQ(opts->size(), 1u);
173+
EXPECT_TRUE((*opts)[0].getLabel().empty());
156174

157175
ClearTownerDialogOptions();
158176
}
@@ -164,10 +182,14 @@ TEST(Stores, RegisterTownerDialogOption_multipleTowners)
164182
RegisterTownerDialogOption("farnham", []() { return std::string("A"); }, []() {});
165183
RegisterTownerDialogOption("griswold", []() { return std::string("B"); }, []() {});
166184

167-
EXPECT_EQ(ExtraTownerOptions.at("farnham").size(), 1u);
168-
EXPECT_EQ(ExtraTownerOptions.at("griswold").size(), 1u);
169-
EXPECT_EQ(ExtraTownerOptions.at("farnham")[0].getLabel(), "A");
170-
EXPECT_EQ(ExtraTownerOptions.at("griswold")[0].getLabel(), "B");
185+
auto *farnhamOpts = FindOptions("farnham");
186+
auto *griswoldOpts = FindOptions("griswold");
187+
ASSERT_NE(farnhamOpts, nullptr);
188+
ASSERT_NE(griswoldOpts, nullptr);
189+
EXPECT_EQ(farnhamOpts->size(), 1u);
190+
EXPECT_EQ(griswoldOpts->size(), 1u);
191+
EXPECT_EQ((*farnhamOpts)[0].getLabel(), "A");
192+
EXPECT_EQ((*griswoldOpts)[0].getLabel(), "B");
171193

172194
ClearTownerDialogOptions();
173195
}

0 commit comments

Comments
 (0)