最终整理版
This commit is contained in:
@@ -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};
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user