载入中...
搜索中...
未找到
DialogueControl.cpp
浏览该文件的文档.
2
3#include "common/Capability.h"
5
6#include <algorithm>
7#include <limits>
8#include <string>
9#include <utility>
10
11namespace eve::dialogue {
12namespace {
13
14template <typename T>
15Result<T> failure(DiagnosticCode code, std::string message, std::string path) {
17}
18
19LogicalId id(std::string_view text) { return LogicalId::parse(text).value(); }
20
21Result<const Value::Object*> object(const Value& value) {
22 const auto* result = value.getIf<Value::Object>();
23 if (!result)
24 return failure<const Value::Object*>(DiagnosticCode::InvalidArgument, "dialogue parameters must be an object",
25 "parameters");
27}
28
29Result<std::string> text(const Value::Object& value, std::string_view name, std::string fallback = {}) {
30 const auto found = value.find(std::string(name));
31 if (found == value.end()) return Result<std::string>::success(std::move(fallback));
32 if (!found->second.isString())
33 return failure<std::string>(DiagnosticCode::InvalidArgument, "dialogue parameter must be a string",
34 "parameters." + std::string(name));
35 return Result<std::string>::success(found->second.asString());
36}
37
38const std::string kStart{"dialogue:start"};
39const std::string kAdvance{"dialogue:advance"};
40const std::string kSelect{"dialogue:select"};
41
42} // namespace
43
45 cap::addListener<IGameplayControlProvider>(this);
46 cap::addListener<IGameplayInstanceCatalog>(this);
47}
48
50 cap::removeListener<IGameplayInstanceCatalog>(this);
51 cap::removeListener<IGameplayControlProvider>(this);
52}
53
55 if (!instance.isValid() || !owner.isValid())
57 DiagnosticCode::InvalidArgument, "dialogue publication needs valid instance and owner ids", "instance"));
58 if (entry_.has_value())
61 "the dialogue runner is already published as " + entry_->instance.format(), "instance"));
62 Entry entry;
63 entry.instance = instance;
64 entry.owner = owner;
65 entry_.emplace(std::move(entry));
66 return Result<void>::success();
67}
68
70 if (!entry_.has_value() || entry_->instance != instance)
72 Diagnostic::error(DiagnosticCode::NotFound, "that dialogue instance is not published", "instance"));
73 entry_.reset();
74 return Result<void>::success();
75}
76
77void DialogueControl::clear() { entry_.reset(); }
78
79int DialogueControl::count() const { return entry_.has_value() ? 1 : 0; }
80
81std::vector<SubjectRef> DialogueControl::instances() const {
82 std::vector<SubjectRef> result;
83 if (entry_.has_value()) result.push_back(entry_->instance);
84 return result;
85}
86
87std::string_view DialogueControl::gameplayDomain() const noexcept { return "dialogue"; }
88
89std::vector<SubjectRef> DialogueControl::gameplayInstances() const { return instances(); }
90
91bool DialogueControl::controls(const GameplaySession& session) const {
92 if (!entry_.has_value()) return false;
94 std::find(session.controlledSubjects.begin(), session.controlledSubjects.end(), entry_->owner) !=
95 session.controlledSubjects.end();
96}
97
98Value DialogueControl::conversationState() const {
99 Value::Array routes;
100 if (flow_ != nullptr) {
101 const int routeCount = flow_->getRouteCount();
102 for (int index = 0; index < routeCount; ++index)
103 routes.emplace_back(Value(Value::Object{{"index", Value(static_cast<std::int64_t>(index))},
104 {"route", Value(flow_->getRouteId(index))}}));
105 }
106 const bool active = flow_ != nullptr && flow_->isActive();
107 return Value(Value::Object{{"active", Value(active)},
108 {"blocked", Value(flow_ != nullptr && flow_->isBlocked())},
109 {"conversation", Value(active ? flow_->getConversationId() : std::string{})},
110 {"node", Value(active ? flow_->getNodeId() : std::string{})},
111 {"nodeKind", Value(active ? flow_->getNodeKind() : std::string{})},
112 {"speaker", Value(active ? flow_->getSpeaker() : std::string{})},
113 {"text", Value(active ? flow_->getText() : std::string{})},
114 {"pool", Value(active ? flow_->getPool() : std::string{})},
115 {"i18nKey", Value(active ? flow_->getI18nKey() : std::string{})},
116 {"routes", Value(std::move(routes))},
117 // Automation has no Squirrel call frame, so a conversation
118 // started here sees an empty binding table.
119 {"bindings", Value("empty")}});
120}
121
123 SubjectRef instance) const {
124 if (!entry_.has_value() || entry_->instance != instance || flow_ == nullptr)
125 return failure<GameplayObservation>(DiagnosticCode::NotFound, "dialogue gameplay instance was not found",
126 "instance");
127 if (!controls(session))
128 return failure<GameplayObservation>(DiagnosticCode::PreconditionViolation,
129 "session does not control the dialogue owner", "instance");
130 GameplayObservation observation;
131 observation.domain = id("gameplay:dialogue");
132 observation.instance = entry_->instance;
133 observation.tick = entry_->tick;
134 observation.revision = entry_->revision;
135 observation.state = conversationState();
136 return Result<GameplayObservation>::success(std::move(observation));
137}
138
140 SubjectRef instance,
141 SubjectRef subject) const {
142 auto observed = observeGameplay(session, instance);
143 if (!observed) return Result<std::vector<GameplayActionDescriptor>>::failure(observed.status());
144 if (!entry_.has_value() || subject != entry_->owner)
145 return failure<std::vector<GameplayActionDescriptor>>(DiagnosticCode::PreconditionViolation,
146 "dialogue action subject must be its owner", "subject");
147
148 const Value stringType(Value::Object{{"type", Value("string")}});
149 std::vector<GameplayActionDescriptor> actions{
150 {id(kStart), Value(Value::Object{{"conversation", stringType}})},
151 {id(kAdvance), Value(Value::Object{})},
152 // The route vocabulary is node-dependent, so discovery advertises the
153 // parameter shape and the caller reads the legal routes from `observe`.
154 {id(kSelect), Value(Value::Object{{"route", stringType}})},
155 };
156 return Result<std::vector<GameplayActionDescriptor>>::success(std::move(actions));
157}
158
159void DialogueControl::record(Entry& entry, const GameplayCommand& command, std::string type, std::string detail,
160 std::int64_t quantity) {
161 ++entry.revision;
162 GameplayEvent event;
163 event.sequence = entry.nextEventSequence++;
164 event.tick = entry.tick;
165 event.type = std::move(type);
166 event.subject = entry.owner;
167 event.causationCommandId = command.id;
168 event.correlationId = command.id;
169 event.payload = Value(Value::Object{
170 {"instance", Value(entry.instance.format())},
171 {"detail", Value(detail)},
172 {"quantity", Value(quantity)},
173 {"node", Value(flow_ != nullptr && flow_->isActive() ? flow_->getNodeId() : std::string{})},
174 {"nodeKind", Value(flow_ != nullptr && flow_->isActive() ? flow_->getNodeKind() : std::string{})}});
175 entry.events.push_back(std::move(event));
176}
177
179 const GameplayCommand& command) {
180 if (!entry_.has_value() || entry_->instance != instance || flow_ == nullptr)
181 return failure<GameplayCommandReceipt>(DiagnosticCode::NotFound, "dialogue gameplay instance was not found",
182 "instance");
183 if (!controls(session) || command.subject != entry_->owner)
184 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
185 "session does not control the dialogue owner", "command.subject");
186 if (command.id.empty())
187 return failure<GameplayCommandReceipt>(DiagnosticCode::InvalidArgument, "command id must not be empty",
188 "command.id");
189 if (command.observedTick != entry_->tick || command.expectedRevision != entry_->revision)
190 return failure<GameplayCommandReceipt>(
191 DiagnosticCode::Conflict, "dialogue command was based on a stale observation", "command.expectedRevision");
192 auto values = object(command.parameters);
194 const Value::Object& parameters = *values.value();
195
196 const std::string action = command.action.format();
197 std::string detail;
198 std::string eventType;
199 std::int64_t applied = 0;
200 if (action == kStart) {
201 auto conversation = text(parameters, "conversation");
202 if (!conversation) return Result<GameplayCommandReceipt>::failure(conversation.status());
203 if (conversation.value().empty())
204 return failure<GameplayCommandReceipt>(DiagnosticCode::InvalidArgument,
205 "dialogue:start needs a conversation id",
206 "command.parameters.conversation");
207 if (flow_->isActive())
208 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation,
209 "a conversation is already active", "command.parameters");
210 const auto started = flow_->startChecked(conversation.value());
212 detail = "started " + conversation.value();
213 eventType = "dialogue.started";
214 applied = 1;
215 } else if (action == kAdvance) {
216 if (!flow_->isActive())
217 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation, "no conversation is active",
218 "command.action");
219 const auto advanced = flow_->advanceChecked();
220 if (!advanced) return Result<GameplayCommandReceipt>::failure(advanced.status());
221 detail = flow_->isActive() ? "advanced to " + flow_->getNodeId() : std::string("conversation finished");
222 eventType = flow_->isActive() ? "dialogue.advanced" : "dialogue.finished";
223 applied = 1;
224 } else if (action == kSelect) {
225 auto route = text(parameters, "route");
226 if (!route) return Result<GameplayCommandReceipt>::failure(route.status());
227 if (route.value().empty())
228 return failure<GameplayCommandReceipt>(DiagnosticCode::InvalidArgument, "dialogue:select needs a route id",
229 "command.parameters.route");
230 if (!flow_->isActive())
231 return failure<GameplayCommandReceipt>(DiagnosticCode::PreconditionViolation, "no conversation is active",
232 "command.action");
233 const auto selected = flow_->select(route.value());
234 if (!selected) return Result<GameplayCommandReceipt>::failure(selected.status());
235 detail = "selected route " + route.value();
236 eventType = "dialogue.selected";
237 applied = 1;
238 } else {
239 return failure<GameplayCommandReceipt>(DiagnosticCode::Unsupported, "unsupported dialogue action",
240 "command.action");
241 }
242
243 record(*entry_, command, eventType, detail, applied);
245 receipt.commandId = command.id;
246 receipt.executionId = command.id + ".executed";
247 receipt.acceptedTick = entry_->tick;
248 receipt.resultingRevision = entry_->revision;
249 receipt.details =
250 Value(Value::Object{{"action", Value(action)},
251 {"detail", Value(detail)},
252 {"quantity", Value(applied)},
253 {"active", Value(flow_->isActive())},
254 {"node", Value(flow_->isActive() ? flow_->getNodeId() : std::string{})},
255 {"nodeKind", Value(flow_->isActive() ? flow_->getNodeKind() : std::string{})}});
257}
258
260 const SimulationStep& step) {
261 if (!entry_.has_value() || entry_->instance != instance || flow_ == nullptr)
262 return failure<GameplayObservation>(DiagnosticCode::NotFound, "dialogue gameplay instance was not found",
263 "instance");
264 if (step.tick <= entry_->tick)
265 return failure<GameplayObservation>(DiagnosticCode::Conflict, "dialogue simulation tick must increase",
266 "step.tick");
267 auto observed = observeGameplay(session, instance);
268 if (!observed) return Result<GameplayObservation>::failure(observed.status());
269 entry_->tick = step.tick;
270 return observeGameplay(session, instance);
271}
272
274 std::uint64_t afterSequence) const {
275 auto observed = observeGameplay(session, instance);
276 if (!observed) return Result<std::vector<GameplayEvent>>::failure(observed.status());
277 std::vector<GameplayEvent> result;
278 for (const auto& event : entry_->events)
279 if (event.sequence > afterSequence) result.push_back(event);
280 const bool empty = result.empty();
281 return Result<std::vector<GameplayEvent>>::success(std::move(result),
283}
284
285} // namespace eve::dialogue
double value
bool & active
int subject
Definition AnimSmr.cpp:163
std::map< std::string, Var > values
std::string message
DiagnosticCode code
std::string text
std::string name
std::string action
Definition PlayHost.cpp:117
std::string path
Definition PlayHost.cpp:110
std::vector< ActionSpec > actions
Definition PlayHost.cpp:126
std::string id
Definition PlayHost.cpp:108
int detail
bool found
std::string string
std::optional< InteractionSession > session
Definition Tactics.cpp:39
Json object
float step
Definition TreeMesh.cpp:314
uint32_t index
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
const std::string & format() const noexcept
Returns the canonical namespace:name representation.
Definition Identity.h:436
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Definition Identity.cpp:36
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Strong, domain-neutral reference to a runtime subject.
Definition SubjectRef.h:26
bool isValid() const noexcept
Return whether this reference is valid and non-nil.
Definition SubjectRef.h:38
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
Result< std::vector< GameplayEvent > > gameplayEvents(const GameplaySession &session, SubjectRef instance, std::uint64_t afterSequence) const override
Return events strictly newer than an instance-local sequence cursor.
Result< GameplayCommandReceipt > submitGameplay(const GameplaySession &session, SubjectRef instance, const GameplayCommand &command) override
Validate authority and atomically submit one domain command.
std::string_view gameplayDomain() const noexcept override
Stable provider domain used for discovery and routing.
~DialogueControl() override
Unpublish without touching the borrowed runner.
Result< GameplayObservation > observeGameplay(const GameplaySession &session, SubjectRef instance) const override
Observe one provider-owned gameplay instance without mutation.
Result< void > publish(SubjectRef instance, SubjectRef owner)
Publish the conversation runner under a stable instance identity.
std::vector< SubjectRef > instances() const
Published instance identities, in publication order.
void clear()
Drop every publication.
Result< void > unpublish(SubjectRef instance)
Drop the publication; NotFound when that instance is not published.
std::vector< SubjectRef > gameplayInstances() const override
Instances currently served, in publication order.
Result< GameplayObservation > advanceGameplay(const GameplaySession &session, SubjectRef instance, const SimulationStep &step) override
Advance one provider-owned instance using injected deterministic time.
DialogueControl(DialogueFlow &flow)
Create an adapter for one borrowed conversation runner.
int count() const
Number of published instances (0 or 1).
Result< std::vector< GameplayActionDescriptor > > availableGameplayActions(const GameplaySession &session, SubjectRef instance, SubjectRef subject) const override
Discover currently legal player-semantic actions for one subject.
Script-facing registry and runner for compiled .dnut conversations.
std::string getI18nKey() const
std::string getSpeaker() const
std::string getConversationId(int index) const
eve::Result< void > select(const std::string &routeId)
Select a route, atomically applying configured payment/state effects.
eve::Result< void > advanceChecked()
Advance the active conversation with a structured runner diagnostic.
eve::Result< void > startChecked(const std::string &id, ssq::Object bindings)
Start a conversation after validating script bindings.
std::string getRouteId(int index) const
std::string getNodeId() const
std::string getPool() const
std::string getNodeKind() const
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
Owning evidence that a gameplay authority accepted one command.
One player-semantic command submitted to a domain authority.
std::uint64_t expectedRevision
SimulationTick observedTick
Owning event projection safe across frames and process boundaries.
std::uint64_t sequence
Immutable owning observation of one authoritative gameplay instance.
Owning authorization context shared by player, script and automation adapters.
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158
bool started
Definition Graphics.cpp:183