mirror of
https://github.com/diasurgical/DevilutionX.git
synced 2026-10-12 02:13:06 +00:00
Add Lua towner API and dynamic dialog options (prework for multi-town PR) (#8539)
* Add Lua towner API and dynamic dialog options Expose TownerShortNames to Lua (devilutionx.towners), per-towner position helpers, and RegisterTownerDialogOption for mod-driven store dialog lines. Includes stores integration and tests.
This commit is contained in:
+41
-16
@@ -1,11 +1,24 @@
|
||||
#include "lua/lua_global.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <ankerl/unordered_dense.h>
|
||||
#include <expected.hpp>
|
||||
#include <function_ref.hpp>
|
||||
#include <sol/bytecode.hpp>
|
||||
#include <sol/debug.hpp>
|
||||
#include <sol/sol.hpp>
|
||||
#include <sol/environment.hpp>
|
||||
#include <sol/forward.hpp>
|
||||
#include <sol/function_types_templated.hpp>
|
||||
#include <sol/protected_function.hpp>
|
||||
#include <sol/stack_push.hpp>
|
||||
#include <sol/state.hpp>
|
||||
#include <sol/table.hpp>
|
||||
#include <sol/types.hpp>
|
||||
|
||||
#include <config.h>
|
||||
|
||||
@@ -24,10 +37,9 @@
|
||||
#include "lua/modules/render.hpp"
|
||||
#include "lua/modules/system.hpp"
|
||||
#include "lua/modules/towners.hpp"
|
||||
#include "monster.h"
|
||||
#include "options.h"
|
||||
#include "player.h"
|
||||
#include "plrmsg.h"
|
||||
#include "stores.h"
|
||||
#include "utils/console.h"
|
||||
#include "utils/log.hpp"
|
||||
#include "utils/str_cat.hpp"
|
||||
@@ -41,12 +53,23 @@ namespace devilution {
|
||||
|
||||
namespace {
|
||||
|
||||
void LuaPanic(const std::optional<std::string> &message)
|
||||
{
|
||||
LogError("Lua is in a panic state and will now abort() the application:\n{}",
|
||||
message.value_or("unknown error"));
|
||||
}
|
||||
|
||||
struct LuaState {
|
||||
sol::state sol = {};
|
||||
sol::table commonPackages = {};
|
||||
ankerl::unordered_dense::segmented_map<std::string, sol::bytecode> compiledScripts = {};
|
||||
sol::environment sandbox = {};
|
||||
sol::table events = {};
|
||||
sol::state sol;
|
||||
sol::table commonPackages;
|
||||
ankerl::unordered_dense::segmented_map<std::string, sol::bytecode> compiledScripts;
|
||||
sol::environment sandbox;
|
||||
sol::table events;
|
||||
|
||||
LuaState()
|
||||
: sol(sol::c_call<decltype(&LuaPanic), &LuaPanic>)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
std::optional<LuaState> CurrentLuaState;
|
||||
@@ -74,6 +97,9 @@ end
|
||||
|
||||
sol::object LuaLoadScriptFromAssets(std::string_view packageName)
|
||||
{
|
||||
if (!CurrentLuaState.has_value()) {
|
||||
app_fatal("Lua state is not initialized");
|
||||
}
|
||||
LuaState &luaState = *CurrentLuaState;
|
||||
constexpr std::string_view PathPrefix = "lua\\";
|
||||
constexpr std::string_view PathSuffix = ".lua";
|
||||
@@ -145,12 +171,6 @@ sol::object RunScript(std::optional<sol::environment> env, std::string_view pack
|
||||
return SafeCallResult(fn(), optional);
|
||||
}
|
||||
|
||||
void LuaPanic(sol::optional<std::string> message)
|
||||
{
|
||||
LogError("Lua is in a panic state and will now abort() the application:\n{}",
|
||||
message.value_or("unknown error"));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Sol2DebugPrintStack(lua_State *state)
|
||||
@@ -166,7 +186,7 @@ void Sol2DebugPrintSection(const std::string &message, lua_State *state)
|
||||
sol::environment CreateLuaSandbox()
|
||||
{
|
||||
sol::state &lua = CurrentLuaState->sol;
|
||||
sol::environment sandbox(CurrentLuaState->sol, sol::create);
|
||||
sol::environment sandbox(lua, sol::create);
|
||||
|
||||
// Registering globals
|
||||
sandbox.set(
|
||||
@@ -223,6 +243,8 @@ void LuaReloadActiveMods()
|
||||
CurrentLuaState->events = RunScript(/*env=*/std::nullopt, "devilutionx.events", /*optional=*/false);
|
||||
CurrentLuaState->commonPackages["devilutionx.events"] = CurrentLuaState->events;
|
||||
|
||||
ClearTownerDialogOptions();
|
||||
|
||||
gbIsHellfire = false;
|
||||
UnloadModArchives();
|
||||
|
||||
@@ -260,7 +282,7 @@ void LuaReloadActiveMods()
|
||||
|
||||
void LuaInitialize()
|
||||
{
|
||||
CurrentLuaState.emplace(LuaState { .sol = { sol::c_call<decltype(&LuaPanic), &LuaPanic> } });
|
||||
CurrentLuaState.emplace();
|
||||
sol::state &lua = CurrentLuaState->sol;
|
||||
lua_setwarnf(lua.lua_state(), LuaWarn, /*ud=*/nullptr);
|
||||
lua.open_libraries(
|
||||
@@ -312,6 +334,9 @@ void LuaShutdown()
|
||||
#ifdef _DEBUG
|
||||
LuaReplShutdown();
|
||||
#endif
|
||||
// Must clear before destroying the Lua state: registered callbacks
|
||||
// capture sol::function handles that reference CurrentLuaState.
|
||||
ClearTownerDialogOptions();
|
||||
CurrentLuaState = std::nullopt;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "lua/modules/towners.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <sol/sol.hpp>
|
||||
@@ -9,28 +9,12 @@
|
||||
#include "engine/point.hpp"
|
||||
#include "lua/metadoc.hpp"
|
||||
#include "player.h"
|
||||
#include "stores.h"
|
||||
#include "towners.h"
|
||||
|
||||
namespace devilution {
|
||||
namespace {
|
||||
|
||||
// Map from towner type enum to Lua table name
|
||||
const std::unordered_map<_talker_id, const char *> TownerTableNames = {
|
||||
{ TOWN_SMITH, "griswold" },
|
||||
{ TOWN_HEALER, "pepin" },
|
||||
{ TOWN_DEADGUY, "deadguy" },
|
||||
{ TOWN_TAVERN, "ogden" },
|
||||
{ TOWN_STORY, "cain" },
|
||||
{ TOWN_DRUNK, "farnham" },
|
||||
{ TOWN_WITCH, "adria" },
|
||||
{ TOWN_BMAID, "gillian" },
|
||||
{ TOWN_PEGBOY, "wirt" },
|
||||
{ TOWN_COW, "cow" },
|
||||
{ TOWN_FARMER, "lester" },
|
||||
{ TOWN_GIRL, "celia" },
|
||||
{ TOWN_COWFARM, "nut" },
|
||||
};
|
||||
|
||||
void PopulateTownerTable(_talker_id townerId, sol::table &out)
|
||||
{
|
||||
LuaSetDocFn(out, "position", "()",
|
||||
@@ -48,14 +32,36 @@ sol::table LuaTownersModule(sol::state_view &lua)
|
||||
sol::table table = lua.create_table();
|
||||
// Iterate over all towner types found in TSV data
|
||||
for (const auto &[townerId, name] : TownerLongNames) {
|
||||
auto tableNameIt = TownerTableNames.find(townerId);
|
||||
if (tableNameIt == TownerTableNames.end())
|
||||
continue; // Skip if no table name mapping
|
||||
auto shortNameIt = TownerShortNames.find(townerId);
|
||||
if (shortNameIt == TownerShortNames.end())
|
||||
continue; // Skip if no short name mapping
|
||||
|
||||
sol::table townerTable = lua.create_table();
|
||||
PopulateTownerTable(townerId, townerTable);
|
||||
LuaSetDoc(table, tableNameIt->second, /*signature=*/"", name.c_str(), std::move(townerTable));
|
||||
LuaSetDoc(table, shortNameIt->second, /*signature=*/"", name.c_str(), std::move(townerTable));
|
||||
}
|
||||
|
||||
LuaSetDocFn(table, "addDialogOption",
|
||||
"(townerName: string, getLabel: function, onSelect: function)",
|
||||
"Adds a dynamic dialog option to a towner's talk menu.\n"
|
||||
"getLabel() is called each time the dialog opens; return a non-empty string to show\n"
|
||||
"the option or an empty string/nil to hide it.\n"
|
||||
"onSelect() is called when the player chooses the option.\n"
|
||||
"All options are cleared when mods reload or Lua shuts down; register from mod init.",
|
||||
[](std::string_view townerName, const sol::function &getLabel, const sol::function &onSelect) {
|
||||
RegisterTownerDialogOption(
|
||||
townerName,
|
||||
[getLabel]() -> std::string {
|
||||
sol::object result = getLabel();
|
||||
if (result.get_type() == sol::type::string)
|
||||
return result.as<std::string>();
|
||||
return {};
|
||||
},
|
||||
[onSelect]() {
|
||||
onSelect();
|
||||
});
|
||||
});
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
|
||||
+118
-28
@@ -7,7 +7,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
@@ -32,6 +36,7 @@
|
||||
#include "towners.h"
|
||||
#include "utils/format_int.hpp"
|
||||
#include "utils/language.h"
|
||||
#include "utils/log.hpp"
|
||||
#include "utils/str_cat.hpp"
|
||||
#include "utils/utf8.hpp"
|
||||
|
||||
@@ -68,6 +73,71 @@ TalkID OldActiveStore;
|
||||
/** Temporary item used to hold the item being traded */
|
||||
Item TempItem;
|
||||
|
||||
std::vector<std::pair<std::string, std::vector<TownerDialogOption>>> ExtraTownerOptions;
|
||||
|
||||
const char *TownerNameForTalkID(TalkID s)
|
||||
{
|
||||
switch (s) {
|
||||
case TalkID::Smith: return "griswold";
|
||||
case TalkID::Witch: return "adria";
|
||||
case TalkID::Boy: return "wirt";
|
||||
case TalkID::Healer: return "pepin";
|
||||
case TalkID::Storyteller: return "cain";
|
||||
case TalkID::Tavern: return "ogden";
|
||||
case TalkID::Drunk: return "farnham";
|
||||
case TalkID::Barmaid: return "gillian";
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/** Finds the entry for a towner in ExtraTownerOptions, or nullptr if none. */
|
||||
static std::vector<TownerDialogOption> *FindExtraTownerOptions(std::string_view townerName)
|
||||
{
|
||||
for (auto &[name, opts] : ExtraTownerOptions) {
|
||||
if (name == townerName)
|
||||
return &opts;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void RegisterTownerDialogOption(std::string_view townerName,
|
||||
std::function<std::string()> getLabel,
|
||||
std::function<void()> onSelect)
|
||||
{
|
||||
// Validate that the towner name is known.
|
||||
bool found = false;
|
||||
for (const auto &[id, shortName] : TownerShortNames) {
|
||||
if (shortName == townerName) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
LogWarn("RegisterTownerDialogOption: unknown towner name \"{}\"", townerName);
|
||||
}
|
||||
|
||||
if (auto *opts = FindExtraTownerOptions(townerName); opts != nullptr) {
|
||||
opts->push_back({ std::move(getLabel), std::move(onSelect) });
|
||||
} else {
|
||||
std::vector<TownerDialogOption> newOpts;
|
||||
newOpts.push_back({ std::move(getLabel), std::move(onSelect) });
|
||||
ExtraTownerOptions.emplace_back(std::string(townerName), std::move(newOpts));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps dialog line number to ExtraTownerOptions vector index for
|
||||
* options visible in the current dialog session.
|
||||
* Indexed by text line number (0..NumStoreLines-1).
|
||||
*/
|
||||
static std::optional<size_t> CurrentExtraOptionIndices[NumStoreLines];
|
||||
|
||||
void ClearTownerDialogOptions()
|
||||
{
|
||||
ExtraTownerOptions.clear();
|
||||
std::fill(std::begin(CurrentExtraOptionIndices), std::end(CurrentExtraOptionIndices), std::nullopt);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/** The current towner being interacted with */
|
||||
@@ -2217,34 +2287,8 @@ void StartStore(TalkID s)
|
||||
ReleaseStoreBtn();
|
||||
|
||||
// Fire StoreOpened Lua event for main store entries
|
||||
switch (s) {
|
||||
case TalkID::Smith:
|
||||
lua::StoreOpened("griswold");
|
||||
break;
|
||||
case TalkID::Witch:
|
||||
lua::StoreOpened("adria");
|
||||
break;
|
||||
case TalkID::Boy:
|
||||
lua::StoreOpened("wirt");
|
||||
break;
|
||||
case TalkID::Healer:
|
||||
lua::StoreOpened("pepin");
|
||||
break;
|
||||
case TalkID::Storyteller:
|
||||
lua::StoreOpened("cain");
|
||||
break;
|
||||
case TalkID::Tavern:
|
||||
lua::StoreOpened("ogden");
|
||||
break;
|
||||
case TalkID::Drunk:
|
||||
lua::StoreOpened("farnham");
|
||||
break;
|
||||
case TalkID::Barmaid:
|
||||
lua::StoreOpened("gillian");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (const char *name = TownerNameForTalkID(s); name != nullptr)
|
||||
lua::StoreOpened(name);
|
||||
|
||||
switch (s) {
|
||||
case TalkID::Smith:
|
||||
@@ -2331,6 +2375,36 @@ void StartStore(TalkID s)
|
||||
break;
|
||||
}
|
||||
|
||||
std::fill(std::begin(CurrentExtraOptionIndices), std::end(CurrentExtraOptionIndices), std::nullopt);
|
||||
if (const char *extraTownerName = TownerNameForTalkID(s); extraTownerName != nullptr) {
|
||||
if (auto *extraOpts = FindExtraTownerOptions(extraTownerName); extraOpts != nullptr) {
|
||||
// Find the last selectable line (the "leave"/"say goodbye" option).
|
||||
int lastSelectableLine = -1;
|
||||
for (int i = NumStoreLines - 1; i >= 0; --i) {
|
||||
if (TextLine[i].isSelectable()) {
|
||||
lastSelectableLine = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert extra options into empty even-numbered lines before the leave option.
|
||||
size_t optIdx = 0;
|
||||
for (int line = 10; line < lastSelectableLine && optIdx < extraOpts->size(); line += 2) {
|
||||
if (TextLine[line].hasText()) continue;
|
||||
std::string label = (*extraOpts)[optIdx].getLabel();
|
||||
if (!label.empty()) {
|
||||
AddSText(0, line, label, UiFlags::ColorWhite | UiFlags::AlignCenter, true);
|
||||
CurrentExtraOptionIndices[line] = optIdx;
|
||||
}
|
||||
++optIdx;
|
||||
}
|
||||
if (optIdx < extraOpts->size()) {
|
||||
LogWarn("Towner \"{}\" dialog: {} extra option(s) could not be placed (no empty lines)",
|
||||
extraTownerName, extraOpts->size() - optIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CurrentTextLine = -1;
|
||||
for (int i = 0; i < NumStoreLines; i++) {
|
||||
if (TextLine[i].isSelectable()) {
|
||||
@@ -2595,6 +2669,22 @@ void StoreEnter()
|
||||
}
|
||||
|
||||
PlaySFX(SfxID::MenuSelect);
|
||||
|
||||
if (CurrentTextLine >= 0 && CurrentTextLine < NumStoreLines && CurrentExtraOptionIndices[CurrentTextLine].has_value()) {
|
||||
size_t optIdx = *CurrentExtraOptionIndices[CurrentTextLine];
|
||||
if (const char *townerName = TownerNameForTalkID(ActiveStore); townerName != nullptr) {
|
||||
if (auto *extraOpts = FindExtraTownerOptions(townerName); extraOpts != nullptr && optIdx < extraOpts->size()) {
|
||||
ActiveStore = TalkID::None;
|
||||
(*extraOpts)[optIdx].onSelect();
|
||||
// If onSelect() set ActiveStore (e.g. to open a sub-dialog), preserve it.
|
||||
// Otherwise it stays TalkID::None (dialog closed).
|
||||
return;
|
||||
}
|
||||
}
|
||||
ActiveStore = TalkID::None;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (ActiveStore) {
|
||||
case TalkID::Smith:
|
||||
SmithEnter();
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "DiabloUI/ui_flags.hpp"
|
||||
#include "control/control.hpp"
|
||||
@@ -106,6 +111,51 @@ extern DVL_API_FOR_TEST TalkID OldActiveStore;
|
||||
/** Temporary item used to hold the item being traded */
|
||||
extern DVL_API_FOR_TEST Item TempItem;
|
||||
|
||||
struct TownerDialogOption {
|
||||
std::function<std::string()> getLabel;
|
||||
std::function<void()> onSelect;
|
||||
};
|
||||
|
||||
/** Extra dialog options injected by mods, keyed by towner short name. */
|
||||
extern DVL_API_FOR_TEST std::vector<std::pair<std::string, std::vector<TownerDialogOption>>> ExtraTownerOptions;
|
||||
|
||||
/**
|
||||
* @brief Returns the towner short name for a top-level TalkID, or nullptr if not a towner store.
|
||||
*
|
||||
* Only maps top-level store entries (Smith, Witch, Boy, Healer, Storyteller, Tavern, Drunk, Barmaid).
|
||||
* Sub-pages (SmithBuy, WitchSell, etc.) return nullptr.
|
||||
*/
|
||||
DVL_API_FOR_TEST const char *TownerNameForTalkID(TalkID s);
|
||||
|
||||
/**
|
||||
* @brief Registers a dynamic dialog option for a towner's talk menu.
|
||||
*
|
||||
* Options are inserted into empty even-numbered lines before the towner's "leave" option.
|
||||
* If no empty lines are available the option is silently skipped for that dialog session
|
||||
* and a warning is logged.
|
||||
*
|
||||
* When the player selects a mod option, onSelect() is called. By default the dialog is
|
||||
* closed afterwards. If onSelect() sets ActiveStore to a value other than TalkID::None,
|
||||
* that value is preserved (e.g. to open a sub-dialog).
|
||||
*
|
||||
* @param townerName Short name of the towner (e.g. "farnham"). Must match one of the
|
||||
* names in TownerShortNames; a warning is logged for unknown names.
|
||||
* @param getLabel Called when the dialog is built; return a non-empty string to show the
|
||||
* option, or an empty string to hide it.
|
||||
* @param onSelect Called when the player chooses this option.
|
||||
*/
|
||||
void RegisterTownerDialogOption(std::string_view townerName,
|
||||
std::function<std::string()> getLabel,
|
||||
std::function<void()> onSelect);
|
||||
|
||||
/**
|
||||
* @brief Clears all mod-registered towner dialog options.
|
||||
*
|
||||
* Must be called before the Lua state is destroyed, since registered callbacks
|
||||
* capture sol::function handles that reference the Lua state.
|
||||
*/
|
||||
void ClearTownerDialogOptions();
|
||||
|
||||
void AddStoreHoldRepair(Item *itm, int8_t i);
|
||||
|
||||
/** Clears premium items sold by Griswold and Wirt. */
|
||||
|
||||
@@ -704,6 +704,22 @@ std::vector<Towner> Towners;
|
||||
|
||||
std::unordered_map<_talker_id, std::string> TownerLongNames;
|
||||
|
||||
const std::unordered_map<_talker_id, const char *> TownerShortNames = {
|
||||
{ TOWN_SMITH, "griswold" },
|
||||
{ TOWN_HEALER, "pepin" },
|
||||
{ TOWN_DEADGUY, "deadguy" },
|
||||
{ TOWN_TAVERN, "ogden" },
|
||||
{ TOWN_STORY, "cain" },
|
||||
{ TOWN_DRUNK, "farnham" },
|
||||
{ TOWN_WITCH, "adria" },
|
||||
{ TOWN_BMAID, "gillian" },
|
||||
{ TOWN_PEGBOY, "wirt" },
|
||||
{ TOWN_COW, "cow" },
|
||||
{ TOWN_FARMER, "lester" },
|
||||
{ TOWN_GIRL, "celia" },
|
||||
{ TOWN_COWFARM, "nut" },
|
||||
};
|
||||
|
||||
size_t GetNumTownerTypes()
|
||||
{
|
||||
return TownerLongNames.size();
|
||||
|
||||
@@ -43,6 +43,7 @@ enum _talker_id : uint8_t {
|
||||
|
||||
// Runtime mappings built from TSV data
|
||||
extern DVL_API_FOR_TEST std::unordered_map<_talker_id, std::string> TownerLongNames; // Maps towner type enum to display name
|
||||
extern const std::unordered_map<_talker_id, const char *> TownerShortNames; // Maps towner type enum to Lua/mod short name
|
||||
|
||||
struct Towner {
|
||||
OptionalOwnedClxSpriteList ownedAnim;
|
||||
|
||||
@@ -6,6 +6,16 @@ using namespace devilution;
|
||||
|
||||
namespace {
|
||||
|
||||
/** Helper to find the options vector for a towner by short name, or nullptr. */
|
||||
std::vector<TownerDialogOption> *FindOptions(std::string_view name)
|
||||
{
|
||||
for (auto &[key, opts] : ExtraTownerOptions) {
|
||||
if (key == name)
|
||||
return &opts;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TEST(Stores, AddStoreHoldRepair_magic)
|
||||
{
|
||||
devilution::Item *item;
|
||||
@@ -71,4 +81,117 @@ TEST(Stores, AddStoreHoldRepair_normal)
|
||||
EXPECT_EQ(1, item->_ivalue);
|
||||
EXPECT_EQ(1, item->_iIvalue);
|
||||
}
|
||||
|
||||
TEST(Stores, TownerNameForTalkID_knownTowners)
|
||||
{
|
||||
EXPECT_STREQ(TownerNameForTalkID(TalkID::Smith), "griswold");
|
||||
EXPECT_STREQ(TownerNameForTalkID(TalkID::Witch), "adria");
|
||||
EXPECT_STREQ(TownerNameForTalkID(TalkID::Boy), "wirt");
|
||||
EXPECT_STREQ(TownerNameForTalkID(TalkID::Healer), "pepin");
|
||||
EXPECT_STREQ(TownerNameForTalkID(TalkID::Storyteller), "cain");
|
||||
EXPECT_STREQ(TownerNameForTalkID(TalkID::Tavern), "ogden");
|
||||
EXPECT_STREQ(TownerNameForTalkID(TalkID::Drunk), "farnham");
|
||||
EXPECT_STREQ(TownerNameForTalkID(TalkID::Barmaid), "gillian");
|
||||
}
|
||||
|
||||
TEST(Stores, TownerNameForTalkID_subPagesReturnNull)
|
||||
{
|
||||
// Sub-pages (buy/sell screens) should not fire StoreOpened
|
||||
EXPECT_EQ(TownerNameForTalkID(TalkID::None), nullptr);
|
||||
EXPECT_EQ(TownerNameForTalkID(TalkID::SmithBuy), nullptr);
|
||||
EXPECT_EQ(TownerNameForTalkID(TalkID::SmithSell), nullptr);
|
||||
EXPECT_EQ(TownerNameForTalkID(TalkID::SmithRepair), nullptr);
|
||||
EXPECT_EQ(TownerNameForTalkID(TalkID::WitchBuy), nullptr);
|
||||
EXPECT_EQ(TownerNameForTalkID(TalkID::Gossip), nullptr);
|
||||
EXPECT_EQ(TownerNameForTalkID(TalkID::StorytellerIdentify), nullptr);
|
||||
EXPECT_EQ(TownerNameForTalkID(TalkID::StorytellerIdentifyShow), nullptr);
|
||||
}
|
||||
|
||||
TEST(Stores, ClearTownerDialogOptions_removesRegistrations)
|
||||
{
|
||||
RegisterTownerDialogOption("farnham", []() { return std::string("A"); }, []() {});
|
||||
ASSERT_FALSE(ExtraTownerOptions.empty());
|
||||
|
||||
ClearTownerDialogOptions();
|
||||
|
||||
EXPECT_TRUE(ExtraTownerOptions.empty());
|
||||
}
|
||||
|
||||
TEST(Stores, RegisterTownerDialogOption_afterClearDoesNotAccumulate)
|
||||
{
|
||||
RegisterTownerDialogOption("farnham", []() { return std::string("First"); }, []() {});
|
||||
ClearTownerDialogOptions();
|
||||
RegisterTownerDialogOption("farnham", []() { return std::string("Second"); }, []() {});
|
||||
|
||||
auto *opts = FindOptions("farnham");
|
||||
ASSERT_NE(opts, nullptr);
|
||||
ASSERT_EQ(opts->size(), 1u);
|
||||
EXPECT_EQ((*opts)[0].getLabel(), "Second");
|
||||
|
||||
ClearTownerDialogOptions();
|
||||
}
|
||||
|
||||
TEST(Stores, RegisterTownerDialogOption_storesOption)
|
||||
{
|
||||
ClearTownerDialogOptions();
|
||||
|
||||
RegisterTownerDialogOption("farnham", []() { return std::string("Go to Tiny Town"); }, []() {});
|
||||
|
||||
auto *opts = FindOptions("farnham");
|
||||
ASSERT_NE(opts, nullptr);
|
||||
ASSERT_EQ(opts->size(), 1u);
|
||||
EXPECT_EQ((*opts)[0].getLabel(), "Go to Tiny Town");
|
||||
|
||||
ClearTownerDialogOptions();
|
||||
}
|
||||
|
||||
TEST(Stores, RegisterTownerDialogOption_callsOnSelect)
|
||||
{
|
||||
ClearTownerDialogOptions();
|
||||
|
||||
bool called = false;
|
||||
RegisterTownerDialogOption("farnham", []() { return std::string("Travel"); }, [&called]() { called = true; });
|
||||
|
||||
auto *opts = FindOptions("farnham");
|
||||
ASSERT_NE(opts, nullptr);
|
||||
ASSERT_EQ(opts->size(), 1u);
|
||||
(*opts)[0].onSelect();
|
||||
EXPECT_TRUE(called);
|
||||
|
||||
ClearTownerDialogOptions();
|
||||
}
|
||||
|
||||
TEST(Stores, RegisterTownerDialogOption_emptyLabelHidesOption)
|
||||
{
|
||||
ClearTownerDialogOptions();
|
||||
|
||||
RegisterTownerDialogOption("farnham", []() { return std::string(""); }, []() {});
|
||||
|
||||
auto *opts = FindOptions("farnham");
|
||||
ASSERT_NE(opts, nullptr);
|
||||
ASSERT_EQ(opts->size(), 1u);
|
||||
EXPECT_TRUE((*opts)[0].getLabel().empty());
|
||||
|
||||
ClearTownerDialogOptions();
|
||||
}
|
||||
|
||||
TEST(Stores, RegisterTownerDialogOption_multipleTowners)
|
||||
{
|
||||
ClearTownerDialogOptions();
|
||||
|
||||
RegisterTownerDialogOption("farnham", []() { return std::string("A"); }, []() {});
|
||||
RegisterTownerDialogOption("griswold", []() { return std::string("B"); }, []() {});
|
||||
|
||||
auto *farnhamOpts = FindOptions("farnham");
|
||||
auto *griswoldOpts = FindOptions("griswold");
|
||||
ASSERT_NE(farnhamOpts, nullptr);
|
||||
ASSERT_NE(griswoldOpts, nullptr);
|
||||
EXPECT_EQ(farnhamOpts->size(), 1u);
|
||||
EXPECT_EQ(griswoldOpts->size(), 1u);
|
||||
EXPECT_EQ((*farnhamOpts)[0].getLabel(), "A");
|
||||
EXPECT_EQ((*griswoldOpts)[0].getLabel(), "B");
|
||||
|
||||
ClearTownerDialogOptions();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user