13constexpr std::string_view kType =
"tactics.battle";
17 if (!
value) std::terminate();
29Result<std::string> migrateLegacyPolicyId(
int value) {
42Result<const Value::Object*>
object(
const Value&
value, std::string
path) {
59 const auto* result =
value.getIf<std::int64_t>();
66Result<std::uint64_t> decimal(
const Value&
value, std::string
path) {
67 const auto*
text =
value.getIf<std::string>();
71 std::uint64_t result = 0;
79Result<std::int64_t> signedDecimal(
const Value&
value, std::string
path) {
80 const auto*
text =
value.getIf<std::string>();
84 std::int64_t result = 0;
92Result<SubjectRef>
subject(
const Value&
value, std::string
path,
bool allowNil =
false) {
93 const auto*
text =
value.getIf<std::string>();
99 if (!
id ||
id->isNil())
105Result<LogicalId> logicalId(
const Value&
value, std::string
path) {
106 const auto*
text =
value.getIf<std::string>();
117Result<Cell> parseCell(
const Value&
value, std::string
path) {
120 if (candidate.value()->size() != 3)
124 for (
const auto& [
name,
target] : {std::pair{
"x", &result.x}, {
"y", &result.y}, {
"layer", &result.layer}}) {
129 *
target =
static_cast<int>(parsed.value());
130 if (
static_cast<std::int64_t
>(*
target) != parsed.value())
138T* resolve(
const ecs::EntityHandle&
handle)
noexcept {
139 return dynamic_cast<T*
>(ecs::try_get(
handle));
142struct UnitCandidate {
145 LogicalId definition;
149 TacticalUnit::TurnResources
turn;
171 std::vector<ecs::EntityHandle>
sides;
179Value eventValue(
const BattleEvent& event) {
181 {
"correlationCommand",
Value(std::to_string(event.correlationCommand))},
182 {
"from",
Value(
static_cast<int>(event.from))},
183 {
"sequence",
Value(std::to_string(event.sequence))},
184 {
"subject", subjectValue(event.subject)},
185 {
"tick",
Value(std::to_string(event.tick.value()))},
186 {
"to",
Value(
static_cast<int>(event.to))},
187 {
"type",
Value(event.type)}});
190Value candidateValue(
const ReactionCandidate& candidate) {
192 {
"initiative",
Value(candidate.initiative)},
193 {
"priority",
Value(candidate.priority)},
194 {
"reactor", subjectValue(candidate.reactor)}});
197Value windowValue(
const ReactionWindow& window) {
199 for (
const auto& candidate :
window.candidates) candidates.push_back(candidateValue(candidate));
202 {
"triggerSequence",
Value(std::to_string(
window.triggerSequence))}});
205Value commandValue(
const BattleCommand& command) {
207 for (
const auto& candidate : command.candidates) candidates.push_back(candidateValue(candidate));
209 {
"action",
Value(command.action.isValid() ? command.action.format() :
std::
string{})},
210 {
"actor", subjectValue(command.actor)},
211 {
"candidates",
Value(std::move(candidates))},
212 {
"cell", cellValue(command.cell)},
213 {
"deltaNanoseconds",
Value(std::to_string(command.step.delta.nanoseconds()))},
214 {
"expectedRevision",
Value(std::to_string(command.expectedRevision.value()))},
215 {
"facing",
Value(command.facing)},
216 {
"kind",
Value(
static_cast<int>(command.kind))},
217 {
"payload",
Value(command.payload)},
218 {
"policy",
Value(command.policyId)},
219 {
"resultingRevision",
Value(std::to_string(command.resultingRevision.value()))},
220 {
"sequence",
Value(std::to_string(command.sequence))},
221 {
"targetUnit", subjectValue(command.targetUnit)},
222 {
"tick",
Value(std::to_string(command.step.tick.value()))},
223 {
"triggerSequence",
Value(std::to_string(command.triggerSequence))},
227Value objectiveValue(
const ObjectiveState& objective) {
229 for (
const Cell
cell : objective.spec.requiredCells)
cells.push_back(cellValue(
cell));
231 {
"beneficiarySide", subjectValue(objective.spec.beneficiarySide)},
232 {
"completedRevision",
Value(std::to_string(objective.completedRevision.value()))},
233 {
"endsBattle",
Value(objective.spec.endsBattle)},
234 {
"id",
Value(objective.spec.id.format())},
235 {
"kind",
Value(
static_cast<int>(objective.spec.kind))},
237 {
"requiredRound",
Value(std::to_string(objective.spec.requiredRound))},
238 {
"status",
Value(
static_cast<int>(objective.status))},
239 {
"targetSide", subjectValue(objective.spec.targetSide)},
245 for (
const auto& [
name, state] :
random.streams)
247 {
"state",
Value(std::to_string(
state.state))}}));
248 return Value(std::move(streams));
251Result<Value> unitValue(TacticalUnit&
unit) {
253 auto*
side = resolve<TacticalSide>(
unit.membership()->side);
261 {
"cell", cellValue(
unit.position()->cell)},
263 {
"definition",
Value(
unit.identity()->definition.isValid() ?
unit.identity()->definition.format()
265 {
"facing",
Value(
unit.position()->facing)},
268 {
"placed",
Value(
unit.position()->placed)},
273 {
"side", subjectValue(
side->identity()->subject)},
274 {
"subject", subjectValue(
unit.identity()->subject)},
283 const int result =
static_cast<int>(parsed.value());
284 if (
static_cast<std::int64_t
>(result) != parsed.value())
290Result<Battle::Events> parseEvents(
const Value&
value) {
292 if (!
root ||
root.value()->size() != 2)
295 auto nextMember =
field(*
root.value(),
"nextSequence",
"payload.events");
296 auto valuesMember =
field(*
root.value(),
"values",
"payload.events");
300 auto next = decimal(*nextMember.value(),
"payload.events.nextSequence");
306 Battle::Events result;
307 result.nextSequence =
next.value();
309 for (std::size_t i = 0; i <
values->size(); ++i) {
310 const std::string
path =
"payload.events.values[" + std::to_string(i) +
"]";
312 if (!record || record.value()->size() != 8)
315 auto sequenceMember =
field(*record.value(),
"sequence",
path);
316 auto tickMember =
field(*record.value(),
"tick",
path);
317 auto subjectMember =
field(*record.value(),
"subject",
path);
318 auto typeMember =
field(*record.value(),
"type",
path);
319 auto causationMember =
field(*record.value(),
"causationCommand",
path);
320 auto correlationMember =
field(*record.value(),
"correlationCommand",
path);
321 auto from = intField(*record.value(),
"from",
path);
322 auto to = intField(*record.value(),
"to",
path);
323 if (!
everyResultValid(sequenceMember, tickMember, subjectMember, typeMember, causationMember,
324 correlationMember,
from,
to) ||
329 auto sequence = decimal(*sequenceMember.value(),
path +
".sequence");
330 auto tick = decimal(*tickMember.value(),
path +
".tick");
331 auto subjectRef =
subject(*subjectMember.value(),
path +
".subject",
true);
332 auto causation = decimal(*causationMember.value(),
path +
".causationCommand");
333 auto correlation = decimal(*correlationMember.value(),
path +
".correlationCommand");
334 const auto*
type = typeMember.value()->getIf<std::string>();
336 causation.value() == 0 || correlation.value() == 0 ||
sequence.value() <=
previous ||
337 sequence.value() >= result.nextSequence)
341 result.values.push_back({
sequence.value(), causation.value(), correlation.value(),
350 if (!
root ||
root.value()->size() != 3)
353 auto maxDepthMember =
field(*
root.value(),
"maxDepth",
"payload.reactions");
354 auto seenMember =
field(*
root.value(),
"seen",
"payload.reactions");
355 auto stackMember =
field(*
root.value(),
"stack",
"payload.reactions");
359 auto maxDepth = decimal(*maxDepthMember.value(),
"payload.reactions.maxDepth");
360 const auto* seen = seenMember.value()->getIf<
Value::Array>();
361 const auto* stack = stackMember.value()->getIf<
Value::Array>();
362 if (!maxDepth || maxDepth.value() == 0 || maxDepth.value() >
static_cast<std::uint64_t
>(SIZE_MAX) || !seen ||
363 !stack || stack->size() > maxDepth.value())
367 Battle::Reactions result;
368 result.maxDepth =
static_cast<std::size_t
>(maxDepth.value());
369 std::set<std::string> uniqueSeen;
370 for (std::size_t i = 0; i < seen->size(); ++i) {
371 const auto*
key = (*seen)[i].getIf<std::string>();
372 if (!
key ||
key->empty() || !uniqueSeen.insert(*key).second)
375 "payload.reactions.seen[" + std::to_string(i) +
"]"));
376 result.seen.push_back(*
key);
378 for (std::size_t i = 0; i < stack->size(); ++i) {
379 const std::string
path =
"payload.reactions.stack[" + std::to_string(i) +
"]";
390 auto trigger = decimal(*triggerMember.value(),
path +
".triggerSequence");
391 auto depth = decimal(*depthMember.value(),
path +
".depth");
392 const auto* candidates = candidatesMember.value()->getIf<
Value::Array>();
397 ReactionWindow restored{trigger.
value(),
static_cast<std::size_t
>(
depth.value()), {}};
398 std::set<std::string> uniqueCandidates;
399 for (std::size_t j = 0; j < candidates->size(); ++j) {
400 const std::string candidatePath =
path +
".candidates[" + std::to_string(j) +
"]";
401 auto candidate =
object((*candidates)[j], candidatePath);
402 if (!candidate || candidate.value()->size() != 4)
405 auto reactorMember =
field(*candidate.value(),
"reactor", candidatePath);
406 auto actionMember =
field(*candidate.value(),
"action", candidatePath);
407 auto priority = intField(*candidate.value(),
"priority", candidatePath);
408 auto initiative = intField(*candidate.value(),
"initiative", candidatePath);
412 auto reactor =
subject(*reactorMember.value(), candidatePath +
".reactor");
413 auto action = logicalId(*actionMember.value(), candidatePath +
".action");
417 const std::string
key = reactor.value().format() +
":" +
action.value().format();
418 if (!uniqueCandidates.insert(
key).second)
421 restored.candidates.push_back(
422 {reactor.value(),
action.value(),
priority.value(), initiative.value()});
424 result.stack.push_back(std::move(restored));
442enum class CommandLogBoundary {
454struct CommandFieldPolicy {
458 CommandLogBoundary
boundary = CommandLogBoundary::EndsAtRevision;
461Result<Battle::Commands> parseCommands(
const Value&
value,
Revision snapshotRevision, CommandFieldPolicy policy) {
463 if (!
root ||
root.value()->size() != 2)
466 auto nextMember =
field(*
root.value(),
"nextSequence",
"payload.commands");
467 auto valuesMember =
field(*
root.value(),
"values",
"payload.commands");
471 auto next = decimal(*nextMember.value(),
"payload.commands.nextSequence");
477 Battle::Commands result;
478 result.nextSequence =
next.value();
479 std::uint64_t previousSequence = 0;
481 for (std::size_t i = 0; i <
values->size(); ++i) {
482 const std::string
path =
"payload.commands.values[" + std::to_string(i) +
"]";
485 const std::size_t expectedCommandFields = policy.abilityTargetFields ? 15u : 13u;
486 if (!record || record.value()->size() != expectedCommandFields)
489 auto sequenceMember =
field(*record.value(),
"sequence",
path);
490 auto expectedMember =
field(*record.value(),
"expectedRevision",
path);
491 auto resultingMember =
field(*record.value(),
"resultingRevision",
path);
492 auto tickMember =
field(*record.value(),
"tick",
path);
493 auto deltaMember =
field(*record.value(),
"deltaNanoseconds",
path);
494 auto triggerMember =
field(*record.value(),
"triggerSequence",
path);
495 auto actorMember =
field(*record.value(),
"actor",
path);
496 auto actionMember =
field(*record.value(),
"action",
path);
497 auto cellMember =
field(*record.value(),
"cell",
path);
498 auto candidatesMember =
field(*record.value(),
"candidates",
path);
499 auto kind = intField(*record.value(),
"kind",
path);
500 auto facing = intField(*record.value(),
"facing",
path);
501 auto policyMember =
field(*record.value(),
"policy",
path);
502 auto targetMember = policy.abilityTargetFields ?
field(*record.value(),
"targetUnit",
path)
504 auto payloadMember = policy.abilityTargetFields ?
field(*record.value(),
"payload",
path)
511 if (!
everyResultValid(sequenceMember, expectedMember, resultingMember, tickMember, deltaMember,
512 triggerMember, actorMember, actionMember, cellMember, candidatesMember,
kind,
513 facing, policyMember, targetMember, payloadMember) ||
514 kind.value() < 0 ||
kind.value() > highestKnownKind)
519 if (policy.policyIdStrings) {
520 if (!policyMember.value()->isString() || policyMember.value()->asString().empty())
525 auto legacy = intField(*record.value(),
"policy",
path);
529 auto migrated = migrateLegacyPolicyId(legacy.value());
531 policyId = std::move(migrated).takeValue();
533 auto sequence = decimal(*sequenceMember.value(),
path +
".sequence");
534 auto expected = decimal(*expectedMember.value(),
path +
".expectedRevision");
535 auto resulting = decimal(*resultingMember.value(),
path +
".resultingRevision");
536 auto tick = decimal(*tickMember.value(),
path +
".tick");
537 auto delta = signedDecimal(*deltaMember.value(),
path +
".deltaNanoseconds");
538 auto trigger = decimal(*triggerMember.value(),
path +
".triggerSequence");
539 auto actor =
subject(*actorMember.value(),
path +
".actor",
true);
540 auto cell = parseCell(*cellMember.value(),
path +
".cell");
541 const auto* actionText = actionMember.value()->getIf<std::string>();
542 const auto* candidates = candidatesMember.value()->getIf<
Value::Array>();
544 !candidates ||
sequence.value() <= previousSequence ||
545 sequence.value() >= result.nextSequence || expected.value() == std::numeric_limits<std::uint64_t>::max() ||
546 resulting.value() != expected.value() + 1 ||
547 (i > 0 && expected.value() != previousRevision.value()))
550 BattleCommand command;
551 command.sequence =
sequence.value();
553 command.expectedRevision =
Revision(expected.value());
554 command.resultingRevision =
Revision(resulting.value());
556 command.actor = actor.value();
557 command.cell =
cell.value();
558 command.facing =
facing.value();
560 command.triggerSequence = trigger.value();
561 if (policy.abilityTargetFields) {
562 auto targetUnit =
subject(*targetMember.value(),
path +
".targetUnit",
true);
564 command.targetUnit = targetUnit.value();
565 const auto*
payload = payloadMember.value()->getIf<std::string>();
576 if (!actionText->empty()) {
581 command.action = *parsedAction;
583 for (std::size_t j = 0; j < candidates->size(); ++j) {
584 const std::string candidatePath =
path +
".candidates[" + std::to_string(j) +
"]";
585 auto candidate =
object((*candidates)[j], candidatePath);
586 if (!candidate || candidate.value()->size() != 4)
589 auto reactorMember =
field(*candidate.value(),
"reactor", candidatePath);
590 auto candidateActionMember =
field(*candidate.value(),
"action", candidatePath);
591 auto priority = intField(*candidate.value(),
"priority", candidatePath);
592 auto initiative = intField(*candidate.value(),
"initiative", candidatePath);
596 auto reactor =
subject(*reactorMember.value(), candidatePath +
".reactor");
597 auto candidateAction = logicalId(*candidateActionMember.value(), candidatePath +
".action");
601 command.candidates.push_back(
602 {reactor.value(), candidateAction.value(),
priority.value(), initiative.value()});
604 const auto invalidCommand = [&]() {
605 switch (command.kind) {
615 return command.triggerSequence == 0 || command.candidates.empty();
617 return command.triggerSequence == 0 || !command.actor.isValid() || !command.action.isValid();
621 return command.triggerSequence != 0 || !command.actor.isValid() || !command.action.isValid();
625 if (invalidCommand())
629 previousRevision = command.resultingRevision;
630 result.values.push_back(std::move(command));
632 if (policy.boundary == CommandLogBoundary::EndsAtRevision && !result.values.empty() &&
633 result.values.back().resultingRevision != snapshotRevision)
639 if (policy.boundary == CommandLogBoundary::StartsAtRevision && !result.values.empty() &&
640 result.values.front().expectedRevision != snapshotRevision)
646Result<Battle::Objectives> parseObjectives(Battle& battle,
const BoardState&
board,
const Value&
value,
652 std::set<std::string> sideSubjects;
658 sideSubjects.insert(
side->identity()->subject.format());
660 Battle::Objectives result;
661 std::set<std::string>
ids;
662 for (std::size_t i = 0; i <
values->size(); ++i) {
663 const std::string
path =
"payload.objectives[" + std::to_string(i) +
"]";
665 if (!record || record.value()->size() != 9)
668 auto idMember =
field(*record.value(),
"id",
path);
669 auto beneficiaryMember =
field(*record.value(),
"beneficiarySide",
path);
670 auto targetMember =
field(*record.value(),
"targetSide",
path);
671 auto roundMember =
field(*record.value(),
"requiredRound",
path);
672 auto cellsMember =
field(*record.value(),
"requiredCells",
path);
673 auto endsMember =
field(*record.value(),
"endsBattle",
path);
674 auto completedMember =
field(*record.value(),
"completedRevision",
path);
675 auto kind = intField(*record.value(),
"kind",
path);
676 auto status = intField(*record.value(),
"status",
path);
677 if (!
everyResultValid(idMember, beneficiaryMember, targetMember, roundMember, cellsMember, endsMember,
683 auto id = logicalId(*idMember.value(),
path +
".id");
684 auto beneficiary =
subject(*beneficiaryMember.value(),
path +
".beneficiarySide");
686 auto round = decimal(*roundMember.value(),
path +
".requiredRound");
687 auto completed = decimal(*completedMember.value(),
path +
".completedRevision");
689 const auto* endsBattle = endsMember.value()->getIf<
bool>();
691 !
ids.insert(
id.
value().format()).
second || !sideSubjects.contains(beneficiary.value().format()) ||
692 (
target.value().isValid() && !sideSubjects.contains(
target.value().format())) ||
693 completed.value() > snapshotRevision.value())
696 ObjectiveState objective;
697 objective.spec.id =
id.
value();
699 objective.spec.beneficiarySide = beneficiary.value();
700 objective.spec.targetSide =
target.value();
701 objective.spec.requiredRound =
round.value();
702 objective.spec.endsBattle = *endsBattle;
704 objective.completedRevision =
Revision(completed.value());
705 for (std::size_t j = 0; j <
cells->size(); ++j) {
706 auto cell = parseCell((*
cells)[j],
path +
".requiredCells[" + std::to_string(j) +
"]");
710 objective.spec.requiredCells.push_back(
cell.value());
719 std::sort(objective.spec.requiredCells.begin(), objective.spec.requiredCells.end());
720 if (std::adjacent_find(objective.spec.requiredCells.begin(), objective.spec.requiredCells.end()) !=
721 objective.spec.requiredCells.end())
724 result.values.push_back(std::move(objective));
729Result<Battle::Random> parseRandom(
const Value&
value) {
734 Battle::Random result;
735 for (
const auto& [
name, stateValue] : *streams) {
740 if (!state ||
state.value()->size() != 2)
743 auto stateMember =
field(*
state.value(),
"state",
"payload.random." +
name);
744 auto indexMember =
field(*
state.value(),
"rollIndex",
"payload.random." +
name);
748 auto parsedState = decimal(*stateMember.value(),
"payload.random." +
name +
".state");
749 auto parsedIndex = decimal(*indexMember.value(),
"payload.random." +
name +
".rollIndex");
750 if (!
everyResultValid(parsedState, parsedIndex) || parsedIndex.value() == 0)
753 result.streams.emplace(
name, Battle::RandomStreamState{parsedState.value(), parsedIndex.value()});
758Result<Candidate> parseCandidate(Battle& battle,
const Value&
payload,
Revision snapshotRevision,
762 static const std::set<std::string>
fields = {
"activeUnit",
"board",
"commands",
"cursor",
"events",
763 "objectives",
"phase",
"policy",
"random",
"reactions",
"round",
764 "seed",
"sides",
"status",
"units"};
768 std::set<std::string> expectedFields =
fields;
769 if (hasSchedule) expectedFields.insert(
"schedule");
771 if (!expectedFields.contains(
name))
772 return
Result<Candidate>::failure(
774 if (
root.value()->size() != expectedFields.size())
779 auto status = intField(*
root.value(),
"status",
"payload");
780 auto phase = intField(*
root.value(),
"phase",
"payload");
790 auto policyMember =
field(*
root.value(),
"policy",
"payload");
794 if (!policyMember.value()->isString() || policyMember.value()->asString().empty())
797 result.policyId = policyMember.value()->asString();
799 auto legacy = intField(*
root.value(),
"policy",
"payload");
802 auto migrated = migrateLegacyPolicyId(legacy.value());
804 result.policyId = std::move(migrated).takeValue();
806 auto roundValue =
field(*
root.value(),
"round",
"payload");
807 auto cursorValue =
field(*
root.value(),
"cursor",
"payload");
810 auto round = decimal(*roundValue.value(),
"payload.round");
811 auto cursor = decimal(*cursorValue.value(),
"payload.cursor");
814 result.round =
round.value();
815 result.cursor =
static_cast<std::size_t
>(
cursor.value());
816 auto seedMember =
field(*
root.value(),
"seed",
"payload");
818 auto seed = decimal(*seedMember.value(),
"payload.seed");
820 result.seed =
seed.value();
822 auto boardMember =
field(*
root.value(),
"board",
"payload");
824 auto boardObject =
object(*boardMember.value(),
"payload.board");
828 const std::size_t expectedBoardFields = hasEdges ? 3u : 2u;
829 if (!boardObject || boardObject.value()->size() != expectedBoardFields)
832 auto topology = intField(*boardObject.value(),
"topology",
"payload.board");
833 auto cellsMember =
field(*boardObject.value(),
"cells",
"payload.board");
836 return
Result<Candidate>::failure(
843 for (std::size_t i = 0; i <
cells->size(); ++i) {
844 const std::string
path =
"payload.board.cells[" + std::to_string(i) +
"]";
846 if (!record || record.value()->size() != 5)
849 auto cellMember =
field(*record.value(),
"cell",
path);
850 auto moveCost = intField(*record.value(),
"moveCost",
path);
851 auto height = intField(*record.value(),
"height",
path);
852 auto passableMember =
field(*record.value(),
"passable",
path);
853 auto tagsMember =
field(*record.value(),
"tags",
path);
857 auto cell = parseCell(*cellMember.value(),
path +
".cell");
858 const auto* passable = passableMember.value()->getIf<
bool>();
859 const auto* tags = tagsMember.value()->getIf<
Value::Array>();
860 if (!
cell || !passable || !tags)
864 for (
const auto&
tag : *tags) {
865 const auto*
text =
tag.getIf<std::string>();
871 auto added = result.board.addCell(
cell.value(), std::move(state));
878 auto edgesMember =
field(*boardObject.value(),
"edges",
"payload.board");
879 const auto*
edges = edgesMember ? edgesMember.value()->getIf<
Value::Array>() : nullptr;
880 if (
edges ==
nullptr)
883 for (std::size_t i = 0; i <
edges->size(); ++i) {
884 const std::string
path =
"payload.board.edges[" + std::to_string(i) +
"]";
886 if (!record || record.value()->size() != 5)
889 auto fromMember =
field(*record.value(),
"from",
path);
890 auto toMember =
field(*record.value(),
"to",
path);
891 auto passableMember =
field(*record.value(),
"passable",
path);
892 auto extraCost = intField(*record.value(),
"extraCost",
path);
893 auto tagsMember =
field(*record.value(),
"tags",
path);
894 if (!
everyResultValid(fromMember, toMember, passableMember, extraCost, tagsMember))
897 auto from = parseCell(*fromMember.value(),
path +
".from");
898 auto to = parseCell(*toMember.value(),
path +
".to");
899 const auto* passable = passableMember.value()->getIf<
bool>();
900 const auto* tags = tagsMember.value()->getIf<
Value::Array>();
905 state.passable = *passable;
906 state.extraCost = extraCost.value();
907 for (
const auto&
tag : *tags) {
908 const auto*
text =
tag.getIf<std::string>();
914 auto added = result.board.addEdge(
from.value(),
to.value(), std::move(state));
919 std::map<std::string, TacticalSide*> currentSides;
925 currentSides.emplace(
side->identity()->subject.format(),
side);
927 auto sidesMember =
field(*
root.value(),
"sides",
"payload");
930 if (!
sides ||
sides->size() != currentSides.size())
933 std::set<std::string> restoredSides;
934 for (std::size_t i = 0; i <
sides->size(); ++i) {
935 auto side =
subject((*
sides)[i],
"payload.sides[" + std::to_string(i) +
"]");
937 const auto found = currentSides.find(
side.value().format());
938 if (
found == currentSides.end() || !restoredSides.insert(
found->first).second)
940 "snapshot side cannot resolve uniquely",
941 "payload.sides[" + std::to_string(i) +
"]"));
942 result.sides.push_back(
found->second->identity()->self);
945 std::map<std::string, TacticalUnit*> currentUnits;
951 currentUnits.emplace(
unit->identity()->subject.format(),
unit);
953 auto unitsMember =
field(*
root.value(),
"units",
"payload");
956 if (!
units ||
units->size() != currentUnits.size())
959 std::set<std::string> restoredSubjects;
960 for (std::size_t i = 0; i <
units->size(); ++i) {
961 const std::string
path =
"payload.units[" + std::to_string(i) +
"]";
964 const std::size_t expectedUnitFields = hasSchedule ? 16u : 15u;
965 if (!unitObject || unitObject.value()->size() != expectedUnitFields)
968 auto subjectMember =
field(*unitObject.value(),
"subject",
path);
969 auto cellMember =
field(*unitObject.value(),
"cell",
path);
970 auto definitionMember =
field(*unitObject.value(),
"definition",
path);
971 auto sideMember =
field(*unitObject.value(),
"side",
path);
972 if (!
everyResultValid(subjectMember, cellMember, definitionMember, sideMember))
975 auto subjectRef =
subject(*subjectMember.value(),
path +
".subject");
976 auto cell = parseCell(*cellMember.value(),
path +
".cell");
977 auto facing = intField(*unitObject.value(),
"facing",
path);
978 const auto* definitionText = definitionMember.value()->getIf<std::string>();
979 auto sideRef =
subject(*sideMember.value(),
path +
".side");
983 const auto found = currentUnits.find(subjectRef.value().format());
984 if (
found == currentUnits.end() || !restoredSubjects.insert(
found->first).second)
987 const auto restoredSide = currentSides.find(sideRef.value().format());
988 if (restoredSide == currentSides.end())
991 LogicalId definition;
992 if (!definitionText->empty()) {
994 if (!parsedDefinition)
997 definition = *parsedDefinition;
999 TacticalUnit::TurnResources
turn;
1005 auto parsed = intField(*unitObject.value(),
name,
path);
1007 *
target = parsed.value();
1010 auto charge = intField(*unitObject.value(),
"charge",
path);
1014 if (charge.value() < 0)
1019 auto aliveMember =
field(*unitObject.value(),
"alive",
path);
1020 auto actedMember =
field(*unitObject.value(),
"acted",
path);
1021 auto placedMember =
field(*unitObject.value(),
"placed",
path);
1022 const bool*
alive = aliveMember ? aliveMember.value()->getIf<
bool>() : nullptr;
1023 const bool* acted = actedMember ? actedMember.value()->getIf<
bool>() : nullptr;
1024 const bool*
placed = placedMember ? placedMember.value()->getIf<
bool>() : nullptr;
1034 auto placement = result.board.place(subjectRef.value(),
cell.value());
1040 if (
facing.value() < 0 ||
facing.value() >= facingCount ||
1044 result.units.push_back(
1045 {
found->second, restoredSide->second->identity()->self, definition,
cell.value(),
facing.value(),
1048 auto activeMember =
field(*
root.value(),
"activeUnit",
"payload");
1050 auto active =
subject(*activeMember.value(),
"payload.activeUnit",
true);
1052 if (
active.value().isValid()) {
1053 const auto found = currentUnits.find(
active.value().format());
1054 if (
found == currentUnits.end())
1056 result.activeUnit =
found->second->identity()->self;
1057 result.activeSide =
found->second->membership()->side;
1059 if (result.cursor >= result.units.size() && !result.units.empty())
1066 auto scheduleMember =
field(*
root.value(),
"schedule",
"payload");
1073 for (std::size_t i = 0; i <
values->size(); ++i) {
1074 const std::string
path =
"payload.schedule[" + std::to_string(i) +
"]";
1077 const auto found = currentUnits.find(entry.value().format());
1078 if (
found == currentUnits.end())
1084 result.schedule.push_back(
found->second->identity()->self);
1088 if (activationPhase && result.cursor >= result.schedule.size())
1094 auto eventsMember =
field(*
root.value(),
"events",
"payload");
1095 auto reactionsMember =
field(*
root.value(),
"reactions",
"payload");
1096 auto commandsMember =
field(*
root.value(),
"commands",
"payload");
1097 auto objectivesMember =
field(*
root.value(),
"objectives",
"payload");
1098 auto randomMember =
field(*
root.value(),
"random",
"payload");
1099 if (!
everyResultValid(eventsMember, reactionsMember, commandsMember, objectivesMember, randomMember))
1102 auto events = parseEvents(*eventsMember.value());
1103 auto reactions = parseReactions(*reactionsMember.value(), result.phase);
1104 CommandFieldPolicy commandFields;
1105 commandFields.policyIdStrings = sourceVersion >=
SchemaVersion(3);
1106 commandFields.allowUseAbility = sourceVersion >=
SchemaVersion(4);
1107 commandFields.abilityTargetFields = sourceVersion >=
SchemaVersion(6);
1108 auto commands = parseCommands(*commandsMember.value(), snapshotRevision, commandFields);
1109 auto objectives = parseObjectives(battle, result.board, *objectivesMember.value(), snapshotRevision);
1110 auto random = parseRandom(*randomMember.value());
1115 const bool eventsOk =
events.ok();
1116 const bool reactionsOk =
reactions.ok();
1117 const bool commandsOk =
commands.ok();
1119 const bool randomOk =
random.ok();
1125 result.events = std::move(
events).takeValue();
1126 result.reactions = std::move(
reactions).takeValue();
1127 result.commands = std::move(
commands).takeValue();
1128 result.objectives = std::move(
objectives).takeValue();
1129 result.random = std::move(
random).takeValue();
1130 std::set<std::uint64_t> commandSequences;
1131 std::map<std::string, std::uint64_t> randomCommandCounts;
1133 commandSequences.insert(command.sequence);
1136 for (
const auto& [stream, state] : result.
random.streams)
1137 if (randomCommandCounts[stream] !=
state.rollIndex)
1140 "payload.random." + stream));
1142 if (!commandSequences.contains(event.causationCommand) ||
1143 !commandSequences.contains(event.correlationCommand))
1146 "event causation/correlation references an absent command",
"payload.events"));
1148 std::set<std::uint64_t> eventSequences;
1150 for (
const auto& window : result.
reactions.stack) {
1151 if (!eventSequences.contains(
window.triggerSequence))
1153 "reaction window references an absent trigger event",
1154 "payload.reactions.stack"));
1155 for (
const auto& reactionCandidate :
window.candidates)
1156 if (!currentUnits.contains(reactionCandidate.reactor.format()))
1157 return
Result<Candidate>::failure(
1159 "payload.reactions.stack"));
1161 auto valid = result.board.validateInvariants();
1170 for (
const auto& record : battle.board()->value.records()) {
1172 for (
const auto&
tag : record.state.tags) tags.emplace_back(
tag);
1174 {
"height",
Value(record.state.height)},
1175 {
"moveCost",
Value(record.state.moveCost)},
1176 {
"passable",
Value(record.state.passable)},
1177 {
"tags",
Value(std::move(tags))}});
1180 for (
const auto& record : battle.board()->value.edgeRecords()) {
1182 for (
const auto&
tag : record.state.tags) tags.emplace_back(
tag);
1184 {
"from", cellValue(record.from)},
1185 {
"passable",
Value(record.state.passable)},
1186 {
"tags",
Value(std::move(tags))},
1187 {
"to", cellValue(record.to)}});
1190 for (
const auto&
handle : battle.turn()->units) {
1195 auto encoded = unitValue(*
unit);
1197 units.push_back(std::move(encoded).takeValue());
1200 for (
const auto&
handle : battle.turn()->sides) {
1205 sides.push_back(subjectValue(
side->identity()->subject));
1208 if (battle.turn()->activeUnit) {
1209 auto*
unit = resolve<TacticalUnit>(*battle.turn()->activeUnit);
1216 for (
const auto& event : battle.events()->values)
events.push_back(eventValue(event));
1218 for (
const auto& window : battle.reactions()->stack) reactionStack.push_back(windowValue(window));
1220 for (
const auto&
key : battle.reactions()->seen) reactionSeen.emplace_back(
key);
1222 for (
const auto& command : battle.commands()->values)
commands.push_back(commandValue(command));
1224 for (
const auto& objective : battle.objectives()->values)
objectives.push_back(objectiveValue(objective));
1228 for (
const auto&
handle : battle.turn()->schedule) {
1233 schedule.push_back(subjectValue(
unit->identity()->subject));
1236 {
"activeUnit", subjectValue(
active)},
1239 {
"topology",
Value(
static_cast<int>(battle.board()->value.topology()))}})},
1240 {
"cursor",
Value(std::to_string(battle.turn()->cursor))},
1246 {
"phase",
Value(
static_cast<int>(battle.turn()->phase))},
1247 {
"policy",
Value(battle.turn()->policyId)},
1248 {
"random", randomValue(*battle.random())},
1250 {
"maxDepth",
Value(std::to_string(battle.reactions()->maxDepth))},
1251 {
"seen",
Value(std::move(reactionSeen))},
1252 {
"stack",
Value(std::move(reactionStack))}})},
1253 {
"round",
Value(std::to_string(battle.turn()->round))},
1255 {
"seed",
Value(std::to_string(battle.identity()->seed))},
1257 {
"status",
Value(
static_cast<int>(battle.turn()->status))},
1262 battle.identity()->subject.persistentId(), battle.turn()->revision, battle.turn()->tick,
1263 std::move(
payload), hashProvider);
1268 for (
const BattleCommand& command : battle.commands()->values) {
1269 if (command.resultingRevision > fromRevision)
values.push_back(commandValue(command));
1275 {
"nextSequence",
Value(std::to_string(battle.commands()->nextSequence))},
1281 CommandFieldPolicy policy;
1282 policy.policyIdStrings =
true;
1283 policy.allowUseAbility =
true;
1284 policy.abilityTargetFields =
true;
1286 policy.boundary = CommandLogBoundary::StartsAtRevision;
1287 return parseCommands(
value, snapshotRevision, policy);
1295 if (
source.instanceId != battle.identity()->subject.persistentId())
1313 auto candidate = parseCandidate(battle,
source.payload,
source.revision,
source.schemaVersion);
1316 Candidate restored = std::move(candidate).takeValue();
1317 battle.board()->value = std::move(restored.board);
1318 battle.identity()->seed = restored.seed;
1319 for (
auto&
unit : restored.units) {
1320 unit.unit->identity()->definition =
unit.definition;
1321 unit.unit->membership()->side =
unit.side;
1323 unit.unit->position()->cell =
unit.cell;
1324 unit.unit->position()->facing =
unit.facing;
1325 unit.unit->position()->placed =
unit.placed;
1327 auto turn = battle.turn();
1328 turn->status = restored.status;
1329 turn->phase = restored.phase;
1330 turn->policyId = std::move(restored.policyId);
1333 turn->round = restored.round;
1334 turn->cursor = restored.cursor;
1335 turn->activeUnit = restored.activeUnit;
1336 turn->activeSide = restored.activeSide;
1337 turn->sides = std::move(restored.sides);
1338 turn->schedule = std::move(restored.schedule);
1339 *battle.events() = std::move(restored.events);
1340 *battle.reactions() = std::move(restored.reactions);
1341 *battle.commands() = std::move(restored.commands);
1342 *battle.objectives() = std::move(restored.objectives);
1343 *battle.random() = std::move(restored.random);
std::map< std::string, Var > values
wgpu::PopErrorScopeStatus status
HexCoordinates to
Cell the unit walks towards on this segment.
graphics::Canvas * previous
std::unique_ptr< gpgpu::Sequence > sequence
std::array< PixelCell, kPixelChunkSize *kPixelChunkSize > cells
std::vector< std::string > fields
std::vector< ecs::EntityHandle > schedule
The round's activation queue, resolved against the target battle.
std::optional< ecs::EntityHandle > activeSide
Battle::Reactions reactions
std::vector< UnitCandidate > units
CommandLogBoundary boundary
Anchor rule, see above.
TacticalUnit::TurnResources turn
bool allowUseAbility
v4+: the UseAbility command kind exists.
Battle::Objectives objectives
bool abilityTargetFields
v6+: commands carry targetUnit and payload.
bool policyIdStrings
v3+: stable policy id string instead of the legacy numeric enum.
std::optional< ecs::EntityHandle > activeUnit
Versioned battle snapshots with transactional restore.
float(ui::Theme::* member)[4]
const UnitySourceAsset & source
std::vector< VegetationPresetCommand > commands
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.
static constexpr Duration fromNanoseconds(std::int64_t nanoseconds) noexcept
Construct an exact duration from nanoseconds.
static std::optional< LogicalId > parse(std::string_view text)
Parses a scoped logical name.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
const T & value() const &
Borrow the value from a const lvalue after checking success.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Strong, domain-neutral reference to a runtime subject.
static SubjectRef fromPersistentId(PersistentId id) noexcept
Wrap a persistent identity without changing its bytes.
static SubjectRef nil() noexcept
Return the invalid nil reference.
The canonical owning dynamic value used by data-facing protocols.
std::map< std::string, Value > Object
std::vector< Value > Array
static std::optional< Id128 > parse(std::string_view text) noexcept
Parses canonical UUID text.
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
Battle short-root and authoritative owner of board and turn state.
bool contains(Cell cell) const noexcept
Return whether the logical cell exists.
static Result< Battle::Commands > parseCommandLog(const Value &value, Revision snapshotRevision)
Parse a command log written by commandLogValue.
static Result< void > restore(Battle &battle, const SnapshotEnvelope &snapshot, const SnapshotHashProvider &hashProvider)
Restore a verified compatible snapshot without partial mutation on failure.
static Result< SnapshotEnvelope > snapshot(Battle &battle, const SnapshotHashProvider &hashProvider)
Capture a version-one, integrity-sealed battle snapshot.
static Result< Value > commandLogValue(Battle &battle, Revision fromRevision)
Serialize the accepted command log after fromRevision in its persisted shape.
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
eve::Diagnostic Diagnostic
@ Cell
A cell was removed; the out-parameter holds it.
std::int32_t moveCost(const HexMap &map, HexCoordinates from, HexCoordinates to, HexDirection direction, const HexOccupancyQuery &occupied)
Cost of moving between two adjacent cells.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
const EditorValue * field(const EditorValue &value, const char *name)
constexpr std::string_view kInitiativePolicyId
Stable id of the built-in per-unit initiative turn policy.
ObjectiveStatus
Objective lifecycle owned by its battle.
constexpr std::string_view kSideAlternatingPolicyId
Stable id of the built-in side-alternating turn policy.
BattleStatus
Tactical battle lifecycle.
BattleCommandKind
Accepted deterministic command kinds recorded for replay.
@ UseAbility
Declare a unit ability activation against a target cell.
ObjectiveKind
Built-in deterministic objective rules.
BattlePhase
Explicit, externally observable battle phase.
constexpr std::size_t kMaxAbilityPayloadBytes
Upper bound on the opaque ability payload a declaration may carry.
BoardTopology
Supported deterministic board-neighbour topologies.
int64_t integer(const RuntimeTensor &v, size_t i=0)
Integer.
WidgetDesc window(std::string title, std::vector< WidgetDesc > children, std::string id)
Top-level window widget with a title bar.
DiagnosticCode
Stable machine-readable diagnostic codes.
Result< void > validateSnapshotPayloadMetadata(const Value &payload, Revision revision, SimulationTick tick)
Validate optional payload copies of envelope revision and tick.
detail::StrongUint64< detail::SchemaVersionTag > SchemaVersion
Persistent data-format version; not a runtime replacement generation.
bool everyResultValid(const Results &... results) noexcept
True only when every argument Result succeeded, observing all of them.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Result< SnapshotEnvelope > makeSnapshotEnvelope(std::string type, LogicalId schema, SchemaVersion schemaVersion, PersistentId instanceId, Revision revision, SimulationTick tick, Value payload, const SnapshotHashProvider &hashProvider)
Construct and seal a snapshot envelope.
detail::StrongUint64< detail::RevisionTag > Revision
Monotonic content/state revision used for optimistic-concurrency checks.
Result< void > verifySnapshotEnvelope(const SnapshotEnvelope &snapshot, const SnapshotHashProvider &hashProvider)
Verify an envelope's content hash without modifying it.
std::function< Result< ContentId >(std::string_view canonicalInput)> SnapshotHashProvider
Injected content-digest implementation used by snapshots.
Stable outer format shared by persistence and cross-process snapshots.
One accepted replay command using only persistent identities and values.
int charge
Accumulated scheduling charge for charge-time policies.