|
7 | 7 |
|
8 | 8 | #include <algorithm> |
9 | 9 | #include <cstdint> |
| 10 | +#include <optional> |
10 | 11 | #include <string> |
11 | 12 | #include <string_view> |
12 | | -#include <unordered_map> |
| 13 | +#include <utility> |
13 | 14 | #include <vector> |
14 | 15 |
|
15 | 16 | #include <fmt/format.h> |
|
35 | 36 | #include "towners.h" |
36 | 37 | #include "utils/format_int.hpp" |
37 | 38 | #include "utils/language.h" |
| 39 | +#include "utils/log.hpp" |
38 | 40 | #include "utils/str_cat.hpp" |
39 | 41 | #include "utils/utf8.hpp" |
40 | 42 |
|
@@ -71,41 +73,69 @@ TalkID OldActiveStore; |
71 | 73 | /** Temporary item used to hold the item being traded */ |
72 | 74 | Item TempItem; |
73 | 75 |
|
74 | | -std::unordered_map<std::string, std::vector<TownerDialogOption>> ExtraTownerOptions; |
| 76 | +std::vector<std::pair<std::string, std::vector<TownerDialogOption>>> ExtraTownerOptions; |
75 | 77 |
|
76 | 78 | const char *TownerNameForTalkID(TalkID s) |
77 | 79 | { |
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 | | - }; |
82 | 80 | 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"; |
91 | 89 | default: return nullptr; |
92 | 90 | } |
93 | 91 | } |
94 | 92 |
|
| 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 | + |
95 | 103 | void RegisterTownerDialogOption(std::string_view townerName, |
96 | 104 | std::function<std::string()> getLabel, |
97 | 105 | std::function<void()> onSelect) |
98 | 106 | { |
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 | + } |
100 | 126 | } |
101 | 127 |
|
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]; |
104 | 134 |
|
105 | 135 | void ClearTownerDialogOptions() |
106 | 136 | { |
107 | 137 | ExtraTownerOptions.clear(); |
108 | | - CurrentExtraOptionIndices.clear(); |
| 138 | + std::fill(std::begin(CurrentExtraOptionIndices), std::end(CurrentExtraOptionIndices), std::nullopt); |
109 | 139 | } |
110 | 140 |
|
111 | 141 | namespace { |
@@ -2345,19 +2375,33 @@ void StartStore(TalkID s) |
2345 | 2375 | break; |
2346 | 2376 | } |
2347 | 2377 |
|
2348 | | - CurrentExtraOptionIndices.clear(); |
| 2378 | + std::fill(std::begin(CurrentExtraOptionIndices), std::end(CurrentExtraOptionIndices), std::nullopt); |
2349 | 2379 | 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. |
2351 | 2391 | 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(); |
2355 | 2395 | if (!label.empty()) { |
2356 | 2396 | AddSText(0, line, label, UiFlags::ColorWhite | UiFlags::AlignCenter, true); |
2357 | 2397 | CurrentExtraOptionIndices[line] = optIdx; |
2358 | 2398 | } |
2359 | 2399 | ++optIdx; |
2360 | 2400 | } |
| 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 | + } |
2361 | 2405 | } |
2362 | 2406 | } |
2363 | 2407 |
|
@@ -2626,10 +2670,15 @@ void StoreEnter() |
2626 | 2670 |
|
2627 | 2671 | PlaySFX(SfxID::MenuSelect); |
2628 | 2672 |
|
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]; |
2630 | 2675 | 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; |
2633 | 2682 | } |
2634 | 2683 | } |
2635 | 2684 | ActiveStore = TalkID::None; |
|
0 commit comments