载入中...
搜索中...
未找到
Crafting.cpp
浏览该文件的文档.
1#include "rpg/Crafting.h"
2
3#include "inventory/Bag.h"
5
6#include <array>
7#include <limits>
8#include <memory>
9#include <optional>
10#include <span>
11#include <utility>
12
13namespace eve::rpg {
14namespace {
15
16template <class T>
17eve::Result<T> failure(eve::DiagnosticCode code, std::string message, std::string path = {}) {
19}
20
21eve::Value encodeReservation(const CraftingRecipe& recipe, std::uint32_t batchSize) {
22 eve::Value::Array outputs;
23 outputs.reserve(recipe.outputs.size());
24 for (const auto& output : recipe.outputs) {
25 outputs.emplace_back(eve::Value::Object{
26 {"item", eve::Value(output.itemId)},
27 {"quantity", eve::Value(std::int64_t(output.quantity) * static_cast<std::int64_t>(batchSize))}});
28 }
29 return eve::Value(eve::Value::Object{{"schema", eve::Value("eve.rpg.crafting-reservation")},
30 {"version", eve::Value(std::int64_t(1))},
31 {"outputs", eve::Value(std::move(outputs))}});
32}
33
35 const eve::resource::CostSpec& source, std::uint32_t batchSize) {
36 const auto multiplier = static_cast<std::int64_t>(batchSize);
37 std::vector<eve::resource::ResourceCost> items;
38 items.reserve(source.items().size());
39 for (const auto& item : source.items()) {
40 if (item.amount.value() > std::numeric_limits<std::int64_t>::max() / multiplier)
41 return failure<eve::resource::CostSpec>(eve::DiagnosticCode::InvalidArgument,
42 "crafting batch ingredient quantity overflows", "batchSize");
44 item.resource.value(), item.amount.value() * multiplier);
45 if (!scaled) return eve::Result<eve::resource::CostSpec>::failure(scaled.status());
46 items.push_back(std::move(scaled).takeValue());
47 }
48 return eve::resource::CostSpec::create(std::move(items));
49}
50
52 const auto* object = reservation.getIf<eve::Value::Object>();
53 if (object == nullptr)
54 return failure<std::vector<inventory::InventoryItemGrant>>(
55 eve::DiagnosticCode::ParseError, "crafting reservation must be an object", "reservation");
56 const auto schema = object->find("schema");
57 const auto version = object->find("version");
58 const auto outputs = object->find("outputs");
59 if (schema == object->end() || !schema->second.isString() ||
60 schema->second.asString() != "eve.rpg.crafting-reservation" || version == object->end() ||
61 !version->second.isInt64() || version->second.asInt() != 1 || outputs == object->end() ||
62 !outputs->second.isArray())
63 return failure<std::vector<inventory::InventoryItemGrant>>(
64 eve::DiagnosticCode::ParseError, "crafting reservation schema is invalid", "reservation");
65 std::vector<inventory::InventoryItemGrant> result;
66 const auto* outputValues = outputs->second.getIf<eve::Value::Array>();
67 for (const auto& value : *outputValues) {
68 const auto* entry = value.getIf<eve::Value::Object>();
69 if (entry == nullptr)
70 return failure<std::vector<inventory::InventoryItemGrant>>(
71 eve::DiagnosticCode::ParseError, "crafting output must be an object", "reservation.outputs");
72 const auto item = entry->find("item");
73 const auto quantity = entry->find("quantity");
74 if (item == entry->end() || !item->second.isString() || item->second.asString().empty() ||
75 quantity == entry->end() || !quantity->second.isInt64() || quantity->second.asInt() <= 0 ||
76 quantity->second.asInt() > std::numeric_limits<int>::max())
77 return failure<std::vector<inventory::InventoryItemGrant>>(
78 eve::DiagnosticCode::ParseError, "crafting output fields are invalid", "reservation.outputs");
79 result.push_back({item->second.asString(), static_cast<int>(quantity->second.asInt())});
80 }
81 if (result.empty())
82 return failure<std::vector<inventory::InventoryItemGrant>>(
83 eve::DiagnosticCode::InvalidArgument, "crafting reservation has no outputs", "reservation.outputs");
84 return eve::Result<std::vector<inventory::InventoryItemGrant>>::success(std::move(result));
85}
86
87class ProductionParticipant final : public transaction::ITransactionParticipant {
88public:
89 ProductionParticipant(production::WorkQueue& queue, production::ProductionRequest request)
90 : queue_(queue), request_(std::move(request)) {}
91
92 [[nodiscard]] std::string_view name() const noexcept override { return "rpg-crafting-production"; }
93
94 [[nodiscard]] eve::Result<void> prepare(const transaction::TransactionContext&) override {
95 auto snapshot = queue_.snapshot();
96 if (!snapshot) return eve::Result<void>::failure(snapshot.status());
97 before_ = std::move(snapshot).takeValue();
98 staged_ = std::make_unique<production::WorkQueue>();
99 auto restored = staged_->restore(before_);
100 if (!restored) return restored;
101 auto enqueued = staged_->enqueue(request_);
102 if (!enqueued) return eve::Result<void>::failure(enqueued.status());
103 taskId_ = std::move(enqueued).takeValue();
104 auto after = staged_->snapshot();
105 if (!after) return eve::Result<void>::failure(after.status());
106 after_ = std::move(after).takeValue();
107 prepared_ = true;
109 }
110
111 [[nodiscard]] eve::Result<void> commit(const transaction::TransactionContext&) override {
112 if (!prepared_ || committed_)
113 return failure<void>(eve::DiagnosticCode::Conflict, "crafting production is not prepared", "production");
114 auto current = queue_.snapshot();
115 if (!current) return eve::Result<void>::failure(current.status());
116 if (current.value() != before_)
117 return failure<void>(eve::DiagnosticCode::StaleHandle,
118 "crafting production queue changed while staged", "production");
119 auto restored = queue_.restore(after_);
120 if (!restored) return restored;
121 committed_ = true;
123 }
124
125 [[nodiscard]] eve::Result<void> rollback(const transaction::TransactionContext&) override {
126 if (committed_)
127 return failure<void>(eve::DiagnosticCode::Conflict, "committed crafting task requires compensation");
128 staged_.reset();
129 prepared_ = false;
131 }
132
133 [[nodiscard]] eve::Result<void> compensate(const transaction::TransactionContext&) override {
134 if (!committed_)
135 return failure<void>(eve::DiagnosticCode::Conflict, "crafting task has not committed");
136 auto current = queue_.snapshot();
137 if (!current) return eve::Result<void>::failure(current.status());
138 if (current.value() != after_)
139 return failure<void>(eve::DiagnosticCode::StaleHandle,
140 "crafting production queue changed before compensation", "production");
141 auto restored = queue_.restore(before_);
142 if (!restored) return restored;
143 staged_.reset();
144 prepared_ = false;
145 committed_ = false;
147 }
148
149 [[nodiscard]] const std::string& taskId() const noexcept { return taskId_; }
150
151private:
152 production::WorkQueue& queue_;
153 production::ProductionRequest request_;
154 std::unique_ptr<production::WorkQueue> staged_;
155 std::string before_;
156 std::string after_;
157 std::string taskId_;
158 bool prepared_ = false;
159 bool committed_ = false;
160};
161
162class SettlementParticipant final : public transaction::ITransactionParticipant {
163public:
164 SettlementParticipant(production::WorkQueue& queue, std::string taskId,
165 production::ProductionSettlementReceipt receipt)
166 : queue_(queue), taskId_(std::move(taskId)), receipt_(std::move(receipt)) {}
167
168 [[nodiscard]] std::string_view name() const noexcept override { return "rpg-crafting-settlement"; }
169
170 [[nodiscard]] eve::Result<void> prepare(const transaction::TransactionContext&) override {
171 auto before = queue_.snapshot();
172 if (!before) return eve::Result<void>::failure(before.status());
173 before_ = std::move(before).takeValue();
174 production::WorkQueue staged;
175 auto restored = staged.restore(before_);
176 if (!restored) return restored;
177 const auto task = staged.find(taskId_);
178 if (!task)
179 return failure<void>(eve::DiagnosticCode::NotFound, "crafting task was not found", "taskId");
180 if (task->get().state == production::TaskState::SettlementFailed) {
181 auto retried = staged.retrySettlement(taskId_);
182 if (!retried) return retried;
183 }
184 auto settled = staged.settle(taskId_, receipt_);
185 if (!settled) return settled;
186 auto after = staged.snapshot();
187 if (!after) return eve::Result<void>::failure(after.status());
188 after_ = std::move(after).takeValue();
189 prepared_ = true;
191 }
192
193 [[nodiscard]] eve::Result<void> commit(const transaction::TransactionContext&) override {
194 if (!prepared_ || committed_)
195 return failure<void>(eve::DiagnosticCode::Conflict, "crafting settlement is not prepared");
196 auto current = queue_.snapshot();
197 if (!current) return eve::Result<void>::failure(current.status());
198 if (current.value() != before_)
199 return failure<void>(eve::DiagnosticCode::StaleHandle,
200 "crafting queue changed while settlement was staged", "production");
201 auto restored = queue_.restore(after_);
202 if (!restored) return restored;
203 committed_ = true;
205 }
206
207 [[nodiscard]] eve::Result<void> rollback(const transaction::TransactionContext&) override {
208 prepared_ = false;
210 }
211
212 [[nodiscard]] eve::Result<void> compensate(const transaction::TransactionContext&) override {
213 if (!committed_)
214 return failure<void>(eve::DiagnosticCode::Conflict, "crafting settlement has not committed");
215 auto current = queue_.snapshot();
216 if (!current) return eve::Result<void>::failure(current.status());
217 if (current.value() != after_)
218 return failure<void>(eve::DiagnosticCode::StaleHandle,
219 "crafting queue changed before settlement compensation", "production");
220 auto restored = queue_.restore(before_);
221 if (!restored) return restored;
222 committed_ = false;
223 prepared_ = false;
225 }
226
227private:
228 production::WorkQueue& queue_;
229 std::string taskId_;
230 production::ProductionSettlementReceipt receipt_;
231 std::string before_;
232 std::string after_;
233 bool prepared_ = false;
234 bool committed_ = false;
235};
236
237class InventoryAddParticipant final : public transaction::ITransactionParticipant {
238public:
239 InventoryAddParticipant(inventory::Bag& output, std::vector<inventory::InventoryItemGrant> grants)
240 : output_(output), grants_(std::move(grants)) {}
241
242 [[nodiscard]] std::string_view name() const noexcept override { return "rpg-crafting-output"; }
243
244 [[nodiscard]] eve::Result<void> prepare(const transaction::TransactionContext&) override {
245 auto prepared = inventory::InventorySystem::prepareAddBatch(&output_, grants_);
246 if (!prepared) return eve::Result<void>::failure(prepared.status());
247 prepared_.emplace(std::move(prepared).takeValue());
249 }
250
251 [[nodiscard]] eve::Result<void> commit(const transaction::TransactionContext&) override {
252 if (!prepared_)
253 return failure<void>(eve::DiagnosticCode::Conflict, "crafting output is not prepared", "output");
254 auto committed = inventory::InventorySystem::commitAddBatch(std::move(*prepared_));
255 prepared_.reset();
256 if (!committed) return eve::Result<void>::failure(committed.status());
257 added_ = std::move(committed).takeValue();
259 }
260
261 [[nodiscard]] eve::Result<void> rollback(const transaction::TransactionContext&) override {
262 prepared_.reset();
264 }
265
266 [[nodiscard]] eve::Result<void> compensate(const transaction::TransactionContext&) override {
267 return failure<void>(eve::DiagnosticCode::Unsupported,
268 "committed crafting output cannot be compensated", "output");
269 }
270
271 [[nodiscard]] int added() const noexcept { return added_; }
272
273private:
274 inventory::Bag& output_;
275 std::vector<inventory::InventoryItemGrant> grants_;
276 std::optional<inventory::PreparedInventoryAdd> prepared_;
277 int added_ = 0;
278};
279
280} // namespace
281
283 if (request.production == nullptr || request.ingredients == nullptr)
284 return failure<CraftingReceipt>(eve::DiagnosticCode::InvalidArgument,
285 "crafting requires production and ingredient account ports", "request");
286 if (request.owner.empty() || request.recipe.id.empty() || request.recipe.process.empty())
287 return failure<CraftingReceipt>(eve::DiagnosticCode::InvalidArgument,
288 "crafting owner, recipe id and process are required", "request");
289 if (!request.recipe.ingredients.isValid() || request.recipe.outputs.empty() || request.batchSize == 0 ||
290 request.recipe.duration.nanoseconds() <= 0)
291 return failure<CraftingReceipt>(eve::DiagnosticCode::InvalidArgument,
292 "crafting recipe requires ingredients, outputs and positive duration",
293 "recipe");
294 std::int64_t totalOutput = 0;
295 for (const auto& output : request.recipe.outputs) {
296 const auto quantity = static_cast<std::int64_t>(output.quantity);
297 const auto batchSize = static_cast<std::int64_t>(request.batchSize);
298 if (output.itemId.empty() || quantity <= 0 ||
299 quantity > static_cast<std::int64_t>(std::numeric_limits<int>::max()) / batchSize)
300 return failure<CraftingReceipt>(eve::DiagnosticCode::InvalidArgument,
301 "crafting outputs require item ids and positive quantities",
302 "recipe.outputs");
303 const auto scaled = quantity * batchSize;
304 if (totalOutput > std::numeric_limits<int>::max() - scaled)
305 return failure<CraftingReceipt>(eve::DiagnosticCode::InvalidArgument,
306 "crafting batch output total exceeds inventory limits",
307 "recipe.outputs");
308 totalOutput += scaled;
309 }
310
311 production::ProductionRequest productionRequest;
312 productionRequest.owner = std::move(request.owner);
313 productionRequest.kind = request.recipe.process;
314 productionRequest.product = request.recipe.id;
315 productionRequest.duration = request.recipe.duration;
316 productionRequest.priority = request.priority;
317 productionRequest.definition = request.recipe.definition;
318 productionRequest.reservation = encodeReservation(request.recipe, request.batchSize);
319 productionRequest.batchSize = request.batchSize;
320 productionRequest.context = eve::Value(eve::Value::Object{{"recipe", eve::Value(request.recipe.id)},
321 {"process", eve::Value(request.recipe.process)}});
322 productionRequest.settlementRequired = true;
323
324 if (request.transactionId.empty()) request.transactionId = "rpg.crafting." + request.recipe.id;
325 auto batchIngredients = scaleCost(request.recipe.ingredients, request.batchSize);
326 if (!batchIngredients) return eve::Result<CraftingReceipt>::failure(batchIngredients.status());
327 transaction::TransactionContext context(std::move(request.transactionId));
328 ProductionParticipant participant(*request.production, std::move(productionRequest));
330 context, *request.ingredients, batchIngredients.value(), participant);
331 if (!committed) return eve::Result<CraftingReceipt>::failure(committed.status());
332 CraftingReceipt receipt{std::move(committed).takeValue(), participant.taskId()};
333 return eve::Result<CraftingReceipt>::success(std::move(receipt),
335}
336
338 std::string_view taskId) {
339 auto task = queue.find(taskId);
340 if (!task)
341 return failure<int>(eve::DiagnosticCode::NotFound, "crafting task was not found", "taskId");
342 const std::string settlementId = "rpg.crafting:" + std::string(taskId);
343 if (task->get().state == production::TaskState::Completed &&
344 task->get().settlement.settlementId == settlementId)
346 if (task->get().state != production::TaskState::ReadyToSettle &&
347 task->get().state != production::TaskState::SettlementFailed)
348 return failure<int>(eve::DiagnosticCode::Conflict, "crafting task is not ready to settle", "taskId");
349 auto outputs = decodeOutputs(task->get().reservation);
350 if (!outputs) return eve::Result<int>::failure(outputs.status());
351 InventoryAddParticipant inventoryParticipant(output, outputs.value());
353 receipt.settlementId = settlementId;
354 receipt.payload = eve::Value(eve::Value::Object{{"destination", eve::Value(output.getId())}});
355 SettlementParticipant settlementParticipant(queue, std::string(taskId), std::move(receipt));
356 transaction::TransactionContext context("rpg.crafting.settle:" + std::string(taskId));
357 std::array<transaction::ITransactionParticipant*, 2> participants{
358 &settlementParticipant, &inventoryParticipant};
359 transaction::Coordinator coordinator;
360 auto committed = coordinator.execute(context, std::span<transaction::ITransactionParticipant*>(participants));
361 if (!committed) {
362 const auto* diagnostic = committed.error();
363 auto failed = queue.failSettlement(taskId,
364 diagnostic == nullptr ? "crafting output rejected" : diagnostic->message());
365 const auto current = queue.find(taskId);
366 if (!failed && (!current || current->get().state != production::TaskState::SettlementFailed))
367 return eve::Result<int>::failure(failed.status());
368 return eve::Result<int>::failure(committed.status());
369 }
370 return eve::Result<int>::success(inventoryParticipant.added(),
372}
373
374} // namespace eve::rpg
double value
std::string output
Small transaction facade for a resource cost plus one domain effect.
RPG recipe adapter over the shared production and inventory protocols.
std::string message
DiagnosticCode code
const GltfImportRequest & request
bool enqueued
std::string name
std::string path
Definition PlayHost.cpp:110
std::string taskId
double current
Json object
const UnitySourceAsset & source
const VegetationPresetContext & context
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
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
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
const T * getIf() const noexcept
Return a typed pointer, or nullptr when the kind differs.
Definition Value.h:180
Value * find(const std::string &key) noexcept
Return an object member, or nullptr when absent.
Definition Value.cpp:124
格子型物品容器(脚本可直接操作)。
Definition Bag.h:21
static eve::Result< int > commitAddBatch(PreparedInventoryAdd prepared)
Commit one prepared addition and then publish its change events.
static eve::Result< PreparedInventoryAdd > prepareAddBatch(Bag *bag, const std::vector< InventoryItemGrant > &grants)
Validate a multi-item addition against a private candidate Bag.
Generic multi-owner, multi-slot continuous-progress work queue.
Definition Production.h:200
eve::OptionalRef< const ProductionTask > find(std::string_view taskId)
Finds a retained task by stable ID.
eve::Result< void > failSettlement(std::string_view taskId, std::string_view reason)
Records a retryable domain settlement failure without losing completed work.
Immutable canonical multi-resource cost.
static eve::Result< CostSpec > create(std::vector< ResourceCost > items)
Validate and canonicalize resource items.
static eve::Result< int > settle(production::WorkQueue &queue, inventory::Bag &output, std::string_view taskId)
Publish every reserved recipe output and settle the task exactly once.
Definition Crafting.cpp:337
static eve::Result< CraftingReceipt > begin(CraftingRequest request)
Atomically debit ingredients and enqueue pinned recipe work.
Definition Crafting.cpp:282
static eve::Result< TransactionReceipt > execute(const TransactionContext &context, std::span< ITransactionParticipant * > participants)
Execute already prepared domain participants as one transaction.
Coordinates participant lifecycles with deterministic ordering.
Result< TransactionReceipt > execute(const TransactionContext &context, std::span< ITransactionParticipant * > participants) const
Atomically execute borrowed participants for one context.
Immutable context shared by every participant of one transaction.
Definition Transaction.h:30
RPG 模块的基础实体:把属性 / 状态 / 技能三张表挂到一个 ECS 实体上。
DiagnosticCode
Stable machine-readable diagnostic codes.
Definition Diagnostic.h:47
Validated input used to create a domain-neutral production task.
Definition Production.h:137
eve::definition::DefinitionHandle definition
Definition Production.h:144
Owning proof that a domain consumer published one task exactly once.
Definition Production.h:46
static eve::Result< ResourceCost > create(std::string_view resourceId, std::int64_t amountValue)
Construct and validate one cost item.
Receipt for an accepted recipe job.
Definition Crafting.h:49
Borrowed inputs for atomically paying ingredients and enqueuing recipe work.
Definition Crafting.h:38