最终整理版

This commit is contained in:
2026-06-03 17:04:06 +08:00
commit 959055ce90
1240 changed files with 80570 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
#include "DialogueEffects.h"
DialogueEffectApplyResult ApplyDialogueEffects(
Inventory& inventory,
std::map<std::string, std::string>* questStates,
const std::vector<DialogueEffect>& effects)
{
std::map<std::string, int> requiredItems;
for (const DialogueEffect& effect : effects) {
if (effect.kind != DialogueEffectKind::RemoveItem) {
continue;
}
requiredItems[effect.itemName] += effect.count;
}
for (const auto& [itemName, requiredCount] : requiredItems) {
const int available = ItemCount(inventory, itemName);
if (available < requiredCount) {
return {false, itemName, requiredCount, available};
}
}
for (const DialogueEffect& effect : effects) {
if (effect.kind == DialogueEffectKind::RemoveItem) {
RemoveItem(inventory, effect.itemName, effect.count);
} else if (effect.kind == DialogueEffectKind::AddItem) {
AddItem(inventory, effect.itemName, effect.count);
} else if (effect.kind == DialogueEffectKind::SetQuest && questStates) {
(*questStates)[effect.questName] = effect.questState;
}
}
return {true, {}, 0, 0};
}
DialogueEffectApplyResult ApplyDialogueEffects(Inventory& inventory, const std::vector<DialogueEffect>& effects)
{
return ApplyDialogueEffects(inventory, nullptr, effects);
}
DialogueEffectApplyResult ApplyDialogueEffects(
Inventory& inventory,
std::map<std::string, std::string>& questStates,
const std::vector<DialogueEffect>& effects)
{
return ApplyDialogueEffects(inventory, &questStates, effects);
}
DialogueEffectApplyResult ApplyDialogueEffects(
Inventory& inventory,
QuestStateStore& questStates,
const std::vector<DialogueEffect>& effects)
{
std::map<std::string, int> requiredItems;
for (const DialogueEffect& effect : effects) {
if (effect.kind != DialogueEffectKind::RemoveItem) {
continue;
}
requiredItems[effect.itemName] += effect.count;
}
for (const auto& [itemName, requiredCount] : requiredItems) {
const int available = ItemCount(inventory, itemName);
if (available < requiredCount) {
return {false, itemName, requiredCount, available};
}
}
for (const DialogueEffect& effect : effects) {
if (effect.kind == DialogueEffectKind::RemoveItem) {
RemoveItem(inventory, effect.itemName, effect.count);
} else if (effect.kind == DialogueEffectKind::AddItem) {
AddItem(inventory, effect.itemName, effect.count);
} else if (effect.kind == DialogueEffectKind::SetQuest) {
questStates.Set(effect.questName, effect.questState);
}
}
return {true, {}, 0, 0};
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "DialogueScript.h"
#include "GameCore.h"
#include "QuestSystem.h"
#include <map>
#include <string>
#include <vector>
struct DialogueEffectApplyResult {
bool applied = false;
std::string blockedItem;
int requiredCount = 0;
int availableCount = 0;
};
DialogueEffectApplyResult ApplyDialogueEffects(Inventory& inventory, const std::vector<DialogueEffect>& effects);
DialogueEffectApplyResult ApplyDialogueEffects(
Inventory& inventory,
std::map<std::string, std::string>& questStates,
const std::vector<DialogueEffect>& effects);
DialogueEffectApplyResult ApplyDialogueEffects(
Inventory& inventory,
QuestStateStore& questStates,
const std::vector<DialogueEffect>& effects);
+614
View File
@@ -0,0 +1,614 @@
#include "DialogueScript.h"
#include <fstream>
#include <algorithm>
#include <cctype>
#include <iterator>
#include <mutex>
#include <regex>
#include <set>
#include <sstream>
namespace {
struct PendingBranchRule {
bool active = false;
std::vector<DialogueBranchRule> rules;
};
std::string ReadTextFile(const std::filesystem::path& path)
{
std::ifstream in(path);
if (!in) {
return {};
}
std::ostringstream buffer;
buffer << in.rdbuf();
return buffer.str();
}
std::filesystem::path ResolveScriptPath(const std::filesystem::path& root, const std::string& scriptPath)
{
constexpr const char* prefix = "res://";
if (scriptPath.rfind(prefix, 0) == 0) {
return root / scriptPath.substr(std::char_traits<char>::length(prefix));
}
return root / "assets/scripts" / scriptPath;
}
std::map<std::filesystem::path, DialogueScript>& DialogueScriptCache()
{
static std::map<std::filesystem::path, DialogueScript> cache;
return cache;
}
std::mutex& DialogueScriptCacheMutex()
{
static std::mutex mutex;
return mutex;
}
bool FindCachedDialogueScript(const std::filesystem::path& path, DialogueScript& script)
{
std::lock_guard<std::mutex> lock(DialogueScriptCacheMutex());
const auto cached = DialogueScriptCache().find(path);
if (cached == DialogueScriptCache().end()) {
return false;
}
script = cached->second;
return true;
}
void StoreCachedDialogueScript(const std::filesystem::path& path, const DialogueScript& script)
{
std::lock_guard<std::mutex> lock(DialogueScriptCacheMutex());
DialogueScriptCache()[path] = script;
}
DialogueKind KindForCall(const std::string& call)
{
if (call == "Choice") {
return DialogueKind::Choice;
}
if (call == "Narrate") {
return DialogueKind::Narration;
}
return DialogueKind::Message;
}
std::set<std::string> ParseFunctionNames(const std::string& text)
{
std::set<std::string> names;
const std::regex functionRegex(R"(^\s*func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\()");
std::istringstream lines(text);
std::string line;
std::smatch match;
while (std::getline(lines, line)) {
if (std::regex_search(line, match, functionRegex)) {
names.insert(match[1].str());
}
}
return names;
}
void AddEntry(DialogueScript& script, DialogueEntry entry)
{
if (!entry.functionName.empty() && !script.functionStart.contains(entry.functionName)) {
script.functionStart[entry.functionName] = static_cast<int>(script.entries.size());
}
script.entries.push_back(std::move(entry));
}
std::string UnescapeGodotString(const std::string& value)
{
std::string result;
result.reserve(value.size());
bool escaped = false;
for (const char ch : value) {
if (escaped) {
if (ch == '"' || ch == '\\') {
result.push_back(ch);
} else if (ch == 'n') {
result.push_back('\n');
} else if (ch == 't') {
result.push_back('\t');
} else {
result.push_back(ch);
}
escaped = false;
} else if (ch == '\\') {
escaped = true;
} else {
result.push_back(ch);
}
}
if (escaped) {
result.push_back('\\');
}
return result;
}
std::string Trim(std::string value)
{
const auto notSpace = [](unsigned char ch) {
return !std::isspace(ch);
};
value.erase(value.begin(), std::find_if(value.begin(), value.end(), notSpace));
value.erase(std::find_if(value.rbegin(), value.rend(), notSpace).base(), value.end());
return value;
}
int LineIndent(const std::string& line)
{
int width = 0;
for (const char ch : line) {
if (ch == ' ') {
++width;
} else if (ch == '\t') {
width += 4;
} else {
break;
}
}
return width;
}
std::string ResolveItemArgument(const std::string& raw, const std::map<std::string, std::string>& itemVariables)
{
const std::string value = Trim(raw);
const std::regex directHashRegex(R"(DB\.GetCellHash\(\s*\"((?:\\.|[^\"\\])*)\"\s*\))");
std::smatch match;
if (std::regex_search(value, match, directHashRegex)) {
return UnescapeGodotString(match[1].str());
}
const auto variable = itemVariables.find(value);
return variable == itemVariables.end() ? std::string{} : variable->second;
}
int ResolveCountArgument(const std::string& raw, const std::map<std::string, int>& countVariables)
{
const std::string value = Trim(raw);
if (value.empty()) {
return 1;
}
if (std::all_of(value.begin(), value.end(), [](unsigned char ch) { return std::isdigit(ch); })) {
return std::stoi(value);
}
const auto variable = countVariables.find(value);
return variable == countVariables.end() ? 1 : variable->second;
}
std::string NormalizeScriptSymbol(const std::string& raw, const std::map<std::string, std::string>& symbolVariables)
{
std::string value = Trim(raw);
const std::size_t comment = value.find('#');
if (comment != std::string::npos) {
value = Trim(value.substr(0, comment));
}
const auto variable = symbolVariables.find(value);
return variable == symbolVariables.end() ? value : variable->second;
}
DialogueConditionOp OpForToken(const std::string& op)
{
if (op == "==") {
return DialogueConditionOp::Equal;
}
if (op == "!=") {
return DialogueConditionOp::NotEqual;
}
if (op == "<") {
return DialogueConditionOp::Less;
}
if (op == ">=") {
return DialogueConditionOp::GreaterEqual;
}
return DialogueConditionOp::Default;
}
int ProgressStateValue(const std::string& state)
{
static const std::map<std::string, int> values = {
{"ProgressCommons.UnknownProgress", 0},
{"ProgressCommons.CompletedProgress", 255},
{"ProgressCommons.SPLATYNA_OFFERING.INACTIVE", 0},
{"ProgressCommons.SPLATYNA_OFFERING.STARTED", 1},
{"ProgressCommons.SPLATYNA_OFFERING.REWARDS_WITHDREW", 255},
{"ProgressCommons.GRAIN_IN_THE_SAND.INACTIVE", 0},
{"ProgressCommons.GRAIN_IN_THE_SAND.STARTED", 1},
{"ProgressCommons.GRAIN_IN_THE_SAND.SEARCHED_CRATES", 2},
{"ProgressCommons.GRAIN_IN_THE_SAND.REWARDS_WITHDREW", 255},
{"ProgressCommons.SNAKE_PIT_THIEF.INACTIVE", 0},
{"ProgressCommons.SNAKE_PIT_THIEF.ALL_CLUES_FOUND", 31},
{"ProgressCommons.SNAKE_PIT_THIEF.RIDDLE_SOLVED", 32},
{"ProgressCommons.SNAKE_PIT_THIEF.REWARDS_WITHDREW", 255},
{"ProgressCommons.SNAKE_PIT_BITING_THIRST.INACTIVE", 0},
{"ProgressCommons.SNAKE_PIT_BITING_THIRST.STARTED", 1},
{"ProgressCommons.SNAKE_PIT_BITING_THIRST.REWARDS_WITHDREW", 255},
{"ProgressCommons.SANDSTORM_MINE_ABANDONED_TREASURE.INACTIVE", 0},
{"ProgressCommons.SANDSTORM_MINE_ABANDONED_TREASURE.KEY_FOUND", 1},
{"ProgressCommons.SANDSTORM_MINE_ABANDONED_TREASURE.REWARDS_WITHDREW", 255},
{"ProgressCommons.DESERT_DEEP_XAKELBAEL.INACTIVE", 0},
{"ProgressCommons.DESERT_DEEP_XAKELBAEL.FIGHTING", 1},
{"ProgressCommons.DESERT_DEEP_XAKELBAEL.DEFEATED", 2},
{"ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.INACTIVE", 0},
{"ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.STARTED", 1},
{"ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.ENVELOPES_FOUND", 2},
{"ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.LETTERS_DELIVERED", 3},
{"ProgressCommons.TULIMSHAR_OLD_FRIENDSHIP.REWARDS_WITHDREW", 255},
{"ProgressCommons.TUTORIAL.INACTIVE", 0},
{"ProgressCommons.TUTORIAL.INTRO_ITEMS_GIVEN", 1},
{"ProgressCommons.TUTORIAL.POTION_GIVEN", 2},
{"ProgressCommons.TUTORIAL.CLOTHES_GIVEN", 3},
{"ProgressCommons.TUTORIAL.UI_EXPLAINED", 4},
{"ProgressCommons.TUTORIAL.ELANORE_DONE", 5},
{"ProgressCommons.TUTORIAL.KAEL_MET", 6},
{"ProgressCommons.TUTORIAL.KAEL_DONE", 7},
{"ProgressCommons.TUTORIAL.EKINU_DONE", 255},
{"ProgressCommons.ELANORE_POTION.INACTIVE", 0},
{"ProgressCommons.ELANORE_POTION.STARTED", 1},
{"ProgressCommons.NINA_HUNGRY.INACTIVE", 0},
{"ProgressCommons.NINA_HUNGRY.STARTED", 1},
{"ProgressCommons.NINA_HUNGRY.REWARDS_WITHDREW", 255},
{"ProgressCommons.MINE_EXPLORATION.INACTIVE", 0},
{"ProgressCommons.MINE_EXPLORATION.STARTED", 1},
{"ProgressCommons.MINE_EXPLORATION.REWARDS_WITHDREW", 255},
{"ProgressCommons.SANDSTORM_NATHAN_WATER.INACTIVE", 0},
{"ProgressCommons.SANDSTORM_NATHAN_WATER.STARTED", 1},
{"ProgressCommons.SANDSTORM_NATHAN_WATER.REWARDS_WITHDREW", 255},
};
const auto value = values.find(state);
if (value != values.end()) {
return value->second;
}
if (state.ends_with(".INACTIVE")) {
return 0;
}
if (state.ends_with(".REWARDS_WITHDREW")) {
return 255;
}
return 0;
}
bool BranchMatches(const DialogueBranchRule& rule, const std::map<std::string, std::string>& questStates)
{
if (rule.op == DialogueConditionOp::Default) {
return true;
}
const auto current = questStates.find(rule.questName);
const int currentValue = current == questStates.end() ? 0 : ProgressStateValue(current->second);
const int expectedValue = ProgressStateValue(rule.questState);
switch (rule.op) {
case DialogueConditionOp::Equal:
return currentValue == expectedValue;
case DialogueConditionOp::NotEqual:
return currentValue != expectedValue;
case DialogueConditionOp::Less:
return currentValue < expectedValue;
case DialogueConditionOp::GreaterEqual:
return currentValue >= expectedValue;
case DialogueConditionOp::Default:
return true;
}
return false;
}
std::vector<DialogueBranchRule> MakeMatchCaseRules(
const std::string& questName,
const std::string& rawStates,
const std::map<std::string, std::string>& symbolVariables)
{
std::vector<DialogueBranchRule> rules;
const std::regex stateRegex(R"([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*|_)");
for (auto it = std::sregex_iterator(rawStates.begin(), rawStates.end(), stateRegex);
it != std::sregex_iterator();
++it) {
DialogueBranchRule rule;
rule.questName = questName;
const std::string state = (*it)[0].str();
if (state == "_") {
rule.op = DialogueConditionOp::Default;
} else {
rule.op = DialogueConditionOp::Equal;
rule.questState = NormalizeScriptSymbol(state, symbolVariables);
}
rules.push_back(std::move(rule));
}
return rules;
}
} // namespace
DialogueScript LoadDialogueScript(const std::filesystem::path& root, const std::string& scriptPath)
{
DialogueScript script;
const std::filesystem::path path = std::filesystem::weakly_canonical(ResolveScriptPath(root, scriptPath));
if (FindCachedDialogueScript(path, script)) {
return script;
}
const std::string text = ReadTextFile(path);
if (text.empty()) {
StoreCachedDialogueScript(path, script);
return script;
}
const std::set<std::string> functionNames = ParseFunctionNames(text);
const std::regex functionRegex(R"(^\s*func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\()");
const std::regex callRegex(R"(\b(Mes|Chat|Narrate|Choice)\s*\(\s*\"((?:\\.|[^\"\\])*)\"\s*(?:,\s*([A-Za-z_][A-Za-z0-9_]*))?)");
const std::regex directCallRegex(R"(^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\()");
const std::regex itemVariableRegex(R"(\b(?:var|const)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=]+)?=\s*DB\.GetCellHash\(\s*\"((?:\\.|[^\"\\])*)\"\s*\))");
const std::regex countVariableRegex(R"(\b(?:var|const)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=]+)?=\s*([0-9]+)\b)");
const std::regex symbolVariableRegex(R"(\b(?:var|const)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=]+)?=\s*(ProgressCommons\.[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\b)");
const std::regex questVariableRegex(R"(\b(?:var\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=]+)?=\s*GetQuest\(\s*([^)]+)\))");
const std::regex matchVariableRegex(R"(^\s*match\s+([A-Za-z_][A-Za-z0-9_]*)\s*:)");
const std::regex matchQuestRegex(R"(^\s*match\s+GetQuest\(\s*([^)]+)\)\s*:)");
const std::regex matchCaseContinuationRegex(R"(^\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*|_)\s*,\s*\\?\s*$)");
const std::regex matchCaseRegex(R"(^\s*((?:[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*|_)(?:\s*,\s*\\?\s*(?:[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*|_))*)\s*:\s*(?:([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\))?)");
const std::regex branchConditionRegex(R"(^\s*(?:if|elif)\s+([A-Za-z_][A-Za-z0-9_]*)\s*(==|!=|<|>=)\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)[^:]*:\s*(?:([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\))?)");
const std::regex elseRegex(R"(^\s*else\s*:\s*(?:([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\))?)");
const std::regex genericConditionalRegex(R"(^\s*(?:if|elif|else|match)\b.*:\s*)");
const std::regex itemEffectRegex(R"(\b(AddItem|RemoveItem)\s*\(\s*(DB\.GetCellHash\(\s*\"(?:\\.|[^\"\\])*\"\s*\)|[A-Za-z_][A-Za-z0-9_]*)\s*(?:,\s*([A-Za-z_][A-Za-z0-9_]*|[0-9]+))?)");
const std::regex setQuestRegex(R"(\bSetQuest\s*\(\s*([^,]+)\s*,\s*([^)]+)\))");
std::istringstream lines(text);
std::string line;
std::string currentFunction;
std::map<std::string, std::string> itemVariables;
std::map<std::string, int> countVariables;
std::map<std::string, std::string> symbolVariables;
std::map<std::string, std::string> questVariables;
std::string activeMatchQuestName;
std::vector<DialogueBranchRule> pendingMatchCaseRules;
std::vector<int> conditionalBlockIndents;
PendingBranchRule pendingBranch;
std::smatch match;
while (std::getline(lines, line)) {
if (std::regex_search(line, match, functionRegex)) {
currentFunction = match[1].str();
questVariables.clear();
activeMatchQuestName.clear();
pendingMatchCaseRules.clear();
conditionalBlockIndents.clear();
pendingBranch = {};
continue;
}
const std::string trimmedLine = Trim(line);
const int indent = LineIndent(line);
if (!trimmedLine.empty()) {
while (!conditionalBlockIndents.empty() && indent <= conditionalBlockIndents.back()) {
conditionalBlockIndents.pop_back();
}
}
const bool insideConditionalBlock = !conditionalBlockIndents.empty();
bool opensConditionalBlock = false;
if (std::regex_search(line, match, itemVariableRegex)) {
itemVariables[match[1].str()] = UnescapeGodotString(match[2].str());
}
if (std::regex_search(line, match, countVariableRegex)) {
countVariables[match[1].str()] = std::stoi(match[2].str());
}
if (std::regex_search(line, match, symbolVariableRegex)) {
symbolVariables[match[1].str()] = match[2].str();
}
if (std::regex_search(line, match, questVariableRegex)) {
questVariables[match[1].str()] = NormalizeScriptSymbol(match[2].str(), symbolVariables);
}
if (!trimmedLine.empty() && trimmedLine.front() == '#') {
continue;
}
if (std::regex_search(line, match, matchVariableRegex)) {
const auto quest = questVariables.find(match[1].str());
activeMatchQuestName = quest == questVariables.end() ? std::string{} : quest->second;
pendingMatchCaseRules.clear();
opensConditionalBlock = true;
} else if (std::regex_search(line, match, matchQuestRegex)) {
activeMatchQuestName = NormalizeScriptSymbol(match[1].str(), symbolVariables);
pendingMatchCaseRules.clear();
opensConditionalBlock = true;
} else if (!activeMatchQuestName.empty() && std::regex_search(line, match, matchCaseContinuationRegex)) {
std::vector<DialogueBranchRule> rules = MakeMatchCaseRules(activeMatchQuestName, match[1].str(), symbolVariables);
pendingMatchCaseRules.insert(
pendingMatchCaseRules.end(),
std::make_move_iterator(rules.begin()),
std::make_move_iterator(rules.end()));
} else if (!activeMatchQuestName.empty() && std::regex_search(line, match, matchCaseRegex)) {
opensConditionalBlock = true;
std::vector<DialogueBranchRule> rules = std::move(pendingMatchCaseRules);
pendingMatchCaseRules.clear();
std::vector<DialogueBranchRule> lineRules = MakeMatchCaseRules(activeMatchQuestName, match[1].str(), symbolVariables);
rules.insert(
rules.end(),
std::make_move_iterator(lineRules.begin()),
std::make_move_iterator(lineRules.end()));
if (match[2].matched && functionNames.contains(match[2].str())) {
for (DialogueBranchRule& rule : rules) {
rule.targetFunction = match[2].str();
script.functionBranchRules[currentFunction].push_back(rule);
}
} else {
pendingBranch.active = true;
pendingBranch.rules = std::move(rules);
}
} else if (std::regex_search(line, match, branchConditionRegex)) {
opensConditionalBlock = true;
const auto quest = questVariables.find(match[1].str());
if (quest != questVariables.end()) {
DialogueBranchRule rule;
rule.questName = quest->second;
rule.op = OpForToken(match[2].str());
rule.questState = NormalizeScriptSymbol(match[3].str(), symbolVariables);
if (match[4].matched && functionNames.contains(match[4].str())) {
rule.targetFunction = match[4].str();
script.functionBranchRules[currentFunction].push_back(rule);
} else {
pendingBranch.active = true;
pendingBranch.rules = {std::move(rule)};
}
}
} else if (std::regex_search(line, match, elseRegex)) {
opensConditionalBlock = true;
DialogueBranchRule rule;
rule.op = DialogueConditionOp::Default;
if (match[1].matched && functionNames.contains(match[1].str())) {
rule.targetFunction = match[1].str();
script.functionBranchRules[currentFunction].push_back(rule);
} else {
pendingBranch.active = true;
pendingBranch.rules = {std::move(rule)};
}
} else if (std::regex_search(line, genericConditionalRegex)) {
opensConditionalBlock = true;
}
if (std::regex_search(line, match, setQuestRegex) && !currentFunction.empty()) {
DialogueEffect effect;
effect.kind = DialogueEffectKind::SetQuest;
effect.questName = NormalizeScriptSymbol(match[1].str(), symbolVariables);
effect.questState = NormalizeScriptSymbol(match[2].str(), symbolVariables);
if (!effect.questName.empty() && !effect.questState.empty()) {
script.functionEffects[currentFunction].push_back(std::move(effect));
}
}
auto effectBegin = std::sregex_iterator(line.begin(), line.end(), itemEffectRegex);
auto effectEnd = std::sregex_iterator();
for (auto it = effectBegin; it != effectEnd; ++it) {
if (currentFunction.empty()) {
continue;
}
DialogueEffect effect;
effect.kind = (*it)[1].str() == "RemoveItem" ? DialogueEffectKind::RemoveItem : DialogueEffectKind::AddItem;
effect.itemName = ResolveItemArgument((*it)[2].str(), itemVariables);
effect.count = ResolveCountArgument((*it)[3].matched ? (*it)[3].str() : std::string{}, countVariables);
if (!effect.itemName.empty() && effect.count > 0) {
script.functionEffects[currentFunction].push_back(std::move(effect));
}
}
bool parsedDialogueCall = false;
auto begin = std::sregex_iterator(line.begin(), line.end(), callRegex);
auto end = std::sregex_iterator();
for (auto it = begin; it != end; ++it) {
DialogueEntry entry;
entry.kind = KindForCall((*it)[1].str());
entry.text = UnescapeGodotString((*it)[2].str());
entry.functionName = currentFunction;
if (entry.kind == DialogueKind::Choice) {
entry.targetFunction = (*it)[3].str();
}
AddEntry(script, std::move(entry));
parsedDialogueCall = true;
}
if (!parsedDialogueCall && std::regex_search(line, match, directCallRegex)) {
const std::string target = match[1].str();
if (functionNames.contains(target)) {
if (pendingBranch.active) {
for (DialogueBranchRule& rule : pendingBranch.rules) {
rule.targetFunction = target;
script.functionBranchRules[currentFunction].push_back(rule);
}
pendingBranch = {};
}
if (!insideConditionalBlock) {
DialogueEntry entry;
entry.kind = DialogueKind::Jump;
entry.text = target;
entry.targetFunction = target;
entry.functionName = currentFunction;
AddEntry(script, std::move(entry));
}
}
}
if (opensConditionalBlock) {
conditionalBlockIndents.push_back(indent);
}
}
StoreCachedDialogueScript(path, script);
return script;
}
void ClearDialogueScriptCache()
{
std::lock_guard<std::mutex> lock(DialogueScriptCacheMutex());
DialogueScriptCache().clear();
}
std::vector<DialogueChoice> DialogueChoicesAt(const DialogueScript& script, int entryIndex)
{
std::vector<DialogueChoice> choices;
if (entryIndex < 0 || entryIndex >= static_cast<int>(script.entries.size())) {
return choices;
}
const DialogueEntry& first = script.entries[static_cast<std::size_t>(entryIndex)];
if (first.kind != DialogueKind::Choice) {
return choices;
}
const std::string functionName = first.functionName;
for (int i = entryIndex; i < static_cast<int>(script.entries.size()); ++i) {
const DialogueEntry& entry = script.entries[static_cast<std::size_t>(i)];
if (entry.kind != DialogueKind::Choice || entry.functionName != functionName) {
break;
}
choices.push_back({i, entry.text, entry.targetFunction});
}
return choices;
}
int ResolveDialogueChoiceTarget(const DialogueScript& script, const DialogueChoice& choice)
{
const auto target = script.functionStart.find(choice.targetFunction);
if (target != script.functionStart.end()) {
return target->second;
}
if (!choice.targetFunction.empty()) {
return -1;
}
const std::vector<DialogueChoice> group = DialogueChoicesAt(script, choice.entryIndex);
if (!group.empty()) {
return group.back().entryIndex + 1;
}
return choice.entryIndex + 1;
}
int SkipDialogueJumps(const DialogueScript& script, int entryIndex)
{
std::set<int> visited;
int index = entryIndex;
while (index >= 0 && index < static_cast<int>(script.entries.size())) {
if (!visited.insert(index).second) {
return index;
}
const DialogueEntry& entry = script.entries[static_cast<std::size_t>(index)];
if (entry.kind != DialogueKind::Jump) {
return index;
}
const auto target = script.functionStart.find(entry.targetFunction);
if (target == script.functionStart.end()) {
return index + 1;
}
index = target->second;
}
return index;
}
std::string ResolveDialogueStartFunction(
const DialogueScript& script,
const std::map<std::string, std::string>& questStates,
const std::string& functionName)
{
const auto rules = script.functionBranchRules.find(functionName);
if (rules == script.functionBranchRules.end()) {
return functionName;
}
for (const DialogueBranchRule& rule : rules->second) {
if (!rule.targetFunction.empty() && BranchMatches(rule, questStates)) {
return rule.targetFunction;
}
}
return functionName;
}
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include <filesystem>
#include <map>
#include <string>
#include <vector>
enum class DialogueKind {
Message,
Choice,
Narration,
Jump
};
enum class DialogueEffectKind {
AddItem,
RemoveItem,
SetQuest
};
enum class DialogueConditionOp {
Equal,
NotEqual,
Less,
GreaterEqual,
Default
};
struct DialogueEffect {
DialogueEffectKind kind = DialogueEffectKind::AddItem;
std::string itemName;
int count = 1;
std::string questName;
std::string questState;
};
struct DialogueBranchRule {
std::string questName;
DialogueConditionOp op = DialogueConditionOp::Default;
std::string questState;
std::string targetFunction;
};
struct DialogueEntry {
DialogueKind kind = DialogueKind::Message;
std::string text;
std::string functionName;
std::string targetFunction;
};
struct DialogueScript {
std::vector<DialogueEntry> entries;
std::map<std::string, int> functionStart;
std::map<std::string, std::vector<DialogueEffect>> functionEffects;
std::map<std::string, std::vector<DialogueBranchRule>> functionBranchRules;
};
struct DialogueChoice {
int entryIndex = 0;
std::string text;
std::string targetFunction;
};
DialogueScript LoadDialogueScript(const std::filesystem::path& root, const std::string& scriptPath);
void ClearDialogueScriptCache();
std::vector<DialogueChoice> DialogueChoicesAt(const DialogueScript& script, int entryIndex);
int ResolveDialogueChoiceTarget(const DialogueScript& script, const DialogueChoice& choice);
int SkipDialogueJumps(const DialogueScript& script, int entryIndex);
std::string ResolveDialogueStartFunction(
const DialogueScript& script,
const std::map<std::string, std::string>& questStates,
const std::string& functionName);
+91
View File
@@ -0,0 +1,91 @@
#include "ScriptedInteractable.h"
#include <regex>
namespace {
bool ContainsText(const std::string& text, const std::string& needle)
{
return text.find(needle) != std::string::npos;
}
} // namespace
int SnakePitClueNumber(const std::string& playerScript)
{
const std::regex clueRegex(R"(Clue([1-5])\.gd)");
std::smatch match;
return std::regex_search(playerScript, match, clueRegex) ? std::stoi(match[1].str()) : 0;
}
ScriptedInteractableKind DetectScriptedInteractable(
const std::string& objectName,
const std::string& playerScript,
const std::string& ownScript)
{
if (SnakePitClueNumber(playerScript) > 0) {
return ScriptedInteractableKind::SnakePitClue;
}
if (ContainsText(playerScript, "ThiefsChest.gd") || ContainsText(objectName, "Thiefs Chest")) {
return ScriptedInteractableKind::ThiefsChest;
}
if (ContainsText(playerScript, "WaterPondFilthy.gd")) {
return ScriptedInteractableKind::FilthyWaterPond;
}
if (ContainsText(playerScript, "WaterPond.gd") || ContainsText(ownScript, "WaterPondGlobal.gd")) {
return ScriptedInteractableKind::CleanWaterPond;
}
return ScriptedInteractableKind::None;
}
ScriptedInteractionResult ApplyScriptedInteractableReward(
Inventory& inventory,
ScriptedInteractableKind kind,
ScriptedInteractionContext context)
{
ScriptedInteractionResult result;
result.handled = kind != ScriptedInteractableKind::None;
if (!result.handled) {
return result;
}
switch (kind) {
case ScriptedInteractableKind::SnakePitClue:
result.success = true;
result.consumeObject = true;
if (context.allSnakePitCluesFound && !HasItem(inventory, "Thief's Key")) {
AddItem(inventory, "Thief's Key", 1);
result.message = "五个蛇坑线索已拼合 获得" + ItemDisplayName("Thief's Key");
} else {
result.message = "记下了蛇坑线索";
}
return result;
case ScriptedInteractableKind::ThiefsChest:
if (!RemoveItem(inventory, "Thief's Key", 1)) {
result.success = false;
result.consumeObject = false;
result.message = "盗贼宝箱上锁 需要" + ItemDisplayName("Thief's Key");
return result;
}
AddItem(inventory, "Scimitar", 1);
result.success = true;
result.consumeObject = true;
result.message = "打开盗贼宝箱 获得" + ItemDisplayName("Scimitar");
return result;
case ScriptedInteractableKind::CleanWaterPond:
AddItem(inventory, "Water Bottle", 1);
result.success = true;
result.consumeObject = true;
result.message = "在清水池装满" + ItemDisplayName("Water Bottle");
return result;
case ScriptedInteractableKind::FilthyWaterPond:
result.success = false;
result.consumeObject = false;
result.message = "这处水池太脏 不能装水";
return result;
case ScriptedInteractableKind::None:
break;
}
result.handled = false;
return result;
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "GameCore.h"
#include <string>
enum class ScriptedInteractableKind {
None,
SnakePitClue,
ThiefsChest,
CleanWaterPond,
FilthyWaterPond
};
struct ScriptedInteractionContext {
bool allSnakePitCluesFound = false;
};
struct ScriptedInteractionResult {
bool handled = false;
bool success = false;
bool consumeObject = false;
std::string message;
};
ScriptedInteractableKind DetectScriptedInteractable(
const std::string& objectName,
const std::string& playerScript,
const std::string& ownScript);
int SnakePitClueNumber(const std::string& playerScript);
ScriptedInteractionResult ApplyScriptedInteractableReward(
Inventory& inventory,
ScriptedInteractableKind kind,
ScriptedInteractionContext context);