载入中...
搜索中...
未找到
RTS.cpp
浏览该文件的文档.
1#include "rts/RTS.h"
3#include "crowd/Crowd.h"
5#include "map/Fov.h"
6#include "map/Pathfinder.h"
7#include "rts/RTSAttributes.h"
9#include "sensing/Sensing.h"
11
12#include "action/Action.h"
13#include "common/Capability.h"
14
15#include <simplesquirrel/simplesquirrel.hpp>
16
17#include <algorithm>
18#include <cmath>
19#include <limits>
20#include <numbers>
21#include <set>
22#include <utility>
23
24namespace eve::rts {
25namespace {
29
30bool validSubject(SubjectRef subject) { return subject.isValid(); }
31
32bool sameHandle(const ecs::EntityHandle& left, const ecs::EntityHandle& right) noexcept {
33 return left.table == right.table && left.type == right.type && left.id == right.id &&
34 left.generation == right.generation;
35}
36
37Result<RTSEffectDefinition> resolveEffectDefinition(definitions::DefinitionRegistry& registry, std::string_view id,
38 SubjectRef source, double durationOverride = -1.0) {
39 if (id.empty() || !source.isValid() || !std::isfinite(durationOverride))
42 "RTS status effect requires an id, valid source, and finite duration override", "effect"));
43 auto resolved = registry.resolve("effect", std::string(id));
44 if (!resolved) return Result<RTSEffectDefinition>::failure(resolved.status());
45 auto parsed = Value::fromJson(resolved.value().get().json);
46 if (!parsed) return Result<RTSEffectDefinition>::failure(parsed.status());
47 const auto* object = parsed.value().getIf<Value::Object>();
48 if (object == nullptr)
50 DiagnosticCode::InvalidArgument, "RTS status effect definition must be an object", "effect"));
51 const auto number = [object](std::string_view name, double fallback) -> std::optional<double> {
52 const auto found = object->find(std::string(name));
53 if (found == object->end()) return fallback;
54 if (const auto* integer = found->second.getIf<std::int64_t>()) return static_cast<double>(*integer);
55 if (const auto* real = found->second.getIf<double>()) return *real;
56 return std::nullopt;
57 };
58 const auto duration = number("duration", 0.0);
59 const auto speed = number("speedMultiplier", 1.0);
60 const auto damage = number("damageMultiplier", 1.0);
61 const auto incoming = number("incomingDamageMultiplier", 1.0);
62 const auto healing = number("healingPerSecond", 0.0);
63 if (!duration || !speed || !damage || !incoming || !healing || !std::isfinite(*duration) ||
64 !std::isfinite(*speed) || !std::isfinite(*damage) || !std::isfinite(*incoming) || !std::isfinite(*healing) ||
65 *duration < 0.0 || *speed < 0.0 || *damage < 0.0 || *incoming < 0.0 || *healing < 0.0)
68 "RTS status effect duration and modifiers must be finite non-negative numbers", "effect"));
69 RTSEffectDefinition definition;
70 definition.id = std::string(id);
71 definition.source = source.format();
72 definition.duration = durationOverride >= 0.0 ? durationOverride : *duration;
73 definition.speedMultiplier = *speed;
74 definition.damageMultiplier = *damage;
75 definition.incomingDamageMultiplier = *incoming;
76 definition.healingPerSecond = *healing;
77 definition.tags = {"status:" + std::string(id)};
79}
80
81Result<resource::CostSpec> scaledCost(const resource::CostSpec& source, double factor) {
82 if (!std::isfinite(factor) || factor <= 0.0)
84 DiagnosticCode::InvalidArgument, "RTS refund factor must be finite and positive", "refund.factor"));
85 std::vector<resource::ResourceCost> items;
86 items.reserve(source.items().size());
87 for (const auto& item : source.items()) {
88 const auto amount = static_cast<std::int64_t>(std::llround(static_cast<double>(item.amount.value()) * factor));
89 if (amount <= 0) continue;
90 auto scaled = resource::ResourceCost::create(item.resource.value(), amount);
91 if (!scaled) return Result<resource::CostSpec>::failure(scaled.status());
92 items.push_back(std::move(scaled).takeValue());
93 }
94 return resource::CostSpec::create(std::move(items));
95}
96
97std::uint64_t stableRallyGroup(SubjectRef subject) {
98 std::uint64_t hash = 1469598103934665603ull;
99 for (unsigned char byte : subject.format()) {
100 hash ^= byte;
101 hash *= 1099511628211ull;
102 }
103 return hash == 0 ? 1 : hash;
104}
105
106SubjectRef deterministicSubject(std::string_view seed, std::uint64_t sequence) {
107 const auto hash = [&](std::uint64_t basis) {
108 std::uint64_t value = basis;
109 for (unsigned char byte : seed) {
110 value ^= byte;
111 value *= 1099511628211ull;
112 }
113 for (unsigned shift = 0; shift < 64; shift += 8) {
114 value ^= static_cast<unsigned char>(sequence >> shift);
115 value *= 1099511628211ull;
116 }
117 return value;
118 };
119 const std::uint64_t high = hash(1469598103934665603ull);
120 const std::uint64_t low = hash(1099511628211ull);
122 for (unsigned index = 0; index < 8; ++index) {
123 bytes[index] = static_cast<std::uint8_t>(high >> ((7 - index) * 8));
124 bytes[8 + index] = static_cast<std::uint8_t>(low >> ((7 - index) * 8));
125 }
126 bytes[6] = static_cast<std::uint8_t>((bytes[6] & 0x0f) | 0x70);
127 bytes[8] = static_cast<std::uint8_t>((bytes[8] & 0x3f) | 0x80);
129}
130
131bool controls(const GameplaySession& session, SubjectRef subject) {
132 return session.access != GameplayAccess::PlayerEquivalent ||
133 std::find(session.controlledSubjects.begin(), session.controlledSubjects.end(), subject) !=
134 session.controlledSubjects.end();
135}
136
137LogicalId gameplayId(std::string_view value) { return LogicalId::parse(value).value(); }
138
139Result<double> numericParameter(const Value& parameters, std::string_view name) {
140 const auto* object = parameters.getIf<Value::Object>();
141 if (!object)
143 "RTS gameplay parameters must be an object", "parameters"));
144 const auto found = object->find(std::string(name));
145 if (found == object->end() || !found->second.isNumeric())
147 "RTS gameplay parameter must be numeric",
148 "parameters." + std::string(name)));
149 return Result<double>::success(found->second.isInt64() ? static_cast<double>(found->second.asInt())
150 : found->second.asDouble());
151}
152
153template <typename T>
154T* tryGetCurrent(ecs::EntityHandle handle) {
155 if (handle.table != ecs::current()) return nullptr;
156 return dynamic_cast<T*>(ecs::try_get(handle));
157}
158
159template <typename T>
160void destroyHandles(std::vector<ecs::EntityHandle>& handles) {
161 for (const auto& handle : handles) {
162 if (auto* typed = tryGetCurrent<T>(handle)) typed->release();
163 }
164 handles.clear();
165}
166
167template <typename T>
168std::size_t countLive(const std::vector<ecs::EntityHandle>& handles) {
169 return static_cast<std::size_t>(std::count_if(handles.begin(), handles.end(), [](const ecs::EntityHandle& handle) {
170 return tryGetCurrent<T>(handle) != nullptr;
171 }));
172}
173
174template <typename T>
175bool owns(const std::vector<ecs::EntityHandle>& handles, const T& entity) {
176 const auto live = ecs::handle_of(const_cast<T*>(&entity));
177 return std::any_of(handles.begin(), handles.end(), [&live](const auto& handle) {
178 return handle.table == live.table && handle.type == live.type && handle.id == live.id &&
179 handle.generation == live.generation;
180 });
181}
182
183template <typename T>
184T* findSubject(const std::vector<ecs::EntityHandle>& handles, SubjectRef subject) {
185 for (const auto& handle : handles) {
186 auto* entity = tryGetCurrent<T>(handle);
187 if (entity != nullptr && entity->identity()->subject == subject) return entity;
188 }
189 return nullptr;
190}
191
192Result<LogicalId> parseScriptDefinition(std::string_view text) {
193 if (text.empty()) return Result<LogicalId>::success(LogicalId{});
194 const auto parsed = LogicalId::parse(text);
195 if (!parsed)
197 DiagnosticCode::InvalidArgument, "RTS script definition must be a canonical logical id", "definition"));
198 return Result<LogicalId>::success(*parsed);
199}
200
201Result<CombatStance> parseCombatStance(std::string_view text) {
206 DiagnosticCode::InvalidArgument, "RTS combat stance must be passive, defensive, or aggressive", "stance"));
207}
208
209} // namespace
210
212
218
228
234
235 struct Checkpoint {
237 std::map<std::string, economy::EconomyLedger::Snapshot> economies;
238 std::map<std::string, map::Fov::Snapshot> fovs;
239 std::map<std::string, SubjectRef> pendingProductionSubjects;
240 std::vector<PaidProduction> paidProduction;
241 std::vector<PaidConstruction> paidConstruction;
243 std::vector<float> navigationCosts;
244 std::vector<float> terrainElevations;
245 std::uint64_t nextTick = 1;
246 std::uint64_t aiProductionSequence = 1;
247 };
248
257 std::map<std::string, std::unique_ptr<EconomySlot>> economies;
258 std::map<std::string, std::unique_ptr<map::Fov>> fovs;
259 std::map<std::string, SubjectRef> pendingProductionSubjects;
260 std::vector<PaidProduction> paidProduction;
261 std::vector<PaidConstruction> paidConstruction;
262 std::map<std::string, Checkpoint> checkpoints;
263 std::uint64_t nextTick = 1;
264 std::uint64_t aiProductionSequence = 1;
265 bool spawningProduction = false;
266 int width = 0;
267 int height = 0;
268 float cellSize = 1.0f;
269 float originX = 0.0f;
270 float originY = 0.0f;
271 std::vector<float> terrainElevations;
272 bool configured = false;
273};
274
279
280RTS::RTS() : gameplayRuntime_(std::make_unique<GameplayRuntime>()) {
281 cap::addListener<IGameplayControlProvider>(this);
282 cap::addListener<IGameplayInstanceCatalog>(this);
283}
284
286 cap::removeListener<IGameplayInstanceCatalog>(this);
287 cap::removeListener<IGameplayControlProvider>(this);
288 FogOfWarSystem::clear(fogState_);
289 setCrowdProvider(nullptr);
290 setCombatProviders(nullptr, nullptr);
291 clearOwnedRoots();
292}
293
294void RTS::clearOwnedRoots() noexcept {
295 movementGroups_.clear();
296 destroyHandles<Match>(matches_);
297 destroyHandles<Unit>(units_);
298 destroyHandles<Building>(buildings_);
299 destroyHandles<ResourceNode>(resourceNodes_);
300 destroyHandles<Player>(players_);
301 destroyHandles<Faction>(factions_);
302 destroyHandles<weapon::WeaponEntity>(weapons_);
303}
304
305void RTS::setFogProvider(FogProvider provider) noexcept {
306 FogOfWarSystem::clear(fogState_);
307 fogProvider_ = std::move(provider);
308 for (const auto& handle : factions_)
309 if (auto* faction = tryGetCurrent<Faction>(handle)) faction->intel()->enabled = static_cast<bool>(fogProvider_);
310}
311
313 if (sensing_ != nullptr && (sensing_ != sensing || damage == nullptr)) {
314 for (const auto& id : combatState_.mirroredSubjects)
315 sensing_->remove(id).ignore("best-effort RTS sensing mirror cleanup");
316 combatState_.mirroredSubjects.clear();
317 combatState_.blockedSubjects.clear();
318 }
319 sensing_ = sensing;
320 damage_ = damage;
321}
322
324 if (damage_ == nullptr)
326 Diagnostic::error(DiagnosticCode::NotFound, "RTS combat damage provider is not attached", "damage"));
327 return damage_->configureSettlementRules(rules);
328}
329
330void RTS::setCrowdProvider(crowd::Crowd* crowd) noexcept {
331 if (crowd_ == crowd) return;
332 if (crowd_ != nullptr) {
333 for (const auto& handle : units_) {
334 auto* unit = tryGetCurrent<Unit>(handle);
335 if (unit != nullptr && unit->crowd()->link.isBound()) crowd_->removeNamedAgent(unit->crowd()->link.key());
336 }
337 }
338
339 crowd_ = crowd;
340}
341
342std::string_view RTS::gameplayDomain() const noexcept { return "rts"; }
343
344std::vector<SubjectRef> RTS::gameplayInstances() const {
345 // One RTS instance is one player; the instance identity is that player's
346 // subject, which is also what `observeGameplay` resolves.
347 std::vector<SubjectRef> result;
348 for (const auto& handle : players_) {
349 auto* player = tryGetCurrent<Player>(handle);
350 if (player != nullptr && player->identity()->subject.isValid()) result.push_back(player->identity()->subject);
351 }
352 std::sort(result.begin(), result.end(),
353 [](const SubjectRef& left, const SubjectRef& right) { return left.format() < right.format(); });
354 return result;
355}
356
358 Player* player = resolvePlayer(instance);
359 if (!player)
361 Diagnostic::error(DiagnosticCode::NotFound, "RTS gameplay player was not found", "instance"));
362 if (!controls(session, instance))
364 DiagnosticCode::PreconditionViolation, "session does not control this RTS player", "instance"));
366 for (const auto& handle : player->selection()->units) {
367 auto* unit = tryGetCurrent<Unit>(handle);
368 if (!unit) continue;
369 auto current = unit->orders()->values.current();
370 std::string orderKind;
371 if (current) {
372 orderKind = orderKindName(current.value().kind);
373 } else if (current.code() == StatusCode::NotFound) {
374 current.ignore("idle RTS unit has no active order");
375 } else {
377 }
378 units.emplace_back(Value::Object{{"activeOrder", Value(std::move(orderKind))},
379 {"arrived", Value(unit->motion()->arrived)},
380 {"subject", Value(unit->identity()->subject.format())},
381 {"x", Value(unit->motion()->x)},
382 {"y", Value(unit->motion()->y)}});
383 }
384 GameplayObservation observation;
385 observation.domain = gameplayId("gameplay:rts");
386 observation.instance = instance;
387 observation.tick = player->selection()->tick;
388 observation.revision = player->selection()->revision;
389 observation.state = Value(Value::Object{{"selectedUnits", Value(std::move(units))}});
390 return Result<GameplayObservation>::success(std::move(observation));
391}
392
394 SubjectRef instance,
395 SubjectRef subject) const {
396 Player* player = resolvePlayer(instance);
397 Unit* unit = resolveUnit(subject);
398 if (!player || !unit)
400 Diagnostic::error(DiagnosticCode::NotFound, "RTS gameplay player or unit was not found", "subject"));
401 if (!controls(session, instance))
403 DiagnosticCode::PreconditionViolation, "session does not control this RTS player", "instance"));
404 const auto unitHandle = ecs::handle_of(unit);
405 const bool selected =
406 std::any_of(player->selection()->units.begin(), player->selection()->units.end(), [&](const auto& handle) {
407 return handle.table == unitHandle.table && handle.type == unitHandle.type && handle.id == unitHandle.id &&
408 handle.generation == unitHandle.generation;
409 });
410 if (!selected)
412 DiagnosticCode::PreconditionViolation, "RTS unit is not in the player's selection", "subject"));
413 Value number(Value::Object{{"type", Value("number")}});
414 Value schema(Value::Object{{"x", number}, {"y", number}});
415 std::vector<GameplayActionDescriptor> actions;
416 actions.push_back({gameplayId("rts:move"), schema});
417 actions.push_back({gameplayId("rts:attack"), std::move(schema)});
418 return Result<std::vector<GameplayActionDescriptor>>::success(std::move(actions));
419}
420
422 const GameplayCommand& command) {
423 Player* player = resolvePlayer(instance);
424 if (!player)
426 Diagnostic::error(DiagnosticCode::NotFound, "RTS gameplay player was not found", "instance"));
427 if (!controls(session, instance))
429 DiagnosticCode::PreconditionViolation, "session does not control this RTS player", "instance"));
430 if (command.id.empty())
432 Diagnostic::error(DiagnosticCode::InvalidArgument, "command id must not be empty", "command.id"));
433 if (command.observedTick != player->selection()->tick || command.expectedRevision != player->selection()->revision)
435 DiagnosticCode::Conflict, "RTS command was based on a stale observation", "command.expectedRevision"));
436 auto x = numericParameter(command.parameters, "x");
437 auto y = numericParameter(command.parameters, "y");
438 if (!x) return Result<GameplayCommandReceipt>::failure(x.status());
439 if (!y) return Result<GameplayCommandReceipt>::failure(y.status());
440
441 CommandSpec spec;
442 if (command.action == gameplayId("rts:move")) {
443 spec.kind = OrderKind::Move;
444 spec.definitionId = "rts:move";
445 } else if (command.action == gameplayId("rts:attack")) {
446 spec.kind = OrderKind::Attack;
447 spec.definitionId = "rts:attack";
448 } else {
450 Diagnostic::error(DiagnosticCode::Unsupported, "unsupported RTS gameplay action", "command.action"));
451 }
452 spec.target = {static_cast<float>(x.value()), static_cast<float>(y.value())};
453 FormationSpec formation;
454 auto accepted = fanOut(*player->selection(), spec, formation);
455 if (!accepted) return Result<GameplayCommandReceipt>::failure(accepted.status());
456 auto fanOutReceipt = std::move(accepted).takeValue();
457
459 receipt.commandId = command.id;
460 receipt.executionId = fanOutReceipt.orderIds.empty() ? std::string{} : fanOutReceipt.orderIds.front();
461 receipt.acceptedTick = player->selection()->tick;
462 receipt.resultingRevision = player->selection()->revision;
463 Value::Array orderIds;
464 for (auto& id : fanOutReceipt.orderIds) orderIds.emplace_back(std::move(id));
465 receipt.details = Value(Value::Object{{"accepted", Value(static_cast<std::int64_t>(fanOutReceipt.accepted))},
466 {"orderIds", Value(std::move(orderIds))}});
467 GameplayEvent event;
468 event.sequence = nextGameplayEventSequence_++;
469 event.tick = player->selection()->tick;
470 event.type = "rts.command.accepted";
471 event.subject = command.subject;
472 event.causationCommandId = command.id;
473 event.correlationId = command.id;
474 event.payload =
475 Value(Value::Object{{"action", Value(command.action.format())},
476 {"instance", Value(instance.format())},
477 {"resultingRevision", Value(static_cast<std::int64_t>(player->selection()->revision))}});
478 gameplayEvents_.push_back(std::move(event));
480}
481
483 const SimulationStep& simulationStep) {
484 Player* player = resolvePlayer(instance);
485 if (!player)
487 Diagnostic::error(DiagnosticCode::NotFound, "RTS gameplay player was not found", "instance"));
488 if (!controls(session, instance))
490 DiagnosticCode::PreconditionViolation, "session does not control this RTS player", "instance"));
491 if (simulationStep.tick <= player->selection()->tick)
493 Diagnostic::error(DiagnosticCode::Conflict, "RTS simulation tick must increase", "step.tick"));
494 auto advanced = step(simulationStep, gameplayRuntime_->adapter);
495 if (!advanced) return Result<GameplayObservation>::failure(advanced.status());
496 std::move(advanced).takeValue();
497 player->selection()->tick = simulationStep.tick;
498 ++player->selection()->revision;
499 return observeGameplay(session, instance);
500}
501
503 std::uint64_t afterSequence) const {
504 auto observation = observeGameplay(session, instance);
505 if (!observation) return Result<std::vector<GameplayEvent>>::failure(observation.status());
506 std::move(observation).takeValue();
507 std::vector<GameplayEvent> result;
508 for (const auto& event : gameplayEvents_) {
509 const auto* eventInstance = event.payload.find("instance");
510 if (event.sequence > afterSequence && eventInstance && eventInstance->isString() &&
511 eventInstance->asString() == instance.format())
512 result.push_back(event);
513 }
514 const bool empty = result.empty();
515 return Result<std::vector<GameplayEvent>>::success(std::move(result),
517}
518
520 if (!validSubject(subject))
522 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS Unit requires a valid SubjectRef", "subject"));
523 if (ownsSubject(subject, scriptRuntime_ && scriptRuntime_->spawningProduction ? SubjectClaimScope::Live
524 : SubjectClaimScope::LiveOrReserved))
526 Diagnostic::error(DiagnosticCode::Conflict, "RTS SubjectRef is already owned by this module", "subject"));
527 Unit* unit = Unit::createUnit(subject, std::move(definition));
528 if (unit == nullptr)
530 Diagnostic::error(DiagnosticCode::Failed, "ECS failed to create an RTS Unit", "unit"));
531 unit->attributes()->values = AttributeComponent{};
532 if (!scriptRuntime_ || !scriptRuntime_->spawningProduction) {
533 auto attributes = RTSUnitAttributeAdapter::ensure(*unit);
534 if (!attributes) {
535 const auto status = attributes.status();
536 unit->release();
538 }
539 }
540 auto effects = unit->effects()->values.bindSubject(subject);
541 if (!effects) {
542 const auto status = effects.status();
543 unit->release();
545 }
546 units_.push_back(ecs::handle_of(unit));
548}
549
551 if (!validSubject(subject))
553 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS Building requires a valid SubjectRef", "subject"));
554 if (ownsSubject(subject, SubjectClaimScope::LiveOrReserved))
556 Diagnostic::error(DiagnosticCode::Conflict, "RTS SubjectRef is already owned by this module", "subject"));
557 Building* building = Building::createBuilding(subject, std::move(definition));
558 if (building == nullptr)
560 Diagnostic::error(DiagnosticCode::Failed, "ECS failed to create an RTS Building", "building"));
561 auto effects = building->effects()->values.bindSubject(subject);
562 if (!effects) {
563 const auto status = effects.status();
564 building->release();
566 }
567 buildings_.push_back(ecs::handle_of(building));
569}
570
571Result<ResourceNode*> RTS::newResourceNode(SubjectRef subject, std::string resourceType, float amount,
572 WorldPosition position, std::size_t workerCapacity) {
573 if (!validSubject(subject) || resourceType.empty() || !std::isfinite(amount) || amount < 0.0f ||
574 !std::isfinite(position.x) || !std::isfinite(position.y) || workerCapacity == 0)
576 DiagnosticCode::InvalidArgument, "RTS resource node requires valid identity, stock, position and capacity",
577 "resourceNode"));
578 if (ownsSubject(subject, SubjectClaimScope::LiveOrReserved))
580 Diagnostic::error(DiagnosticCode::Conflict, "RTS SubjectRef is already owned by this module", "subject"));
582 if (node == nullptr)
584 Diagnostic::error(DiagnosticCode::Failed, "ECS failed to create an RTS ResourceNode", "resourceNode"));
585 node->position()->x = position.x;
586 node->position()->y = position.y;
587 node->stock()->resourceType = std::move(resourceType);
588 node->stock()->remaining = amount;
589 node->stock()->maximum = amount;
590 node->harvest()->capacity = workerCapacity;
591 resourceNodes_.push_back(ecs::handle_of(node));
593}
594
596 if (!validSubject(subject))
598 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS Player requires a valid SubjectRef", "subject"));
599 if (ownsSubject(subject, SubjectClaimScope::LiveOrReserved))
601 Diagnostic::error(DiagnosticCode::Conflict, "RTS SubjectRef is already owned by this module", "subject"));
603 if (player == nullptr)
605 Diagnostic::error(DiagnosticCode::Failed, "ECS failed to create an RTS Player", "player"));
606 players_.push_back(ecs::handle_of(player));
608}
609
611 if (!validSubject(subject))
613 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS Faction requires a valid SubjectRef", "subject"));
614 if (ownsSubject(subject, SubjectClaimScope::LiveOrReserved))
616 Diagnostic::error(DiagnosticCode::Conflict, "RTS SubjectRef is already owned by this module", "subject"));
618 if (faction == nullptr)
620 Diagnostic::error(DiagnosticCode::Failed, "ECS failed to create an RTS Faction", "faction"));
621 factions_.push_back(ecs::handle_of(faction));
622 faction->intel()->enabled = static_cast<bool>(fogProvider_);
623 if (scriptRuntime_ && scriptRuntime_->configured) {
624 const std::string key = faction->identity()->subject.format();
625 scriptRuntime_->economies.emplace(key, std::make_unique<ScriptRuntime::EconomySlot>());
626 auto factionFov = std::make_unique<map::Fov>(scriptRuntime_->width, scriptRuntime_->height);
627 factionFov->setMode("heightmap");
628 factionFov->setEyeOffset(1.0f);
629 factionFov->setCliffBlock(0.0f);
630 for (int y = 0; y < scriptRuntime_->height; ++y)
631 for (int x = 0; x < scriptRuntime_->width; ++x)
632 if (!scriptRuntime_->terrainElevations.empty())
633 factionFov->setElevation(
634 x, y,
635 scriptRuntime_->terrainElevations[static_cast<std::size_t>(y * scriptRuntime_->width + x)]);
636 scriptRuntime_->fovs.emplace(key, std::move(factionFov));
637 auto economyLink = EconomyLink::bind("rts/script/economy/" + key);
638 if (!economyLink) {
639 const Status status = economyLink.status();
640 scriptRuntime_->economies.erase(key);
641 scriptRuntime_->fovs.erase(key);
642 faction->release();
643 factions_.pop_back();
645 }
646 faction->economy()->link = std::move(economyLink).takeValue();
647 }
649}
650
651Result<void> RTS::materialize(Unit& unit) {
652 if (definitions_ == nullptr || !unit.definition()->id.isValid())
654 const std::size_t weaponCount = weapons_.size();
655 ArchetypeWeaponFactory factory = [this](std::string_view definitionId,
657 auto runtime = weapon::WeaponDefinitionRuntime::create(*definitions_, definitionId, instanceId);
658 if (!runtime) return Result<weapon::WeaponEntity*>::failure(runtime.status());
659 auto* entity = weapon::WeaponEntity::createWeapon();
660 if (entity == nullptr)
662 Diagnostic::error(DiagnosticCode::Failed, "ECS failed to create an RTS weapon instance", "weapon"));
663 auto applied = runtime.value().applyTo(entity);
664 if (!applied) {
665 const Status status = applied.status();
666 entity->release();
668 }
669 weapons_.push_back(ecs::handle_of(entity));
671 };
672 auto applied = RTSArchetypeMaterializer::apply(*definitions_, unit, factory);
673 if (!applied) {
674 while (weapons_.size() > weaponCount) {
675 if (auto* entity = dynamic_cast<weapon::WeaponEntity*>(ecs::try_get(weapons_.back()))) entity->release();
676 weapons_.pop_back();
677 }
678 }
679 return applied;
680}
681
682Result<void> RTS::materialize(Building& building) {
683 if (definitions_ == nullptr || !building.definition()->id.isValid())
685 const std::size_t weaponCount = weapons_.size();
686 ArchetypeWeaponFactory factory = [this](std::string_view definitionId,
687 PersistentId instanceId) -> Result<weapon::WeaponEntity*> {
688 auto runtime = weapon::WeaponDefinitionRuntime::create(*definitions_, definitionId, instanceId);
689 if (!runtime) return Result<weapon::WeaponEntity*>::failure(runtime.status());
690 auto* entity = weapon::WeaponEntity::createWeapon();
691 if (entity == nullptr)
693 Diagnostic::error(DiagnosticCode::Failed, "ECS failed to create an RTS weapon instance", "weapon"));
694 auto applied = runtime.value().applyTo(entity);
695 if (!applied) {
696 const Status status = applied.status();
697 entity->release();
699 }
700 weapons_.push_back(ecs::handle_of(entity));
702 };
703 auto applied = RTSArchetypeMaterializer::apply(*definitions_, building, factory);
704 if (!applied) {
705 while (weapons_.size() > weaponCount) {
706 if (auto* entity = dynamic_cast<weapon::WeaponEntity*>(ecs::try_get(weapons_.back()))) entity->release();
707 weapons_.pop_back();
708 }
709 }
710 return applied;
711}
712
714 if (!owns(factions_, faction))
716 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS Faction does not belong to this facade", "faction"));
717 auto created = newUnit(subject, std::move(definition));
718 if (!created) return created;
719 Unit* unit = std::move(created).takeValue();
720 const std::size_t weaponCount = weapons_.size();
721 const auto rollbackWeapons = [this, weaponCount]() {
722 while (weapons_.size() > weaponCount) {
723 if (auto* weapon = dynamic_cast<weapon::WeaponEntity*>(ecs::try_get(weapons_.back()))) weapon->release();
724 weapons_.pop_back();
725 }
726 };
727 auto archetype = materialize(*unit);
728 if (!archetype) {
729 const Status status = archetype.status();
730 unit->release();
731 units_.pop_back();
732 rollbackWeapons();
734 }
735 auto link = FactionLink::bind(ecs::handle_of(&faction));
736 if (!link) {
737 const Status status = link.status();
738 unit->release();
739 units_.pop_back();
740 rollbackWeapons();
742 }
743 unit->faction()->link = std::move(link).takeValue();
744 faction.members()->units.push_back(ecs::handle_of(unit));
745 if (scriptRuntime_ && scriptRuntime_->configured) {
746 const std::string key = unit->identity()->subject.format();
747 auto crowdLink = CrowdLink::bind(key);
748 auto sensingLink = SensingLink::bind(key);
749 if (!crowdLink || !sensingLink) {
750 const Status status = !crowdLink ? crowdLink.status() : sensingLink.status();
751 faction.members()->units.pop_back();
752 unit->release();
753 units_.pop_back();
754 rollbackWeapons();
756 }
757 unit->crowd()->link = std::move(crowdLink).takeValue();
758 unit->sensing()->link = std::move(sensingLink).takeValue();
759 }
761}
762
764 if (!owns(factions_, faction))
766 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS Faction does not belong to this facade", "faction"));
767 auto created = newBuilding(subject, std::move(definition));
768 if (!created) return created;
769 Building* building = std::move(created).takeValue();
770 const std::size_t weaponCount = weapons_.size();
771 const auto rollbackWeapons = [this, weaponCount]() {
772 while (weapons_.size() > weaponCount) {
773 if (auto* weapon = dynamic_cast<weapon::WeaponEntity*>(ecs::try_get(weapons_.back()))) weapon->release();
774 weapons_.pop_back();
775 }
776 };
777 auto archetype = materialize(*building);
778 if (!archetype) {
779 const Status status = archetype.status();
780 building->release();
781 buildings_.pop_back();
782 rollbackWeapons();
784 }
785 auto link = FactionLink::bind(ecs::handle_of(&faction));
786 if (!link) {
787 const Status status = link.status();
788 building->release();
789 buildings_.pop_back();
790 rollbackWeapons();
792 }
793 building->faction()->link = std::move(link).takeValue();
794 faction.members()->buildings.push_back(ecs::handle_of(building));
796}
797
799 if (!validSubject(subject))
801 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS Match requires a valid SubjectRef", "subject"));
802 if (ownsSubject(subject, SubjectClaimScope::LiveOrReserved))
804 Diagnostic::error(DiagnosticCode::Conflict, "RTS SubjectRef is already owned by this module", "subject"));
806 if (match == nullptr)
808 Diagnostic::error(DiagnosticCode::Failed, "ECS failed to create an RTS Match", "match"));
809 matches_.push_back(ecs::handle_of(match));
811}
812
813Result<void> RTS::configureMatch(Match& match, VictoryRule rule, std::string archetype, double targetValue) const {
814 if (!owns(matches_, match))
816 "RTS match is not owned by this composition root", "match"));
817 if (match.state()->phase != MatchPhase::Setup)
819 Diagnostic::error(DiagnosticCode::Conflict, "RTS match rules are immutable after start", "match.phase"));
820 if (rule == VictoryRule::DestroyHeadquarters && archetype.empty())
822 "RTS headquarters victory requires an archetype", "archetype"));
823 if (rule == VictoryRule::ResourceTarget && (archetype.empty() || !std::isfinite(targetValue) || targetValue <= 0.0))
825 "RTS resource victory requires a resource and positive target",
826 "targetValue"));
827 match.rules()->rule = rule;
828 match.rules()->archetype = std::move(archetype);
829 match.rules()->targetValue = targetValue;
831}
832
833Result<void> RTS::addMatchParticipant(Match& match, Faction& faction, int team) const {
834 if (!owns(matches_, match) || findFaction(faction.identity()->subject) != &faction)
836 DiagnosticCode::InvalidArgument, "RTS match and faction must share this composition root", "participant"));
837 return MatchSystem::addParticipant(match, faction, team);
838}
839
841 if (!owns(matches_, match))
843 "RTS match is not owned by this composition root", "match"));
844 return MatchSystem::start(match);
845}
846
848 if (!owns(matches_, match) || findFaction(faction.identity()->subject) != &faction)
850 DiagnosticCode::InvalidArgument, "RTS match and faction must share this composition root", "participant"));
851 return MatchSystem::surrender(match, faction);
852}
853
855 if (!owns(matches_, match))
857 "RTS match is not owned by this composition root", "match"));
858 const auto phase = match.state()->phase == MatchPhase::Setup ? "setup"
859 : match.state()->phase == MatchPhase::Running ? "running"
860 : "finished";
861 Value::Array participants;
862 for (auto& entry : match.participants()->entries) {
863 auto* faction = dynamic_cast<Faction*>(entry.faction.resolve());
864 participants.emplace_back(
865 Value::Object{{"faction", faction == nullptr ? std::string{} : faction->identity()->subject.format()},
866 {"team", entry.team},
867 {"eliminated", entry.eliminated},
868 {"surrendered", entry.surrendered},
869 {"reason", entry.reason}});
870 }
872 for (const auto& event : match.events()->values)
873 events.emplace_back(Value::Object{{"sequence", static_cast<std::int64_t>(event.sequence)},
874 {"kind", event.kind},
875 {"faction", event.faction.isValid() ? event.faction.format() : std::string{}},
876 {"team", event.team},
877 {"reason", event.reason}});
878 return Result<Value>::success(Value(Value::Object{{"subject", match.identity()->subject.format()},
879 {"phase", phase},
880 {"winningTeam", match.state()->winningTeam},
881 {"rule", static_cast<std::int64_t>(match.rules()->rule)},
882 {"archetype", match.rules()->archetype},
883 {"target", match.rules()->targetValue},
884 {"participants", Value(std::move(participants))},
885 {"events", Value(std::move(events))}}),
887}
888
889Unit* RTS::findUnit(SubjectRef subject) const noexcept { return findSubject<Unit>(units_, subject); }
890
891Building* RTS::findBuilding(SubjectRef subject) const noexcept { return findSubject<Building>(buildings_, subject); }
892
893ResourceNode* RTS::findResourceNode(SubjectRef subject) const noexcept {
894 return findSubject<ResourceNode>(resourceNodes_, subject);
895}
896
897bool RTS::ownsSubject(SubjectRef subject, SubjectClaimScope scope) const noexcept {
898 if (scope == SubjectClaimScope::LiveOrReserved && scriptRuntime_ &&
899 std::any_of(scriptRuntime_->pendingProductionSubjects.begin(), scriptRuntime_->pendingProductionSubjects.end(),
900 [subject](const auto& entry) { return entry.second == subject; }))
901 return true;
902 return findSubject<Unit>(units_, subject) != nullptr || findSubject<Building>(buildings_, subject) != nullptr ||
903 findSubject<ResourceNode>(resourceNodes_, subject) != nullptr ||
904 findSubject<Player>(players_, subject) != nullptr || findSubject<Faction>(factions_, subject) != nullptr ||
905 findSubject<Match>(matches_, subject) != nullptr;
906}
907
908Result<void> RTS::setUnitStance(std::span<const SubjectRef> subjects, CombatStance stance, float leashRange) const {
909 if (stance != CombatStance::Passive && stance != CombatStance::Defensive && stance != CombatStance::Aggressive)
911 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS combat stance is invalid", "stance"));
912 if (!std::isfinite(leashRange) || leashRange < 0.0f)
914 DiagnosticCode::InvalidArgument, "RTS combat leash range must be finite and non-negative", "leashRange"));
915 std::vector<Unit*> units;
916 units.reserve(subjects.size());
917 for (std::size_t index = 0; index < subjects.size(); ++index) {
918 auto* unit = findUnit(subjects[index]);
919 if (unit == nullptr)
921 "RTS stance unit identity was not found",
922 "subjects[" + std::to_string(index) + "]"));
923 units.push_back(unit);
924 }
925 for (auto* unit : units) {
926 auto combat = unit->combat();
927 combat->stance = stance;
928 combat->leashRange = leashRange;
929 combat->guardX = unit->motion()->x;
930 combat->guardY = unit->motion()->y;
931 combat->guardSet = true;
932 if (stance == CombatStance::Passive && unit->orders()->values.orderCount() == 0) combat->target = {};
933 }
935}
936
937Result<void> RTS::setUnitMovementPriority(std::span<const SubjectRef> subjects, int priority) const {
938 std::vector<Unit*> units;
939 units.reserve(subjects.size());
940 for (std::size_t index = 0; index < subjects.size(); ++index) {
941 auto* unit = findUnit(subjects[index]);
942 if (unit == nullptr)
944 "RTS movement-priority unit identity was not found",
945 "subjects[" + std::to_string(index) + "]"));
946 units.push_back(unit);
947 }
948 const int clamped = std::clamp(priority, -100, 100);
949 for (auto* unit : units) unit->navigation()->movementPriority = clamped;
951}
952
953Result<void> RTS::assignWorker(Unit& worker, ResourceNode& node, Building& dropoff) const {
954 if (!owns(units_, worker) || !owns(resourceNodes_, node) || !owns(buildings_, dropoff))
956 DiagnosticCode::StaleHandle, "RTS worker assignment requires roots owned by this facade", "assignment"));
957 if (!worker.durability()->alive || worker.durability()->state.health <= 0.0)
958 return Result<void>::failure(Diagnostic::error(DiagnosticCode::Conflict, "RTS worker must be alive", "worker"));
959 if (worker.worker()->capacity <= 0.0f || worker.worker()->gatherRate <= 0.0f ||
960 worker.worker()->resourceType.empty())
962 Diagnostic::error(DiagnosticCode::Conflict, "RTS unit is not configured as a resource worker", "worker"));
963 if ((!node.stock()->infinite && node.stock()->remaining <= 0.0f) ||
964 node.stock()->resourceType != worker.worker()->resourceType)
966 DiagnosticCode::Conflict, "RTS resource node is depleted or incompatible with the worker", "node"));
967 if (!dropoff.integrity()->alive || dropoff.construction()->progress < 1.0f ||
968 worker.faction()->link.resolve() == nullptr ||
969 worker.faction()->link.resolve() != dropoff.faction()->link.resolve() ||
970 std::find(dropoff.dropoff()->acceptedResources.begin(), dropoff.dropoff()->acceptedResources.end(),
971 worker.worker()->resourceType) == dropoff.dropoff()->acceptedResources.end())
973 DiagnosticCode::Conflict, "RTS dropoff must be completed, friendly, and accept the resource", "dropoff"));
974
975 auto& assigned = node.harvest()->workers;
976 std::erase_if(assigned, [](const ecs::EntityHandle& handle) { return tryGetCurrent<Unit>(handle) == nullptr; });
977 const auto workerHandle = ecs::handle_of(&worker);
978 const bool alreadyAssigned = std::any_of(assigned.begin(), assigned.end(),
979 [&](const auto& handle) { return sameHandle(handle, workerHandle); });
980 if (!alreadyAssigned && assigned.size() >= node.harvest()->capacity)
982 Diagnostic::error(DiagnosticCode::Conflict, "RTS resource node worker capacity is full", "node"));
983
984 auto nodeLink = ResourceNodeLink::bind(ecs::handle_of(&node));
985 if (!nodeLink) return Result<void>::failure(nodeLink.status());
986 auto dropoffLink = BuildingLink::bind(ecs::handle_of(&dropoff));
987 if (!dropoffLink) return Result<void>::failure(dropoffLink.status());
988 CommandSpec command;
989 command.kind = OrderKind::Gather;
990 command.target = {node.position()->x, node.position()->y};
991 command.targetEntity = ecs::handle_of(&node);
992 auto ordered = worker.orders()->values.replace(command);
993 if (!ordered) return Result<void>::failure(ordered.status());
994 std::move(ordered).takeValue();
995
996 if (auto* previous = dynamic_cast<ResourceNode*>(worker.worker()->resourceNode.resolve());
997 previous != nullptr && previous != &node) {
998 std::erase_if(previous->harvest()->workers,
999 [&](const auto& handle) { return sameHandle(handle, workerHandle); });
1000 }
1001 if (!alreadyAssigned) assigned.push_back(workerHandle);
1002 worker.worker()->resourceNode = std::move(nodeLink).takeValue();
1003 worker.worker()->dropoff = std::move(dropoffLink).takeValue();
1005}
1006
1007Result<void> RTS::setWorkerAutoAssignment(std::span<const SubjectRef> subjects, bool enabled) const {
1008 std::vector<Unit*> workers;
1009 workers.reserve(subjects.size());
1010 for (std::size_t index = 0; index < subjects.size(); ++index) {
1011 auto* unit = findUnit(subjects[index]);
1012 if (unit == nullptr)
1014 "RTS auto-assignment unit identity was not found",
1015 "subjects[" + std::to_string(index) + "]"));
1016 if (unit->worker()->capacity <= 0.0f || unit->worker()->gatherRate <= 0.0f ||
1017 unit->worker()->resourceType.empty())
1019 "RTS auto-assignment target is not a resource worker",
1020 "subjects[" + std::to_string(index) + "]"));
1021 workers.push_back(unit);
1022 }
1023 for (auto* worker : workers) worker->worker()->autoAssign = enabled;
1025}
1026
1027Result<void> RTS::configureWorkforce(Faction& faction, bool autoConstruction, int maxBuildersPerSite, bool autoRepair,
1028 int maxRepairersPerBuilding, int reserveWorkers) const {
1029 if (!owns(factions_, faction))
1031 DiagnosticCode::StaleHandle, "RTS workforce faction does not belong to this facade", "faction"));
1032 if (maxBuildersPerSite <= 0 || maxRepairersPerBuilding <= 0 || reserveWorkers < 0)
1033 return Result<void>::failure(
1035 "RTS workforce limits must be positive and reserve workers non-negative", "workforce"));
1036 auto workforce = faction.workforce();
1037 workforce->autoConstruction = autoConstruction;
1038 workforce->autoRepair = autoRepair;
1039 workforce->maxBuildersPerSite = static_cast<std::size_t>(maxBuildersPerSite);
1040 workforce->maxRepairersPerBuilding = static_cast<std::size_t>(maxRepairersPerBuilding);
1041 workforce->reserveWorkers = static_cast<std::size_t>(reserveWorkers);
1043}
1044
1045Result<std::string> RTS::assignBuilder(Unit& worker, Building& building) const {
1046 if (!owns(units_, worker) || !owns(buildings_, building))
1048 DiagnosticCode::StaleHandle, "RTS builder assignment requires roots owned by this facade", "assignment"));
1049 if (!worker.durability()->alive || worker.containment()->container.isBound() || worker.worker()->buildRate <= 0.0f)
1051 DiagnosticCode::Conflict, "RTS builder must be alive, deployed, and construction-capable", "worker"));
1052 if (!building.integrity()->alive || building.construction()->progress >= 1.0f || building.construction()->paused)
1054 DiagnosticCode::Conflict, "RTS construction target must be alive, unfinished, and active", "building"));
1055 if (worker.faction()->link.resolve() == nullptr ||
1056 worker.faction()->link.resolve() != building.faction()->link.resolve())
1058 DiagnosticCode::Conflict, "RTS builder and construction target must share a faction", "assignment"));
1059 CommandSpec command;
1060 command.kind = OrderKind::Build;
1061 command.target = {building.placement()->worldX, building.placement()->worldY};
1062 command.targetEntity = building.identity()->self;
1063 auto ordered = worker.orders()->values.replace(command);
1064 if (!ordered) return Result<std::string>::failure(ordered.status());
1065 return ordered;
1066}
1067
1068Result<int> RTS::addUnitReserveAmmo(Unit& unit, int rounds) const {
1069 if (!owns(units_, unit))
1071 DiagnosticCode::StaleHandle, "RTS reserve-ammunition unit does not belong to this facade", "unit"));
1072 if (!unit.durability()->alive)
1073 return Result<int>::failure(
1074 Diagnostic::error(DiagnosticCode::Conflict, "RTS reserve-ammunition unit must be alive", "unit"));
1075 auto* weaponEntity = dynamic_cast<weapon::WeaponEntity*>(unit.weapon()->link.resolve());
1076 if (weaponEntity == nullptr)
1077 return Result<int>::failure(
1078 Diagnostic::error(DiagnosticCode::NotFound, "RTS unit has no canonical weapon", "unit.weapon"));
1079 auto& resource = weaponEntity->state()->resource;
1080 if (resource.kind != weapon::ResourceKind::Ammo || resource.infinite)
1082 DiagnosticCode::Conflict, "RTS unit weapon has no finite reserve ammunition", "unit.weapon.resource"));
1083 if (auto* pool = weaponEntity->state()->ammoPool) {
1084 if (pool->state()->max < 0)
1086 DiagnosticCode::Conflict, "RTS unit weapon ammunition pool is infinite", "unit.weapon.ammoPool"));
1087 const long long adjusted = static_cast<long long>(pool->state()->count) + rounds;
1088 pool->state()->count = static_cast<int>(std::clamp<long long>(adjusted, 0, pool->state()->max));
1089 return Result<int>::success(pool->state()->count, Status::success(StatusCode::Applied));
1090 }
1091 if (resource.reserve < 0)
1093 "RTS unit weapon reserve ammunition is infinite",
1094 "unit.weapon.resource.reserve"));
1095 const auto* definition = weaponEntity->definition()->def;
1096 const int capacity = std::max(resource.reserve, definition == nullptr ? resource.reserve : definition->reserveSize);
1097 const long long adjusted = static_cast<long long>(resource.reserve) + rounds;
1098 resource.reserve = static_cast<int>(std::clamp<long long>(adjusted, 0, capacity));
1100}
1101
1102Result<float> RTS::addUnitAmmoSupply(Unit& unit, float rounds) const {
1103 if (!owns(units_, unit))
1105 DiagnosticCode::StaleHandle, "RTS ammunition-supply unit does not belong to this facade", "unit"));
1106 if (!unit.durability()->alive)
1108 Diagnostic::error(DiagnosticCode::Conflict, "RTS ammunition-supply unit must be alive", "unit"));
1109 if (!std::isfinite(rounds))
1111 "RTS ammunition supply adjustment must be finite", "rounds"));
1112 auto supply = unit.supply();
1113 supply->stock = std::clamp(supply->stock + rounds, 0.0f, std::max(0.0f, supply->capacity));
1115}
1116
1117Result<float> RTS::addBuildingAmmoSupply(Building& building, float rounds) const {
1118 if (!owns(buildings_, building))
1120 DiagnosticCode::StaleHandle, "RTS ammunition-supply building does not belong to this facade", "building"));
1121 if (!building.integrity()->alive)
1123 Diagnostic::error(DiagnosticCode::Conflict, "RTS ammunition-supply building must be alive", "building"));
1124 if (!std::isfinite(rounds))
1126 "RTS ammunition supply adjustment must be finite", "rounds"));
1127 auto supply = building.supply();
1128 supply->stock = std::clamp(supply->stock + rounds, 0.0f, std::max(0.0f, supply->capacity));
1130}
1131
1132Result<void> RTS::setUnitAutoResupply(Unit& unit, bool enabled) const {
1133 if (!owns(units_, unit))
1135 DiagnosticCode::StaleHandle, "RTS automatic-resupply unit does not belong to this facade", "unit"));
1136 if (!unit.durability()->alive)
1137 return Result<void>::failure(
1138 Diagnostic::error(DiagnosticCode::Conflict, "RTS automatic-resupply unit must be alive", "unit"));
1139 auto supply = unit.supply();
1140 supply->autoDispatch = enabled;
1142 auto current = unit.orders()->values.current();
1143 if (current && (current.value().kind == OrderKind::Resupply || current.value().kind == OrderKind::SupplyRelay)) {
1144 auto cancelled = unit.orders()->values.cancel(current.value().id, "automatic resupply disabled");
1145 if (!cancelled) return cancelled;
1146 supply->assignedTarget = {};
1147 supply->reservedStock = 0.0f;
1148 supply->returning = false;
1149 supply->rendezvousActive = false;
1150 supply->convoyWaiting = false;
1151 supply->convoyLeader = {};
1152 auto navigation = unit.navigation();
1153 navigation->waypoints.clear();
1154 navigation->waypointIndex = 0;
1155 navigation->plannedOrderId.clear();
1156 navigation->unreachable = false;
1157 navigation->unreachableReported = false;
1158 unit.motion()->arrived = true;
1159 }
1161}
1162
1163Result<FanOutReceipt> RTS::suppressArea(std::span<const SubjectRef> subjects, WorldPosition start, WorldPosition end,
1164 float width, int shotsPerUnit) const {
1165 if (!std::isfinite(start.x) || !std::isfinite(start.y) || !std::isfinite(end.x) || !std::isfinite(end.y) ||
1166 !std::isfinite(width) || width <= 0.0f || shotsPerUnit < 0 ||
1167 std::hypot(end.x - start.x, end.y - start.y) <= 1e-3f)
1170 "RTS suppression requires a non-degenerate corridor, positive width, and non-negative shots",
1171 "suppression"));
1172 std::vector<Unit*> units;
1173 units.reserve(subjects.size());
1174 for (std::size_t index = 0; index < subjects.size(); ++index) {
1175 auto* unit = findUnit(subjects[index]);
1176 if (unit == nullptr)
1178 "RTS suppression unit identity was not found",
1179 "subjects[" + std::to_string(index) + "]"));
1180 if (!unit->durability()->alive || unit->containment()->container.isBound() ||
1181 unit->weapon()->link.resolve() == nullptr)
1183 Diagnostic::error(DiagnosticCode::Conflict, "RTS suppression unit must be alive, deployed, and armed",
1184 "subjects[" + std::to_string(index) + "]"));
1185 units.push_back(unit);
1186 }
1187 CommandSpec command;
1188 command.kind = OrderKind::SuppressArea;
1189 command.target = start;
1190 command.secondaryTarget = end;
1191 command.radius = width;
1192 auto issued = commandUnits(subjects, command);
1193 if (!issued) return issued;
1194 for (auto* unit : units) {
1195 unit->artillery()->suppressionShotsRemaining = shotsPerUnit == 0 ? -1 : shotsPerUnit;
1196 unit->artillery()->fireSupportRequester = {};
1197 }
1198 return issued;
1199}
1200
1201Result<FanOutReceipt> RTS::escortUnits(std::span<const SubjectRef> subjects, SubjectRef protectedSubject,
1202 float guardRadius, float spacing) const {
1203 if (!std::isfinite(guardRadius) || !std::isfinite(spacing) || guardRadius <= 0.0f || spacing <= 0.0f)
1206 "RTS escort guard radius and spacing must be finite and positive", "escort"));
1207 ecs::Entity* protectedEntity = findUnit(protectedSubject);
1208 if (protectedEntity == nullptr) protectedEntity = findBuilding(protectedSubject);
1209 ecs::Entity* protectedFaction = nullptr;
1211 float protectedRadius = 0.0f;
1212 if (auto* unit = dynamic_cast<Unit*>(protectedEntity);
1213 unit != nullptr && unit->durability()->alive && !unit->containment()->container.isBound()) {
1214 protectedFaction = unit->faction()->link.resolve();
1215 center = {unit->motion()->x, unit->motion()->y};
1216 protectedRadius = unit->crowd()->radius;
1217 } else if (auto* building = dynamic_cast<Building*>(protectedEntity);
1218 building != nullptr && building->integrity()->alive) {
1219 protectedFaction = building->faction()->link.resolve();
1220 center = {building->placement()->worldX, building->placement()->worldY};
1221 } else {
1223 Diagnostic::error(DiagnosticCode::NotFound, "RTS escort target must be a live deployed unit or building",
1224 "protectedSubject"));
1225 }
1226 if (protectedFaction == nullptr)
1228 DiagnosticCode::Conflict, "RTS escort target must have an owning faction", "protectedSubject"));
1229
1230 std::vector<Unit*> escorts;
1231 escorts.reserve(subjects.size());
1232 for (std::size_t index = 0; index < subjects.size(); ++index) {
1233 auto* unit = findUnit(subjects[index]);
1234 if (unit == nullptr)
1236 "RTS escort unit identity was not found",
1237 "subjects[" + std::to_string(index) + "]"));
1238 if (unit == protectedEntity || !unit->durability()->alive || unit->containment()->container.isBound() ||
1239 !FactionRelationSystem::isAllied(dynamic_cast<Faction*>(unit->faction()->link.resolve()),
1240 dynamic_cast<Faction*>(protectedFaction)))
1243 "RTS escort units must be live, deployed, friendly, and distinct from the target",
1244 "subjects[" + std::to_string(index) + "]"));
1245 escorts.push_back(unit);
1246 }
1247 std::sort(escorts.begin(), escorts.end(), [](Unit* left, Unit* right) {
1248 return left->identity()->subject.format() < right->identity()->subject.format();
1249 });
1250 std::vector<SubjectRef> orderedSubjects;
1251 orderedSubjects.reserve(escorts.size());
1252 for (auto* unit : escorts) orderedSubjects.push_back(unit->identity()->subject);
1253 CommandSpec command;
1254 command.kind = OrderKind::Escort;
1255 command.target = center;
1256 command.targetEntity = ecs::handle_of(protectedEntity);
1257 auto issued = commandUnits(orderedSubjects, command);
1258 if (!issued) return issued;
1259 constexpr float tau = 2.0f * static_cast<float>(std::numbers::pi);
1260 for (std::size_t index = 0; index < escorts.size(); ++index) {
1261 const float angle = tau * static_cast<float>(index) / static_cast<float>(escorts.size());
1262 const float distance = protectedRadius + escorts[index]->crowd()->radius + spacing;
1263 escorts[index]->tactics()->escortOffsetX = std::cos(angle) * distance;
1264 escorts[index]->tactics()->escortOffsetY = std::sin(angle) * distance;
1265 escorts[index]->tactics()->protectionRange = guardRadius;
1266 escorts[index]->combat()->leashRange = guardRadius;
1267 }
1268 return issued;
1269}
1270
1271Value RTS::inspectState() const {
1273 for (const auto& handle : units_) {
1274 auto* unit = tryGetCurrent<Unit>(handle);
1275 if (unit == nullptr) continue;
1276 std::string faction;
1277 if (auto* owner = dynamic_cast<Faction*>(unit->faction()->link.resolve()); owner != nullptr)
1278 faction = owner->identity()->subject.format();
1279 std::string order = "idle";
1280 auto current = unit->orders()->values.current();
1281 if (current) order = orderKindName(std::move(current).takeValue().kind);
1282 auto* resourceNode = dynamic_cast<ResourceNode*>(unit->worker()->resourceNode.resolve());
1283 auto* dropoff = dynamic_cast<Building*>(unit->worker()->dropoff.resolve());
1284 int reserveAmmo = 0;
1285 int reserveAmmoCapacity = 0;
1286 if (auto* weaponEntity = dynamic_cast<weapon::WeaponEntity*>(unit->weapon()->link.resolve())) {
1287 const auto& resource = weaponEntity->state()->resource;
1288 if (resource.kind == weapon::ResourceKind::Ammo && !resource.infinite) {
1289 if (auto* pool = weaponEntity->state()->ammoPool) {
1290 reserveAmmo = pool->state()->count;
1291 reserveAmmoCapacity = pool->state()->max;
1292 } else {
1293 reserveAmmo = resource.reserve;
1294 reserveAmmoCapacity = weaponEntity->definition()->def == nullptr
1295 ? resource.reserve
1296 : weaponEntity->definition()->def->reserveSize;
1297 }
1298 }
1299 }
1300 units.emplace_back(Value::Object{
1301 {"subject", unit->identity()->subject.format()},
1302 {"definition", unit->definition()->id.format()},
1303 {"faction", std::move(faction)},
1304 {"x", unit->motion()->x},
1305 {"y", unit->motion()->y},
1306 {"airborne", unit->motion()->airborne},
1307 {"order", std::move(order)},
1308 {"queuedOrders", static_cast<std::int64_t>(unit->orders()->values.orderCount())},
1309 {"stance", combatStanceName(unit->combat()->stance)},
1310 {"leashRange", unit->combat()->leashRange},
1311 {"movementPriority", unit->navigation()->movementPriority},
1312 {"trafficWaiting", unit->navigation()->trafficWaiting},
1313 {"cloaked", unit->vision()->cloaked},
1314 {"health", unit->durability()->state.health},
1315 {"maxHealth", unit->durability()->state.maxHealth},
1316 {"shield", unit->shield()->value},
1317 {"maxShield", unit->shield()->capacity},
1318 {"experience", unit->veterancy()->experience},
1319 {"veterancyLevel", static_cast<std::int64_t>(unit->veterancy()->level)},
1320 {"activeEffects", static_cast<std::int64_t>(unit->effects()->values.count())},
1321 {"inCommand", unit->command()->inCommand},
1322 {"garrisoned", unit->containment()->container.resolve() != nullptr},
1323 {"resourceType", unit->worker()->resourceType},
1324 {"cargo", unit->worker()->cargo},
1325 {"cargoCapacity", unit->worker()->capacity},
1326 {"autoAssign", unit->worker()->autoAssign},
1327 {"resourceNode", resourceNode == nullptr ? std::string{} : resourceNode->identity()->subject.format()},
1328 {"dropoff", dropoff == nullptr ? std::string{} : dropoff->identity()->subject.format()},
1329 {"reserveAmmo", static_cast<std::int64_t>(reserveAmmo)},
1330 {"reserveAmmoCapacity", static_cast<std::int64_t>(reserveAmmoCapacity)},
1331 {"supplyStock", unit->supply()->stock},
1332 {"supplyCapacity", unit->supply()->capacity},
1333 {"autoResupply", unit->supply()->autoDispatch},
1334 {"reservedSupply", unit->supply()->reservedStock},
1335 {"supplyReturning", unit->supply()->returning},
1336 });
1337 }
1338
1339 Value::Array buildings;
1340 for (const auto& handle : buildings_) {
1341 auto* building = tryGetCurrent<Building>(handle);
1342 if (building == nullptr) continue;
1343 std::string faction;
1344 if (auto* owner = dynamic_cast<Faction*>(building->faction()->link.resolve()); owner != nullptr)
1345 faction = owner->identity()->subject.format();
1346 buildings.emplace_back(Value::Object{
1347 {"subject", building->identity()->subject.format()},
1348 {"definition", building->definition()->id.format()},
1349 {"faction", std::move(faction)},
1350 {"x", building->placement()->worldX},
1351 {"y", building->placement()->worldY},
1352 {"complete", building->construction()->progress >= 1.0f},
1353 {"progress", building->construction()->progress},
1354 {"powered", building->infrastructure()->powered},
1355 {"productionQueue", static_cast<std::int64_t>(building->production()->values.taskCount())},
1356 {"health", building->integrity()->state.health},
1357 {"maxHealth", building->integrity()->state.maxHealth},
1358 {"shield", building->shield()->value},
1359 {"maxShield", building->shield()->capacity},
1360 {"activeEffects", static_cast<std::int64_t>(building->effects()->values.count())},
1361 {"supplyStock", building->supply()->stock},
1362 {"supplyCapacity", building->supply()->capacity},
1363 });
1364 }
1365
1366 Value::Array resourceNodes;
1367 for (const auto& handle : resourceNodes_) {
1368 auto* node = tryGetCurrent<ResourceNode>(handle);
1369 if (node == nullptr) continue;
1370 resourceNodes.emplace_back(Value::Object{
1371 {"subject", node->identity()->subject.format()},
1372 {"resource", node->stock()->resourceType},
1373 {"remaining", node->stock()->remaining},
1374 {"maximum", node->stock()->maximum},
1375 {"x", node->position()->x},
1376 {"y", node->position()->y},
1377 {"capacity", static_cast<std::int64_t>(node->harvest()->capacity)},
1378 {"workers", static_cast<std::int64_t>(node->harvest()->workers.size())},
1379 });
1380 }
1381
1382 Value::Array factions;
1383 auto* currentTable = ecs::current();
1384 for (const auto& handle : factions_) {
1385 if (handle.table != currentTable) continue;
1386 auto* faction = tryGetCurrent<Faction>(handle);
1387 if (faction == nullptr) continue;
1388 factions.emplace_back(Value::Object{
1389 {"subject", faction->identity()->subject.format()},
1390 {"displayName", faction->identity()->displayName},
1391 {"units", static_cast<std::int64_t>(faction->members()->units.size())},
1392 {"buildings", static_cast<std::int64_t>(faction->members()->buildings.size())},
1393 {"autoConstruction", faction->workforce()->autoConstruction},
1394 {"autoRepair", faction->workforce()->autoRepair},
1395 {"maxBuildersPerSite", static_cast<std::int64_t>(faction->workforce()->maxBuildersPerSite)},
1396 {"maxRepairersPerBuilding", static_cast<std::int64_t>(faction->workforce()->maxRepairersPerBuilding)},
1397 {"reserveWorkers", static_cast<std::int64_t>(faction->workforce()->reserveWorkers)},
1398 });
1399 }
1400
1401 return Value(Value::Object{
1402 {"units", Value(std::move(units))},
1403 {"buildings", Value(std::move(buildings))},
1404 {"resourceNodes", Value(std::move(resourceNodes))},
1405 {"factions", Value(std::move(factions))},
1406 });
1407}
1408
1409Value RTS::inspectFrameEvents() const {
1410 const auto channelName = [](DamageChannel channel) {
1411 switch (channel) {
1412 case DamageChannel::Ability: return "ability";
1413 case DamageChannel::Projectile: return "projectile";
1414 case DamageChannel::Weapon: return "weapon";
1415 }
1416 return "weapon";
1417 };
1418 const auto reactionName = [](combat::HitReaction reaction) {
1419 switch (reaction) {
1420 case combat::HitReaction::None: return "none";
1421 case combat::HitReaction::Flinch: return "flinch";
1422 case combat::HitReaction::Stagger: return "stagger";
1423 case combat::HitReaction::Knockdown: return "knockdown";
1424 case combat::HitReaction::Death: return "death";
1425 }
1426 return "none";
1427 };
1428 std::vector<std::pair<std::uint64_t, Value>> sequenced;
1429 sequenced.reserve(frameDamageEvents_.size() + frameCombatEvents_.size() + frameLifecycleEvents_.size());
1430 for (const auto& event : frameDamageEvents_) {
1431 sequenced.emplace_back(event.sequence, Value(Value::Object{
1432 {"type", "damage"},
1433 {"tick", static_cast<std::int64_t>(event.tick.value())},
1434 {"channel", channelName(event.channel)},
1435 {"source", event.outcome.source.format()},
1436 {"target", event.outcome.target.format()},
1437 {"damageType", event.request.damageType},
1438 {"previousHealth", event.outcome.previousHealth},
1439 {"health", event.outcome.health},
1440 {"appliedHealthDamage", event.outcome.appliedHealthDamage},
1441 {"appliedPoiseDamage", event.outcome.appliedPoiseDamage},
1442 {"reaction", reactionName(event.outcome.reaction)},
1443 {"killed", event.outcome.reaction == combat::HitReaction::Death},
1444 }));
1445 }
1446 const auto fireType = [](CombatFireEventKind kind) {
1447 switch (kind) {
1448 case CombatFireEventKind::WeaponFired: return "weapon_fired";
1449 case CombatFireEventKind::ProjectileFired: return "projectile_fired";
1450 case CombatFireEventKind::ShotMissed: return "shot_missed";
1451 case CombatFireEventKind::ReloadStarted: return "reload_started";
1452 case CombatFireEventKind::ReloadCompleted: return "reload_completed";
1453 case CombatFireEventKind::WeaponDry: return "weapon_dry";
1454 case CombatFireEventKind::FireBlocked: return "fire_blocked";
1455 }
1456 return "weapon_fired";
1457 };
1458 for (const auto& value : frameCombatEvents_)
1459 sequenced.emplace_back(value.sequence, Value(Value::Object{
1460 {"type", fireType(value.event.kind)},
1461 {"tick", static_cast<std::int64_t>(value.tick.value())},
1462 {"source", value.event.source.format()},
1463 {"target", value.event.target.format()},
1464 {"x", value.event.point.x},
1465 {"y", value.event.point.y},
1466 }));
1467 const auto lifecycleType = [](LifecycleEventKind kind) {
1468 switch (kind) {
1469 case LifecycleEventKind::SuppressionRecovered: return "suppression_recovered";
1470 case LifecycleEventKind::ShieldRecharged: return "shield_recharged";
1471 case LifecycleEventKind::ConstructionCompleted: return "construction_completed";
1472 case LifecycleEventKind::ProductionSpawnBlocked: return "production_spawn_blocked";
1473 case LifecycleEventKind::ProductionSpawnCleared: return "production_spawn_cleared";
1474 case LifecycleEventKind::UnitProduced: return "unit_produced";
1475 case LifecycleEventKind::BuildingCaptured: return "building_captured";
1476 case LifecycleEventKind::AbilityCast: return "ability_cast";
1477 case LifecycleEventKind::AbilityChannelStarted: return "ability_channel_started";
1478 case LifecycleEventKind::AbilityChannelTick: return "ability_channel_tick";
1479 case LifecycleEventKind::AbilityChannelCompleted: return "ability_channel_completed";
1480 case LifecycleEventKind::AbilityInterrupted: return "ability_interrupted";
1481 case LifecycleEventKind::AbilityChannelCancelled: return "ability_channel_cancelled";
1482 case LifecycleEventKind::StatusApplied: return "status_applied";
1483 case LifecycleEventKind::StatusExpired: return "status_expired";
1484 case LifecycleEventKind::AmmoProduced: return "ammo_produced";
1485 case LifecycleEventKind::SupplyDispatched: return "supply_dispatched";
1486 case LifecycleEventKind::SupplyRelayDispatched: return "supply_rendezvous_dispatched";
1487 case LifecycleEventKind::AmmoResupplied: return "ammo_resupplied";
1488 case LifecycleEventKind::SupplyRelayTransferred: return "supply_relay_transferred";
1489 case LifecycleEventKind::SupplyReturning: return "supply_returning";
1490 case LifecycleEventKind::SupplyReturned: return "supply_returned";
1491 }
1492 return "lifecycle";
1493 };
1494 for (const auto& value : frameLifecycleEvents_)
1495 sequenced.emplace_back(value.sequence, Value(Value::Object{
1496 {"type", lifecycleType(value.event.kind)},
1497 {"tick", static_cast<std::int64_t>(value.tick.value())},
1498 {"source", value.event.source.format()},
1499 {"target", value.event.target.format()},
1500 {"detail", value.event.detail},
1501 {"value", value.event.value},
1502 }));
1503 std::sort(sequenced.begin(), sequenced.end(),
1504 [](const auto& left, const auto& right) { return left.first < right.first; });
1506 events.reserve(sequenced.size());
1507 for (auto& [sequence, value] : sequenced) {
1508 (void)sequence;
1509 events.push_back(std::move(value));
1510 }
1511 return Value(std::move(events));
1512}
1513
1514void RTS::recordDamageEvent(const combat::DamageRequest& request, const combat::DamageOutcome& outcome,
1516 frameDamageEvents_.push_back({frameEventSequence_++, tick, channel, request, outcome});
1517}
1518
1519void RTS::recordCombatEvent(const CombatFireEvent& event, SimulationTick tick) {
1520 frameCombatEvents_.push_back({frameEventSequence_++, tick, event});
1521}
1522
1523void RTS::recordLifecycleEvent(const LifecycleEvent& event, SimulationTick tick) {
1524 frameLifecycleEvents_.push_back({frameEventSequence_++, tick, event});
1525}
1526
1527Result<double> RTS::readUnitAttribute(Unit& unit, std::string_view attribute) const {
1528 if (!owns(units_, unit))
1530 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS Unit does not belong to this facade", "unit"));
1531 return RTSUnitAttributeAdapter::read(unit, attribute);
1532}
1533
1534Result<void> RTS::setUnitAttribute(Unit& unit, std::string_view attribute, double value) const {
1535 if (!owns(units_, unit))
1536 return Result<void>::failure(
1537 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS Unit does not belong to this facade", "unit"));
1538 return RTSUnitAttributeAdapter::setBase(unit, attribute, value);
1539}
1540
1541Result<effects::EffectHandle> RTS::applyEffect(Unit& unit, const RTSEffectDefinition& definition) const {
1542 if (!owns(units_, unit))
1544 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS Unit does not belong to this facade", "unit"));
1545 return unit.effects()->values.apply(definition);
1546}
1547
1548Result<effects::EffectHandle> RTS::applyEffect(Building& building, const RTSEffectDefinition& definition) const {
1549 if (!owns(buildings_, building))
1551 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS Building does not belong to this facade", "building"));
1552 return building.effects()->values.apply(definition);
1553}
1554
1555Result<double> RTS::heal(SubjectRef source, SubjectRef target, double amount) const {
1556 if (!source.isValid() || !target.isValid() || !std::isfinite(amount) || amount <= 0.0)
1559 "RTS healing requires valid subjects and a finite positive amount", "healing"));
1560 if (!ownsSubject(source))
1562 Diagnostic::error(DiagnosticCode::NotFound, "RTS healing source was not found", "source"));
1563 combat::CombatState* state = nullptr;
1564 bool* alive = nullptr;
1565 if (auto* unit = findUnit(target)) {
1566 state = &unit->durability()->state;
1567 alive = &unit->durability()->alive;
1568 } else if (auto* building = findBuilding(target)) {
1569 state = &building->integrity()->state;
1570 alive = &building->integrity()->alive;
1571 } else {
1573 Diagnostic::error(DiagnosticCode::NotFound, "RTS healing target was not found", "target"));
1574 }
1575 if (!*alive || state->health <= 0.0)
1577 Diagnostic::error(DiagnosticCode::Conflict, "RTS healing target must be alive", "target"));
1578 auto valid = state->validate();
1579 if (!valid) return Result<double>::failure(valid.status());
1580 combat::DamageRuntime defaultSettlement;
1581 auto& settlement = damage_ != nullptr ? *damage_ : defaultSettlement;
1582 auto settled = settlement.heal(*state, source, amount);
1583 if (!settled) return Result<double>::failure(settled.status());
1584 const double applied = settled.value().applied;
1586}
1587
1589 double durationOverride) {
1590 if (definitions_ == nullptr)
1592 DiagnosticCode::Conflict, "RTS status effects require a definition registry", "definitions"));
1593 if (!ownsSubject(source))
1595 Diagnostic::error(DiagnosticCode::NotFound, "RTS status-effect source was not found", "source"));
1596 auto definition = resolveEffectDefinition(*definitions_, effect, source, durationOverride);
1597 if (!definition) return Result<effects::EffectHandle>::failure(definition.status());
1598 if (auto* unit = findUnit(target)) {
1599 if (!unit->durability()->alive)
1601 Diagnostic::error(DiagnosticCode::Conflict, "RTS status-effect target must be alive", "target"));
1602 auto applied = applyEffect(*unit, definition.value());
1603 if (applied)
1604 recordLifecycleEvent(
1605 {LifecycleEventKind::StatusApplied, source, target, effect, definition.value().duration},
1606 SimulationTick(scriptTick()));
1607 return applied;
1608 }
1609 if (auto* building = findBuilding(target)) {
1610 if (!building->integrity()->alive)
1612 Diagnostic::error(DiagnosticCode::Conflict, "RTS status-effect target must be alive", "target"));
1613 auto applied = applyEffect(*building, definition.value());
1614 if (applied)
1615 recordLifecycleEvent(
1616 {LifecycleEventKind::StatusApplied, source, target, effect, definition.value().duration},
1617 SimulationTick(scriptTick()));
1618 return applied;
1619 }
1621 Diagnostic::error(DiagnosticCode::NotFound, "RTS status-effect target was not found", "target"));
1622}
1623
1626 Duration duration, std::string productionKind, int priority,
1627 std::string transactionId, definition::DefinitionHandle definition) {
1628 if (!owns(buildings_, building))
1630 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS Building does not belong to this facade", "building"));
1631 std::vector<RTSProductionResourceReserve> reserves;
1632 if (auto* faction = dynamic_cast<Faction*>(building.faction()->link.resolve()); faction != nullptr) {
1633 reserves.reserve(faction->productionPolicy()->resourceReserves.size());
1634 for (const auto& [resource, policy] : faction->productionPolicy()->resourceReserves) {
1635 auto item = resource::ResourceCost::create(resource, policy.amount);
1636 if (!item) return Result<RTSBuildReceipt>::failure(item.status());
1637 reserves.push_back({std::move(item).takeValue(), policy.minimumPriority});
1638 }
1639 }
1640 return RTSProductionActionAdapter::build(building, action, account, std::move(cost), std::move(product),
1641 std::move(duration), std::move(productionKind), priority,
1642 std::move(transactionId), std::move(reserves), std::move(definition));
1643}
1644
1645Result<void> RTS::setProductionResourceReserve(Faction& faction, std::string resource, std::int64_t amount,
1646 int minimumPriority) {
1647 if (!owns(factions_, faction))
1648 return Result<void>::failure(
1649 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS Faction does not belong to this facade", "faction"));
1650 if (resource.empty() || amount < 0)
1653 "RTS production resource reserve requires a resource and non-negative amount", "reserve"));
1654 if (amount == 0) {
1655 faction.productionPolicy()->resourceReserves.erase(resource);
1657 }
1659 if (!valid) return Result<void>::failure(valid.status());
1660 faction.productionPolicy()->resourceReserves[std::move(resource)] = {amount, minimumPriority};
1662}
1663
1665 std::string productionTaskId, std::string orderId,
1666 resource::CostSpec refund, std::string reason) {
1667 if (!owns(buildings_, building))
1669 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS Building does not belong to this facade", "building"));
1670 return RTSProductionActionAdapter::cancel(building, account, std::move(productionTaskId), std::move(orderId),
1671 std::move(refund), std::move(reason));
1672}
1673
1674Result<std::size_t> RTS::step(const SimulationStep& simulationStep, IRTSActionExecutor& executor) {
1675 if (!preserveFrameEventsOnNextStep_) {
1676 frameDamageEvents_.clear();
1677 frameCombatEvents_.clear();
1678 frameLifecycleEvents_.clear();
1679 frameEventSequence_ = 0;
1680 }
1681 preserveFrameEventsOnNextStep_ = false;
1682 const DamageEventSink damageEvents =
1684 DamageChannel channel) { recordDamageEvent(request, outcome, tick, channel); };
1685 const CombatFireEventSink fireEvents = [this](const CombatFireEvent& event, SimulationTick tick) {
1686 recordCombatEvent(event, tick);
1687 };
1688 const LifecycleEventSink lifecycleEvents = [this](const LifecycleEvent& event, SimulationTick tick) {
1689 recordLifecycleEvent(event, tick);
1690 };
1691 auto assignments = WorkerAssignmentSystem::step();
1692 if (!assignments) return Result<std::size_t>::failure(assignments.status());
1693 std::size_t processed = std::move(assignments).takeValue();
1694
1695 auto workforce = WorkforceAssignmentSystem::step();
1696 if (!workforce) return Result<std::size_t>::failure(workforce.status());
1697 processed += std::move(workforce).takeValue();
1698
1699 auto ai = AISystem::step(simulationStep, aiProductionRequest_);
1700 if (!ai) return Result<std::size_t>::failure(ai.status());
1701 processed += std::move(ai).takeValue();
1702
1703 auto tactics = TacticsSystem::step(damage_, simulationStep);
1704 if (!tactics) return Result<std::size_t>::failure(tactics.status());
1705 processed += std::move(tactics).takeValue();
1706
1707 auto command = CommandNetworkSystem::step();
1708 if (!command) return Result<std::size_t>::failure(command.status());
1709 processed += std::move(command).takeValue();
1710
1711 auto commandState = CommandStateSystem::step();
1712 if (!commandState) return Result<std::size_t>::failure(commandState.status());
1713 processed += std::move(commandState).takeValue();
1714
1715 auto patrol = PatrolSystem::step();
1716 if (!patrol) return Result<std::size_t>::failure(patrol.status());
1717 processed += std::move(patrol).takeValue();
1718
1719 if (pathfinder_ != nullptr) {
1720 auto navigation = NavigationSystem::step(*pathfinder_, navigationGrid_, navigationEvent_);
1721 if (!navigation) return Result<std::size_t>::failure(navigation.status());
1722 processed += std::move(navigation).takeValue();
1723 auto traffic = TrafficReservationSystem::step(*pathfinder_, navigationGrid_);
1724 if (!traffic) return Result<std::size_t>::failure(traffic.status());
1725 processed += std::move(traffic).takeValue();
1726 }
1727
1728 auto convoy = SupplyConvoySystem::step();
1729 if (!convoy) return Result<std::size_t>::failure(convoy.status());
1730 processed += std::move(convoy).takeValue();
1731
1732 auto movementGroups = stepMovementGroups();
1733 if (!movementGroups) return Result<std::size_t>::failure(movementGroups.status());
1734 processed += std::move(movementGroups).takeValue();
1735
1736 if (fogProvider_) {
1737 auto fog = FogOfWarSystem::step(simulationStep, navigationGrid_, fogState_, fogProvider_);
1738 if (!fog) return Result<std::size_t>::failure(fog.status());
1739 processed += std::move(fog).takeValue();
1740 }
1741
1742 auto motion = MotionSystem::step(simulationStep);
1743 if (!motion) return Result<std::size_t>::failure(motion.status());
1744 processed += std::move(motion).takeValue();
1745
1746 if (crowd_ != nullptr) {
1747 auto crowdMotion = CrowdMotionSystem::step(simulationStep, *crowd_);
1748 if (!crowdMotion) return Result<std::size_t>::failure(crowdMotion.status());
1749 processed += std::move(crowdMotion).takeValue();
1750 }
1751
1752 auto movementOrders = MovementOrderSystem::step();
1753 if (!movementOrders) return Result<std::size_t>::failure(movementOrders.status());
1754 processed += std::move(movementOrders).takeValue();
1755
1756 auto containment = ContainmentSystem::step();
1757 if (!containment) return Result<std::size_t>::failure(containment.status());
1758 processed += std::move(containment).takeValue();
1759
1760 auto morale = MoraleSystem::step(simulationStep, lifecycleEvents);
1761 if (!morale) return Result<std::size_t>::failure(morale.status());
1762 processed += std::move(morale).takeValue();
1763
1764 auto shields = ShieldSystem::step(simulationStep, lifecycleEvents);
1765 if (!shields) return Result<std::size_t>::failure(shields.status());
1766 processed += std::move(shields).takeValue();
1767
1768 auto supply = SupplySystem::step(
1769 simulationStep, ammoProductionPurchase_, pathfinder_, navigationGrid_,
1770 [this](const LifecycleEvent& event, SimulationTick tick) { recordLifecycleEvent(event, tick); });
1771 if (!supply) return Result<std::size_t>::failure(supply.status());
1772 processed += std::move(supply).takeValue();
1773
1774 auto artillery = ArtillerySystem::step(simulationStep);
1775 if (!artillery) return Result<std::size_t>::failure(artillery.status());
1776 processed += std::move(artillery).takeValue();
1777
1778 auto fireSupport = FireSupportSystem::step(simulationStep);
1779 if (!fireSupport) return Result<std::size_t>::failure(fireSupport.status());
1780 processed += std::move(fireSupport).takeValue();
1781
1782 if (resourceCredit_) {
1783 auto mining = MiningSystem::step(simulationStep, resourceCredit_);
1784 if (!mining) return Result<std::size_t>::failure(mining.status());
1785 processed += std::move(mining).takeValue();
1786 }
1787
1788 auto construction = ConstructionSystem::step(simulationStep, lifecycleEvents);
1789 if (!construction) return Result<std::size_t>::failure(construction.status());
1790 processed += std::move(construction).takeValue();
1791
1792 auto infrastructure = InfrastructureSystem::step(simulationStep, passiveIncomeCredit_);
1793 if (!infrastructure) return Result<std::size_t>::failure(infrastructure.status());
1794 processed += std::move(infrastructure).takeValue();
1795
1796 combat::DamageRuntime defaultHealthSettlement;
1797 auto& healthSettlement = damage_ != nullptr ? *damage_ : defaultHealthSettlement;
1798 if (repairDebit_) {
1799 auto repair = RepairSystem::step(simulationStep, healthSettlement, repairDebit_);
1800 if (!repair) return Result<std::size_t>::failure(repair.status());
1801 processed += std::move(repair).takeValue();
1802 }
1803
1804 auto capture = CaptureSystem::step(simulationStep, lifecycleEvents);
1805 if (!capture) return Result<std::size_t>::failure(capture.status());
1806 processed += std::move(capture).takeValue();
1807
1808 if (damage_ != nullptr) {
1809 auto abilities = AbilitySystem::step(simulationStep, *damage_, damageEvents, lifecycleEvents);
1810 if (!abilities) return Result<std::size_t>::failure(abilities.status());
1811 processed += std::move(abilities).takeValue();
1812 auto projectileImpacts = projectiles_.step(simulationStep, *damage_, projectileCollisionQuery_, damageEvents);
1813 if (!projectileImpacts) return Result<std::size_t>::failure(projectileImpacts.status());
1814 processed += std::move(projectileImpacts).takeValue();
1815 }
1816 if (sensing_ != nullptr && damage_ != nullptr) {
1817 auto combat =
1818 CombatFireSystem::step(simulationStep, combatState_, *sensing_, *damage_, &projectiles_, fireLineQuery_,
1819 pathfinder_, navigationGrid_, combatHeightQuery_, damageEvents, fireEvents);
1820 if (!combat) return Result<std::size_t>::failure(combat.status());
1821 processed += std::move(combat).takeValue();
1822 }
1823
1824 auto actions = OrderActionSystem::step(simulationStep, executor);
1825 if (!actions) return Result<std::size_t>::failure(actions.status());
1826 processed += std::move(actions).takeValue();
1827
1828 auto reinforcementPolicy = ReinforcementProductionPolicySystem::step(simulationStep, reinforcementCancel_);
1829 if (!reinforcementPolicy) return Result<std::size_t>::failure(reinforcementPolicy.status());
1830 processed += std::move(reinforcementPolicy).takeValue();
1831
1832 if (crowd_ != nullptr) {
1833 // Containment/actions can change footprints after the movement phase.
1834 // Reconcile those projections without advancing simulation time before spawning.
1835 auto synchronized = CrowdMotionSystem::step({simulationStep.tick, Duration{}}, *crowd_);
1836 if (!synchronized) return Result<std::size_t>::failure(synchronized.status());
1837 processed += std::move(synchronized).takeValue();
1838 }
1839
1840 auto production =
1841 BuildingProductionSystem::step(simulationStep, productionSpawn_, productionSpawnPosition_, lifecycleEvents);
1842 if (!production) return Result<std::size_t>::failure(production.status());
1843 processed += std::move(production).takeValue();
1844 for (const auto& handle : units_) {
1845 auto* unit = tryGetCurrent<Unit>(handle);
1846 if (unit == nullptr || unit->attributes()->values.initialized()) continue;
1847 auto initialized = RTSUnitAttributeAdapter::ensure(*unit);
1848 if (!initialized) return Result<std::size_t>::failure(initialized.status());
1849 }
1850
1851 auto reinforcements = ReinforcementSystem::step();
1852 if (!reinforcements) return Result<std::size_t>::failure(reinforcements.status());
1853 processed += std::move(reinforcements).takeValue();
1854 if (definitions_ != nullptr) {
1855 auto technology = TechnologySystem::step(*definitions_);
1856 if (!technology) return Result<std::size_t>::failure(technology.status());
1857 processed += std::move(technology).takeValue();
1858 }
1859
1860 auto effects = EffectSystem::step(simulationStep, healthSettlement, lifecycleEvents);
1861 if (!effects) return Result<std::size_t>::failure(effects.status());
1862 processed += std::move(effects).takeValue();
1863 for (const auto& handle : matches_) {
1864 auto* match = tryGetCurrent<Match>(handle);
1865 if (match == nullptr) continue;
1866 auto outcome = MatchSystem::step(*match, matchResourceQuery_);
1867 if (!outcome) return Result<std::size_t>::failure(outcome.status());
1868 processed += std::move(outcome).takeValue();
1869 }
1870 auto cleanup = cleanupDestroyed();
1871 if (!cleanup) return Result<std::size_t>::failure(cleanup.status());
1872 processed += std::move(cleanup).takeValue();
1874}
1875
1876Result<std::size_t> RTS::stepScript(double seconds) {
1877 auto duration = Duration::fromSeconds(seconds);
1878 if (!duration || seconds <= 0.0) {
1879 if (!duration) return Result<std::size_t>::failure(duration.status());
1881 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS script step duration must be positive", "seconds"));
1882 }
1883 if (!scriptRuntime_) scriptRuntime_ = std::make_unique<ScriptRuntime>();
1884 if (scriptRuntime_->nextTick == std::numeric_limits<std::uint64_t>::max())
1886 Diagnostic::error(DiagnosticCode::Conflict, "RTS script simulation tick overflow", "tick"));
1887 const SimulationTick tick{scriptRuntime_->nextTick++};
1888 frameDamageEvents_.clear();
1889 frameCombatEvents_.clear();
1890 frameLifecycleEvents_.clear();
1891 frameEventSequence_ = 0;
1892 preserveFrameEventsOnNextStep_ = true;
1893 auto commands = scriptRuntime_->commandLog.apply(tick, *this);
1894 if (!commands) {
1895 preserveFrameEventsOnNextStep_ = false;
1896 return Result<std::size_t>::failure(commands.status());
1897 }
1898 const SimulationStep simulationStep{tick, std::move(duration).takeValue()};
1899 auto stepped = step(simulationStep, scriptRuntime_->adapter);
1900 if (!stepped) return stepped;
1901 std::erase_if(scriptRuntime_->paidProduction, [this](const auto& record) {
1902 auto* producer = findBuilding(record.producer);
1903 if (producer == nullptr) return true;
1904 auto task = producer->production()->values.find(record.taskId);
1905 if (!task || task->get().state == production::TaskState::Cancelled ||
1906 task->get().state == production::TaskState::Failed)
1907 return true;
1908 if (task->get().state != production::TaskState::Completed) return false;
1909 // A completed unit task can still be waiting for a free production exit.
1910 const auto& settled = producer->rally()->settledProductionTasks;
1911 return record.kind != "unit" || std::find(settled.begin(), settled.end(), record.taskId) != settled.end();
1912 });
1914}
1915
1916Result<void> RTS::configureScriptWorld(int width, int height, float cellSize, float originX, float originY) {
1917 if (width <= 0 || height <= 0 || !std::isfinite(cellSize) || cellSize <= 0.0f || !std::isfinite(originX) ||
1918 !std::isfinite(originY))
1919 return Result<void>::failure(
1921 "RTS script world requires positive dimensions and finite grid geometry", "grid"));
1922 if (!scriptRuntime_) scriptRuntime_ = std::make_unique<ScriptRuntime>();
1923 scriptRuntime_->pathfinder.setSize(width, height);
1924 scriptRuntime_->crowd.resizeField(width, height, cellSize, originX, originY);
1925 scriptRuntime_->crowd.setClampToField(false);
1926 // RTS uses navigation-cell world units; legacy flocking defaults use pixel-scale distances.
1927 // Predictive avoidance and contact resolution supply clearance without long-range repulsion.
1928 scriptRuntime_->crowd.setArriveRadius(cellSize);
1929 scriptRuntime_->crowd.setSeparationWeight(0.0f);
1930 crowd::AvoidanceSettings avoidance;
1931 avoidance.enabled = true;
1932 auto configuredAvoidance = scriptRuntime_->crowd.configureAvoidance(avoidance);
1933 if (!configuredAvoidance) return configuredAvoidance;
1934 scriptRuntime_->width = width;
1935 scriptRuntime_->height = height;
1936 scriptRuntime_->cellSize = cellSize;
1937 scriptRuntime_->originX = originX;
1938 scriptRuntime_->originY = originY;
1939 scriptRuntime_->terrainElevations.assign(static_cast<std::size_t>(width * height), 0.0f);
1940 scriptRuntime_->commandLog.clear();
1941 scriptRuntime_->nextTick = 1;
1942 scriptRuntime_->aiProductionSequence = 1;
1943 scriptRuntime_->configured = true;
1944 setDefinitionRegistry(&scriptRuntime_->definitions);
1945 setNavigationProvider(&scriptRuntime_->pathfinder, {cellSize, originX, originY});
1946 setCrowdProvider(&scriptRuntime_->crowd);
1947 setCombatProviders(&scriptRuntime_->sensing, &scriptRuntime_->damage);
1948 const auto firstBlockedPoint = [this](WorldPosition from, WorldPosition to) -> std::optional<WorldPosition> {
1949 if (!scriptRuntime_ || !scriptRuntime_->configured) return std::nullopt;
1950 const float dx = to.x - from.x;
1951 const float dy = to.y - from.y;
1952 const float distance = std::hypot(dx, dy);
1953 const float sampleLength = std::max(scriptRuntime_->cellSize * 0.25f, 0.001f);
1954 const int samples = std::max(1, static_cast<int>(std::ceil(distance / sampleLength)));
1955 int previousX = static_cast<int>(std::floor((from.x - scriptRuntime_->originX) / scriptRuntime_->cellSize));
1956 int previousY = static_cast<int>(std::floor((from.y - scriptRuntime_->originY) / scriptRuntime_->cellSize));
1957 for (int sample = 1; sample <= samples; ++sample) {
1958 const float t = static_cast<float>(sample) / static_cast<float>(samples);
1959 const WorldPosition point{from.x + dx * t, from.y + dy * t};
1960 const int cellX =
1961 static_cast<int>(std::floor((point.x - scriptRuntime_->originX) / scriptRuntime_->cellSize));
1962 const int cellY =
1963 static_cast<int>(std::floor((point.y - scriptRuntime_->originY) / scriptRuntime_->cellSize));
1964 if (cellX == previousX && cellY == previousY) continue;
1965 previousX = cellX;
1966 previousY = cellY;
1967 if (cellX < 0 || cellY < 0 || cellX >= scriptRuntime_->width || cellY >= scriptRuntime_->height ||
1968 !scriptRuntime_->pathfinder.isWalkable(cellX, cellY))
1969 return point;
1970 }
1971 return std::nullopt;
1972 };
1973 const auto terrainAt = [this](WorldPosition point) {
1974 if (!scriptRuntime_ || scriptRuntime_->terrainElevations.empty()) return 0.0f;
1975 const int x = static_cast<int>(std::floor((point.x - scriptRuntime_->originX) / scriptRuntime_->cellSize));
1976 const int y = static_cast<int>(std::floor((point.y - scriptRuntime_->originY) / scriptRuntime_->cellSize));
1977 if (x < 0 || y < 0 || x >= scriptRuntime_->width || y >= scriptRuntime_->height) return 0.0f;
1978 return scriptRuntime_->terrainElevations[static_cast<std::size_t>(y * scriptRuntime_->width + x)];
1979 };
1980 const auto relativeHeight = [](ecs::EntityHandle handle, bool firing) {
1981 if (auto* unit = tryGetCurrent<Unit>(handle))
1982 return firing ? unit->combat()->firingHeight : unit->combat()->targetHeight;
1983 if (auto* building = tryGetCurrent<Building>(handle))
1984 return firing ? building->combat()->firingHeight : building->combat()->targetHeight;
1985 return firing ? 1.0f : 0.0f;
1986 };
1987 const auto terrainBlocks = [this, terrainAt](WorldPosition from, WorldPosition to, float fromHeight,
1988 float toHeight) {
1989 const float distance = std::hypot(to.x - from.x, to.y - from.y);
1990 const float sampleLength = std::max(scriptRuntime_->cellSize * 0.25f, 0.001f);
1991 const int samples = std::max(1, static_cast<int>(std::ceil(distance / sampleLength)));
1992 for (int sample = 1; sample < samples; ++sample) {
1993 const float t = static_cast<float>(sample) / static_cast<float>(samples);
1994 const WorldPosition point{from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t};
1995 if (terrainAt(point) >= fromHeight + (toHeight - fromHeight) * t) return std::optional{point};
1996 }
1997 return std::optional<WorldPosition>{};
1998 };
1999 setFireLineQuery([firstBlockedPoint, terrainAt, relativeHeight, terrainBlocks](
2000 WorldPosition from, WorldPosition to, ecs::EntityHandle source, ecs::EntityHandle target,
2002 const float sourceHeight = terrainAt(from) + relativeHeight(source, true);
2003 const float targetHeight = terrainAt(to) + relativeHeight(target, false);
2004 const bool blocked =
2005 firstBlockedPoint(from, to).has_value() || terrainBlocks(from, to, sourceHeight, targetHeight).has_value();
2007 });
2008 setCombatHeightQuery([terrainAt, relativeHeight](WorldPosition from, WorldPosition to, ecs::EntityHandle source,
2009 ecs::EntityHandle target) {
2010 return CombatHeightProfile{terrainAt(from) + relativeHeight(source, true),
2011 terrainAt(to) + relativeHeight(target, false)};
2012 });
2013 setProjectileCollisionQuery([firstBlockedPoint, terrainBlocks](
2014 WorldPosition from, float fromHeight, WorldPosition to, float toHeight, SubjectRef,
2015 ecs::EntityHandle) -> Result<std::optional<ProjectileCollision>> {
2016 const auto blocked = firstBlockedPoint(from, to);
2017 const auto terrainImpact = terrainBlocks(from, to, fromHeight, toHeight);
2018 if (!blocked && !terrainImpact)
2020 const WorldPosition impact = blocked ? *blocked : *terrainImpact;
2023 });
2024 setFogProvider([this](Faction& faction) -> map::Fov* {
2025 if (!scriptRuntime_) return nullptr;
2026 const auto found = scriptRuntime_->fovs.find(faction.identity()->subject.format());
2027 return found == scriptRuntime_->fovs.end() ? nullptr : found->second.get();
2028 });
2029 setProductionSpawn([this](Building& producer, const production::ProductionTask& task,
2031 if (!scriptRuntime_)
2033 Diagnostic::error(DiagnosticCode::Conflict, "RTS script runtime is unavailable", "production"));
2034 const auto pending = scriptRuntime_->pendingProductionSubjects.find(task.id);
2035 if (pending == scriptRuntime_->pendingProductionSubjects.end())
2037 DiagnosticCode::NotFound, "RTS produced unit has no reserved stable subject", "production.task"));
2038 const auto definition = LogicalId::parse("unit:" + task.product);
2039 if (!definition)
2042 "RTS produced unit has an invalid logical definition", "production.product"));
2043 auto* faction = dynamic_cast<Faction*>(producer.faction()->link.resolve());
2044 if (faction == nullptr)
2046 DiagnosticCode::StaleHandle, "RTS producer faction link is stale", "production.faction"));
2047 scriptRuntime_->spawningProduction = true;
2048 auto created = newFactionUnit(*faction, pending->second, *definition);
2049 scriptRuntime_->spawningProduction = false;
2051 Unit* unit = std::move(created).takeValue();
2052 auto placed = placeProducedUnit(*unit, requested);
2053 if (!placed || placed.value().state == ProductionSpawnState::Blocked) {
2054 removeUnitRoot(*unit, UnitRemovalReason::SpawnRollback);
2055 return placed;
2056 }
2057 scriptRuntime_->pendingProductionSubjects.erase(pending);
2058 return placed;
2059 });
2060 setAIProductionRequest([this](Faction& faction, Building& producer, const LogicalId& definition) -> Result<void> {
2061 if (!scriptRuntime_ || !scriptRuntime_->configured)
2063 DiagnosticCode::Conflict, "RTS script AI production requires a configured world", "scriptRuntime"));
2064 if (scriptRuntime_->aiProductionSequence == std::numeric_limits<std::uint64_t>::max())
2066 DiagnosticCode::Conflict, "RTS script AI production identity sequence overflow", "ai.sequence"));
2067 SubjectRef generated;
2068 do {
2069 generated = deterministicSubject(faction.identity()->subject.format() + ":" +
2070 producer.identity()->subject.format() + ":" + definition.format(),
2071 scriptRuntime_->aiProductionSequence++);
2072 } while (ownsSubject(generated, SubjectClaimScope::LiveOrReserved) &&
2073 scriptRuntime_->aiProductionSequence != std::numeric_limits<std::uint64_t>::max());
2074 if (ownsSubject(generated, SubjectClaimScope::LiveOrReserved))
2075 return Result<void>::failure(
2077 "RTS script AI could not allocate a stable production subject", "ai.sequence"));
2078 auto queued = queueScriptUnit(producer, generated, definition);
2079 if (!queued) return Result<void>::failure(queued.status());
2081 });
2082
2083 for (const auto& handle : factions_) {
2084 auto* faction = tryGetCurrent<Faction>(handle);
2085 if (faction == nullptr) continue;
2086 const std::string key = faction->identity()->subject.format();
2087 if (!scriptRuntime_->economies.contains(key))
2088 scriptRuntime_->economies.emplace(key, std::make_unique<ScriptRuntime::EconomySlot>());
2089 auto& fov = scriptRuntime_->fovs[key];
2090 if (!fov)
2091 fov = std::make_unique<map::Fov>(width, height);
2092 else
2093 fov->setSize(width, height);
2094 fov->setMode("heightmap");
2095 fov->setEyeOffset(1.0f);
2096 fov->setCliffBlock(0.0f);
2097 auto link = EconomyLink::bind("rts/script/economy/" + key);
2098 if (!link) return Result<void>::failure(link.status());
2099 faction->economy()->link = std::move(link).takeValue();
2100 }
2101 for (const auto& handle : units_) {
2102 auto* unit = tryGetCurrent<Unit>(handle);
2103 if (unit == nullptr) continue;
2104 const std::string key = unit->identity()->subject.format();
2105 auto crowdLink = CrowdLink::bind(key);
2106 auto sensingLink = SensingLink::bind(key);
2107 if (!crowdLink) return Result<void>::failure(crowdLink.status());
2108 if (!sensingLink) return Result<void>::failure(sensingLink.status());
2109 unit->crowd()->link = std::move(crowdLink).takeValue();
2110 unit->sensing()->link = std::move(sensingLink).takeValue();
2111 }
2112
2113 const auto accountForFaction = [this](Faction* faction) -> resource::IResourceAccount* {
2114 if (faction == nullptr || !scriptRuntime_) return nullptr;
2115 const auto found = scriptRuntime_->economies.find(faction->identity()->subject.format());
2116 return found == scriptRuntime_->economies.end() ? nullptr : &found->second->account;
2117 };
2118 setResourceCredit([accountForFaction](Unit& unit, const resource::CostSpec& cost) -> Result<resource::Receipt> {
2119 auto* account = accountForFaction(dynamic_cast<Faction*>(unit.faction()->link.resolve()));
2120 if (account == nullptr)
2122 DiagnosticCode::NotFound, "RTS script faction economy was not found", "unit.faction"));
2123 return account->credit(cost);
2124 });
2125 setRepairDebit(
2126 [accountForFaction](Unit& unit, Building&, const resource::CostSpec& cost) -> Result<resource::Receipt> {
2127 auto* account = accountForFaction(dynamic_cast<Faction*>(unit.faction()->link.resolve()));
2128 if (account == nullptr)
2130 DiagnosticCode::NotFound, "RTS script faction economy was not found", "unit.faction"));
2131 return account->debit(cost);
2132 });
2133 setPassiveIncomeCredit(
2134 [accountForFaction](Building& building, const resource::CostSpec& cost) -> Result<resource::Receipt> {
2135 auto* account = accountForFaction(dynamic_cast<Faction*>(building.faction()->link.resolve()));
2136 if (account == nullptr)
2138 DiagnosticCode::NotFound, "RTS script faction economy was not found", "building.faction"));
2139 return account->credit(cost);
2140 });
2141 setMatchResourceQuery([this](Faction& faction, std::string_view resource) -> Result<double> {
2142 auto balance = scriptResource(faction, resource);
2143 if (!balance) return Result<double>::failure(balance.status());
2144 return Result<double>::success(static_cast<double>(balance.value()), Status::success(StatusCode::Applied));
2145 });
2147}
2148
2149Result<void> RTS::setScriptNavigationBlocked(int x, int y, bool blocked) {
2150 if (!scriptRuntime_ || !scriptRuntime_->configured)
2151 return Result<void>::failure(
2152 Diagnostic::error(DiagnosticCode::Conflict, "RTS script world is not configured", "grid"));
2153 if (x < 0 || y < 0 || x >= scriptRuntime_->width || y >= scriptRuntime_->height)
2155 "RTS script navigation cell is outside the grid", "cell"));
2156 scriptRuntime_->pathfinder.setBlocked(x, y, blocked);
2157 scriptRuntime_->crowd.setBlocked(x, y, blocked);
2158 for (auto& [key, fov] : scriptRuntime_->fovs) {
2159 (void)key;
2160 if (fov) fov->setOpaque(x, y, blocked);
2161 }
2163}
2164
2165Result<void> RTS::rebindScriptRootProviders() {
2166 if (!scriptRuntime_ || !scriptRuntime_->configured)
2167 return Result<void>::failure(
2168 Diagnostic::error(DiagnosticCode::Conflict, "RTS script world is not configured", "scriptWorld"));
2169 for (const auto& handle : factions_) {
2170 auto* faction = tryGetCurrent<Faction>(handle);
2171 if (faction == nullptr) continue;
2172 const std::string key = faction->identity()->subject.format();
2173 if (!scriptRuntime_->economies.contains(key))
2174 scriptRuntime_->economies.emplace(key, std::make_unique<ScriptRuntime::EconomySlot>());
2175 if (!scriptRuntime_->fovs.contains(key) || !scriptRuntime_->fovs.at(key))
2176 scriptRuntime_->fovs[key] = std::make_unique<map::Fov>(scriptRuntime_->width, scriptRuntime_->height);
2177 scriptRuntime_->fovs[key]->setMode("heightmap");
2178 scriptRuntime_->fovs[key]->setEyeOffset(1.0f);
2179 scriptRuntime_->fovs[key]->setCliffBlock(0.0f);
2180 auto economy = EconomyLink::bind("rts/script/economy/" + key);
2181 if (!economy) return Result<void>::failure(economy.status());
2182 faction->economy()->link = std::move(economy).takeValue();
2183 }
2184 for (const auto& handle : units_) {
2185 auto* unit = tryGetCurrent<Unit>(handle);
2186 if (unit == nullptr) continue;
2187 const std::string key = unit->identity()->subject.format();
2188 auto crowdLink = CrowdLink::bind(key);
2189 auto sensingLink = SensingLink::bind(key);
2190 if (!crowdLink) return Result<void>::failure(crowdLink.status());
2191 if (!sensingLink) return Result<void>::failure(sensingLink.status());
2192 unit->crowd()->link = std::move(crowdLink).takeValue();
2193 unit->sensing()->link = std::move(sensingLink).takeValue();
2194 }
2196}
2197
2198Result<void> RTS::setScriptNavigationCost(int x, int y, float cost) {
2199 if (!scriptRuntime_ || !scriptRuntime_->configured)
2200 return Result<void>::failure(
2201 Diagnostic::error(DiagnosticCode::Conflict, "RTS script world is not configured", "grid"));
2202 if (x < 0 || y < 0 || x >= scriptRuntime_->width || y >= scriptRuntime_->height || !std::isfinite(cost) ||
2203 cost <= 0.0f)
2204 return Result<void>::failure(
2206 "RTS script navigation cost requires an in-grid cell and positive finite cost", "cell"));
2207 scriptRuntime_->pathfinder.setCellCost(x, y, cost);
2208 scriptRuntime_->crowd.setCellCost(x, y, cost);
2210}
2211
2212Result<void> RTS::setScriptTerrainElevation(int x, int y, float elevation) {
2213 if (!scriptRuntime_ || !scriptRuntime_->configured)
2214 return Result<void>::failure(
2215 Diagnostic::error(DiagnosticCode::Conflict, "RTS script world is not configured", "terrain"));
2216 if (x < 0 || y < 0 || x >= scriptRuntime_->width || y >= scriptRuntime_->height || !std::isfinite(elevation))
2217 return Result<void>::failure(
2219 "RTS terrain elevation requires an in-grid cell and finite value", "terrain.cell"));
2220 scriptRuntime_->terrainElevations[static_cast<std::size_t>(y * scriptRuntime_->width + x)] = elevation;
2221 for (auto& [faction, fov] : scriptRuntime_->fovs) {
2222 (void)faction;
2223 if (fov) fov->setElevation(x, y, elevation);
2224 }
2226}
2227
2228Result<float> RTS::scriptTerrainElevation(int x, int y) const {
2229 if (!scriptRuntime_ || !scriptRuntime_->configured)
2231 Diagnostic::error(DiagnosticCode::Conflict, "RTS script world is not configured", "terrain"));
2232 if (x < 0 || y < 0 || x >= scriptRuntime_->width || y >= scriptRuntime_->height)
2234 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS terrain cell is outside the grid", "terrain.cell"));
2236 scriptRuntime_->terrainElevations[static_cast<std::size_t>(y * scriptRuntime_->width + x)],
2238}
2239
2240Result<void> RTS::addScriptResource(Faction& faction, std::string resource, std::int64_t amount) {
2241 if (!owns(factions_, faction))
2242 return Result<void>::failure(
2243 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS Faction does not belong to this facade", "faction"));
2244 if (!scriptRuntime_ || !scriptRuntime_->configured)
2245 return Result<void>::failure(
2246 Diagnostic::error(DiagnosticCode::Conflict, "RTS script world is not configured", "economy"));
2247 const auto found = scriptRuntime_->economies.find(faction.identity()->subject.format());
2248 if (found == scriptRuntime_->economies.end())
2249 return Result<void>::failure(
2250 Diagnostic::error(DiagnosticCode::NotFound, "RTS script faction economy was not found", "faction"));
2251 auto cost = resource::CostSpec::single(std::move(resource), amount);
2252 if (!cost) return Result<void>::failure(cost.status());
2253 auto credited = found->second->account.credit(std::move(cost).takeValue());
2254 if (!credited) return Result<void>::failure(credited.status());
2255 std::move(credited).takeValue();
2257}
2258
2259Result<std::int64_t> RTS::scriptResource(Faction& faction, std::string_view resource) const {
2260 if (!owns(factions_, faction))
2262 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS Faction does not belong to this facade", "faction"));
2263 if (!scriptRuntime_ || !scriptRuntime_->configured || resource.empty())
2266 "RTS script resource query requires a configured world and resource", "resource"));
2267 const auto found = scriptRuntime_->economies.find(faction.identity()->subject.format());
2268 if (found == scriptRuntime_->economies.end())
2270 Diagnostic::error(DiagnosticCode::NotFound, "RTS script faction economy was not found", "faction"));
2271 return Result<std::int64_t>::success(found->second->ledger.get(std::string(resource)));
2272}
2273
2274Result<void> RTS::configureScriptAI(Faction& faction, LogicalId workerDefinition, LogicalId armyDefinition,
2275 LogicalId targetBuildingDefinition, int desiredWorkers, int attackThreshold,
2276 float thinkInterval, float formationSpacing, bool enabled) {
2277 if (!owns(factions_, faction))
2278 return Result<void>::failure(
2279 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS AI faction does not belong to this facade", "faction"));
2280 if (!scriptRuntime_ || !scriptRuntime_->configured || definitions_ == nullptr)
2282 DiagnosticCode::Conflict, "RTS AI requires a configured script world and content", "scriptRuntime"));
2283 if (!workerDefinition.isValid() || !armyDefinition.isValid() || !targetBuildingDefinition.isValid() ||
2284 desiredWorkers < 0 || attackThreshold <= 0 || !std::isfinite(thinkInterval) || thinkInterval <= 0.0f ||
2285 !std::isfinite(formationSpacing) || formationSpacing <= 0.0f ||
2286 !definitions_->resolve("unit", std::string(workerDefinition.name())) ||
2287 !definitions_->resolve("unit", std::string(armyDefinition.name())) ||
2288 !definitions_->resolve("building", std::string(targetBuildingDefinition.name())))
2291 "RTS AI policy requires known definitions and positive finite policy values", "strategy"));
2292 auto strategy = faction.strategy();
2293 strategy->workerDefinition = std::move(workerDefinition);
2294 strategy->armyDefinition = std::move(armyDefinition);
2295 strategy->targetBuildingDefinition = std::move(targetBuildingDefinition);
2296 strategy->desiredWorkers = desiredWorkers;
2297 strategy->attackThreshold = attackThreshold;
2298 strategy->thinkInterval = thinkInterval;
2299 strategy->thinkAccumulator = 0.0f;
2300 strategy->formationSpacing = formationSpacing;
2301 strategy->enabled = enabled;
2303}
2304
2305Result<ContentImportReceipt> RTS::loadScriptContent(std::string_view json) {
2306 if (!scriptRuntime_) scriptRuntime_ = std::make_unique<ScriptRuntime>();
2307 setDefinitionRegistry(&scriptRuntime_->definitions);
2308 return loadContent(scriptRuntime_->definitions, json);
2309}
2310
2311Result<RTSBuildReceipt> RTS::queueScriptUnit(Building& producer, SubjectRef unitSubject, LogicalId unitDefinition,
2312 int priority) {
2313 if (!owns(buildings_, producer))
2315 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS producer does not belong to this facade", "producer"));
2316 if (!scriptRuntime_ || !scriptRuntime_->configured || definitions_ == nullptr)
2318 DiagnosticCode::Conflict, "RTS script world and content must be configured", "scriptRuntime"));
2319 if (!unitSubject.isValid() || ownsSubject(unitSubject, SubjectClaimScope::LiveOrReserved))
2321 DiagnosticCode::Conflict, "RTS produced unit subject is invalid or already owned", "unitSubject"));
2322 if (!unitDefinition.isValid())
2324 DiagnosticCode::InvalidArgument, "RTS production requires a unit definition", "unitDefinition"));
2325 auto resolved = definitions_->resolve("unit", std::string(unitDefinition.name()));
2326 if (!resolved) return Result<RTSBuildReceipt>::failure(resolved.status());
2327 auto parsed = Value::fromJson(resolved.value().get().json);
2328 if (!parsed) return Result<RTSBuildReceipt>::failure(parsed.status());
2329 const auto* object = parsed.value().getIf<Value::Object>();
2330 if (object == nullptr)
2332 DiagnosticCode::InvalidArgument, "RTS unit definition must be an object", "unitDefinition"));
2333 const auto resourceIt = object->find("costResource");
2334 const auto costIt = object->find("cost");
2335 const auto timeIt = object->find("buildTime");
2336 const auto producerIt = object->find("producer");
2337 if (resourceIt == object->end() || costIt == object->end() || timeIt == object->end())
2339 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS unit definition lacks production cost or duration",
2340 "unitDefinition"));
2341 const auto* resourceName = resourceIt->second.getIf<std::string>();
2342 const auto number = [](const Value& value) -> std::optional<double> {
2343 if (const auto* integer = value.getIf<std::int64_t>()) return static_cast<double>(*integer);
2344 if (const auto* real = value.getIf<double>()) return *real;
2345 return std::nullopt;
2346 };
2347 const auto costNumber = number(costIt->second);
2348 const auto timeNumber = number(timeIt->second);
2349 if (resourceName == nullptr || resourceName->empty() || !costNumber || !timeNumber || !std::isfinite(*costNumber) ||
2350 *costNumber < 0.0 || std::floor(*costNumber) != *costNumber || !std::isfinite(*timeNumber) ||
2351 *timeNumber <= 0.0)
2353 DiagnosticCode::InvalidArgument, "RTS unit production values are invalid", "unitDefinition"));
2354 if (producerIt != object->end()) {
2355 const auto* requiredProducer = producerIt->second.getIf<std::string>();
2356 if (requiredProducer == nullptr || *requiredProducer != producer.definition()->id.name())
2358 DiagnosticCode::Conflict, "RTS building cannot produce this unit definition", "producer"));
2359 }
2360 auto* faction = dynamic_cast<Faction*>(producer.faction()->link.resolve());
2361 if (faction == nullptr)
2363 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS producer faction link is stale", "producer.faction"));
2364 const auto economy = scriptRuntime_->economies.find(faction->identity()->subject.format());
2365 if (economy == scriptRuntime_->economies.end())
2367 Diagnostic::error(DiagnosticCode::NotFound, "RTS producer economy was not found", "producer.faction"));
2368 auto cost = resource::CostSpec::single(*resourceName, static_cast<std::int64_t>(*costNumber));
2369 if (!cost) return Result<RTSBuildReceipt>::failure(cost.status());
2370 const resource::CostSpec paidCost = cost.value();
2371 auto duration = Duration::fromSeconds(*timeNumber);
2372 if (!duration) return Result<RTSBuildReceipt>::failure(duration.status());
2373 auto receipt = build(producer, scriptRuntime_->actions, economy->second->account, std::move(cost).takeValue(),
2374 std::string(unitDefinition.name()), std::move(duration).takeValue(), "unit", priority,
2375 "rts.script.production." + unitSubject.format());
2376 if (!receipt) return receipt;
2377 scriptRuntime_->pendingProductionSubjects.emplace(receipt.value().productionTaskId, unitSubject);
2378 scriptRuntime_->paidProduction.push_back({producer.identity()->subject, unitSubject, "unit",
2379 std::string(unitDefinition.name()), receipt.value().productionTaskId,
2380 receipt.value().orderId, paidCost});
2381 return receipt;
2382}
2383
2384Result<ReinforcementRequestReceipt> RTS::queueScriptReinforcement(Building& producer, SubjectRef unitSubject,
2385 LogicalId preferredDefinition, int priority) {
2386 if (!owns(buildings_, producer))
2388 DiagnosticCode::StaleHandle, "RTS reinforcement producer does not belong to this facade", "producer"));
2389 if (!unitSubject.isValid() || ownsSubject(unitSubject, SubjectClaimScope::LiveOrReserved) ||
2390 !preferredDefinition.isValid())
2393 "RTS reinforcement requires a new stable subject and preferred unit definition", "reinforcement"));
2394 return ReinforcementProductionPolicySystem::request(
2395 producer, std::string(preferredDefinition.name()),
2396 [&](Building& selectedProducer, std::string_view candidate) -> Result<std::string> {
2397 auto definition = LogicalId::parse(candidate);
2398 if (!definition) definition = LogicalId::parse("unit:" + std::string(candidate));
2399 if (!definition)
2400 return Result<std::string>::failure(Diagnostic::error(
2401 DiagnosticCode::InvalidArgument, "RTS reinforcement fallback is not a valid unit definition",
2402 "reinforcement.fallback"));
2403 auto queued = queueScriptUnit(selectedProducer, unitSubject, *definition, priority);
2404 if (!queued) return Result<std::string>::failure(queued.status());
2405 return Result<std::string>::success(queued.value().productionTaskId, Status::success(StatusCode::Applied));
2406 });
2407}
2408
2409Result<Building*> RTS::startScriptConstruction(Faction& faction, SubjectRef buildingSubject,
2410 LogicalId buildingDefinition, WorldPosition position, Unit& builder) {
2411 if (!owns(factions_, faction) || !owns(units_, builder))
2414 "RTS construction faction or builder does not belong to this facade", "construction"));
2415 if (builder.faction()->link.resolve() != &faction || builder.worker()->buildRate <= 0.0f)
2417 DiagnosticCode::PreconditionViolation, "RTS construction requires a same-faction builder", "builder"));
2418 if (!scriptRuntime_ || !scriptRuntime_->configured || definitions_ == nullptr || !buildingSubject.isValid() ||
2419 ownsSubject(buildingSubject) || !buildingDefinition.isValid() || !std::isfinite(position.x) ||
2420 !std::isfinite(position.y))
2423 "RTS construction request is invalid or the world is not configured", "construction"));
2424 auto resolved = definitions_->resolve("building", std::string(buildingDefinition.name()));
2425 if (!resolved) return Result<Building*>::failure(resolved.status());
2426 auto parsed = Value::fromJson(resolved.value().get().json);
2427 if (!parsed) return Result<Building*>::failure(parsed.status());
2428 const auto* object = parsed.value().getIf<Value::Object>();
2429 if (object == nullptr)
2431 DiagnosticCode::InvalidArgument, "RTS building definition must be an object", "buildingDefinition"));
2432 const auto resourceIt = object->find("costResource");
2433 const auto costIt = object->find("cost");
2434 if (resourceIt == object->end() || costIt == object->end())
2436 "RTS building definition lacks a construction cost",
2437 "buildingDefinition"));
2438 const auto* resourceName = resourceIt->second.getIf<std::string>();
2439 double costNumber = -1.0;
2440 if (const auto* integer = costIt->second.getIf<std::int64_t>())
2441 costNumber = static_cast<double>(*integer);
2442 else if (const auto* real = costIt->second.getIf<double>())
2443 costNumber = *real;
2444 if (resourceName == nullptr || resourceName->empty() || !std::isfinite(costNumber) || costNumber < 0.0 ||
2445 std::floor(costNumber) != costNumber)
2447 DiagnosticCode::InvalidArgument, "RTS building construction cost is invalid", "buildingDefinition"));
2448 const auto economy = scriptRuntime_->economies.find(faction.identity()->subject.format());
2449 if (economy == scriptRuntime_->economies.end())
2451 Diagnostic::error(DiagnosticCode::NotFound, "RTS construction economy was not found", "faction"));
2452 auto cost = resource::CostSpec::single(*resourceName, static_cast<std::int64_t>(costNumber));
2453 if (!cost) return Result<Building*>::failure(cost.status());
2454 auto previousOrders = builder.orders()->values.snapshotState();
2455 if (!previousOrders) return Result<Building*>::failure(previousOrders.status());
2456 auto paid = economy->second->account.debit(cost.value());
2457 if (!paid) return Result<Building*>::failure(paid.status());
2458
2459 const std::size_t weaponCount = weapons_.size();
2460 auto created = newFactionBuilding(faction, buildingSubject, buildingDefinition);
2461 if (!created) {
2462 economy->second->account.credit(cost.value()).ignore("compensate failed construction root creation");
2463 return created;
2464 }
2465 Building* building = created.value();
2466 building->placement()->worldX = position.x;
2467 building->placement()->worldY = position.y;
2468 building->construction()->progress = 0.0f;
2469 building->construction()->builders = {ecs::handle_of(&builder)};
2470 CommandSpec move;
2471 move.kind = OrderKind::Move;
2472 move.target = position;
2473 auto ordered = builder.orders()->values.replace(move);
2474 if (ordered) {
2475 CommandSpec buildCommand;
2476 buildCommand.kind = OrderKind::Build;
2477 buildCommand.target = position;
2478 buildCommand.targetEntity = ecs::handle_of(building);
2479 ordered = builder.orders()->values.enqueue(buildCommand);
2480 }
2481 if (!ordered) {
2482 const Status status = ordered.status();
2483 builder.orders()->values.restoreState(previousOrders.value()).ignore("restore construction builder orders");
2484 faction.members()->buildings.pop_back();
2485 building->release();
2486 buildings_.pop_back();
2487 while (weapons_.size() > weaponCount) {
2488 if (auto* weapon = dynamic_cast<weapon::WeaponEntity*>(ecs::try_get(weapons_.back()))) weapon->release();
2489 weapons_.pop_back();
2490 }
2491 economy->second->account.credit(cost.value()).ignore("compensate failed construction order");
2493 }
2494 std::move(ordered).takeValue();
2495 scriptRuntime_->paidConstruction.push_back({buildingSubject, faction.identity()->subject, cost.value()});
2497}
2498
2499Result<resource::Receipt> RTS::cancelScriptConstruction(Building& building) {
2500 if (!owns(buildings_, building))
2502 DiagnosticCode::StaleHandle, "RTS construction does not belong to this facade", "building"));
2503 if (!scriptRuntime_ || !scriptRuntime_->configured || !building.integrity()->alive ||
2504 building.construction()->progress >= 1.0f)
2507 "RTS construction cancellation requires a live unfinished script building", "building.construction"));
2508 const auto payment =
2509 std::find_if(scriptRuntime_->paidConstruction.begin(), scriptRuntime_->paidConstruction.end(),
2510 [&](const auto& value) { return value.building == building.identity()->subject; });
2511 if (payment == scriptRuntime_->paidConstruction.end())
2513 DiagnosticCode::NotFound, "RTS construction payment record was not found", "building.construction"));
2514 auto* faction = findFaction(payment->faction);
2515 if (faction == nullptr)
2517 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS construction faction is stale", "building.faction"));
2518 const auto economy = scriptRuntime_->economies.find(payment->faction.format());
2519 if (economy == scriptRuntime_->economies.end())
2521 Diagnostic::error(DiagnosticCode::NotFound, "RTS construction economy was not found", "building.faction"));
2522 auto refund = scaledCost(payment->cost,
2523 1.0 - 0.5 * std::clamp(static_cast<double>(building.construction()->progress), 0.0, 1.0));
2524 if (!refund) return Result<resource::Receipt>::failure(refund.status());
2525 auto credited = economy->second->account.credit(refund.value());
2526 if (!credited) return Result<resource::Receipt>::failure(credited.status());
2527
2528 for (const auto& builderHandle : building.construction()->builders) {
2529 auto* builder = dynamic_cast<Unit*>(ecs::try_get(builderHandle));
2530 if (builder != nullptr) builder->orders()->values.clear();
2531 }
2532 building.construction()->builders.clear();
2533 building.integrity()->alive = false;
2534 building.integrity()->state.health = 0.0;
2535 building.placement()->placed = false;
2536 const auto buildingHandle = ecs::handle_of(&building);
2537 std::erase_if(faction->members()->buildings, [&](const auto& value) { return sameHandle(value, buildingHandle); });
2538 const auto weaponHandle = building.weapon()->link.handle();
2539 if (auto* weapon = dynamic_cast<weapon::WeaponEntity*>(building.weapon()->link.resolve())) weapon->release();
2540 std::erase_if(weapons_, [&](const auto& value) { return sameHandle(value, weaponHandle); });
2541 std::erase_if(buildings_, [&](const auto& value) { return sameHandle(value, buildingHandle); });
2542 scriptRuntime_->paidConstruction.erase(payment);
2543 building.release();
2544 return credited;
2545}
2546
2547Result<resource::Receipt> RTS::sellScriptBuilding(Building& building) {
2548 if (!owns(buildings_, building))
2550 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS building does not belong to this facade", "building"));
2551 if (!scriptRuntime_ || !scriptRuntime_->configured || definitions_ == nullptr || !building.integrity()->alive ||
2552 building.construction()->progress < 1.0f)
2555 "RTS sale requires a live completed script building", "building.construction"));
2556 auto* faction = dynamic_cast<Faction*>(building.faction()->link.resolve());
2557 if (faction == nullptr)
2559 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS building faction link is stale", "building.faction"));
2560 const auto economy = scriptRuntime_->economies.find(faction->identity()->subject.format());
2561 if (economy == scriptRuntime_->economies.end())
2563 Diagnostic::error(DiagnosticCode::NotFound, "RTS building economy was not found", "building.faction"));
2564 auto definition = definitions_->resolve("building", std::string(building.definition()->id.name()));
2565 if (!definition) return Result<resource::Receipt>::failure(definition.status());
2566 auto parsed = Value::fromJson(definition.value().get().json);
2567 if (!parsed) return Result<resource::Receipt>::failure(parsed.status());
2568 const auto* object = parsed.value().getIf<Value::Object>();
2569 if (object == nullptr)
2571 DiagnosticCode::InvalidArgument, "RTS building definition must be an object", "building.definition"));
2572 const auto resourceIt = object->find("costResource");
2573 const auto costIt = object->find("cost");
2574 if (resourceIt == object->end() || costIt == object->end() || resourceIt->second.getIf<std::string>() == nullptr)
2576 DiagnosticCode::InvalidArgument, "RTS building definition lacks a sale cost", "building.definition"));
2577 double costNumber = -1.0;
2578 if (const auto* integer = costIt->second.getIf<std::int64_t>())
2579 costNumber = static_cast<double>(*integer);
2580 else if (const auto* real = costIt->second.getIf<double>())
2581 costNumber = *real;
2582 double ratio = 0.5;
2583 if (const auto ratioIt = object->find("sellRefundRatio"); ratioIt != object->end()) {
2584 if (const auto* integer = ratioIt->second.getIf<std::int64_t>())
2585 ratio = static_cast<double>(*integer);
2586 else if (const auto* real = ratioIt->second.getIf<double>())
2587 ratio = *real;
2588 else
2589 ratio = -1.0;
2590 }
2591 if (!std::isfinite(costNumber) || costNumber <= 0.0 || std::floor(costNumber) != costNumber ||
2592 !std::isfinite(ratio) || ratio <= 0.0 || ratio > 1.0)
2594 DiagnosticCode::InvalidArgument, "RTS building sale values are invalid", "building.definition"));
2595 auto baseCost =
2596 resource::CostSpec::single(*resourceIt->second.getIf<std::string>(), static_cast<std::int64_t>(costNumber));
2597 if (!baseCost) return Result<resource::Receipt>::failure(baseCost.status());
2598 auto saleRefund = scaledCost(baseCost.value(), ratio);
2599 if (!saleRefund) return Result<resource::Receipt>::failure(saleRefund.status());
2600
2601 auto rootBefore = snapshotState();
2602 if (!rootBefore) return Result<resource::Receipt>::failure(rootBefore.status());
2603 const auto ledgerBefore = economy->second->ledger.snapshot();
2604 const auto pendingBefore = scriptRuntime_->pendingProductionSubjects;
2605 const auto paymentsBefore = scriptRuntime_->paidProduction;
2606 const SubjectRef producerSubject = building.identity()->subject;
2607 const auto rollback = [&]() {
2608 economy->second->ledger.restore(ledgerBefore);
2609 scriptRuntime_->pendingProductionSubjects = pendingBefore;
2610 scriptRuntime_->paidProduction = paymentsBefore;
2611 (void)restoreState(rootBefore.value());
2612 };
2613 for (const auto& record : paymentsBefore) {
2614 if (record.producer != producerSubject) continue;
2615 auto task = building.production()->values.find(record.taskId);
2616 if (!task || task->get().state == production::TaskState::Completed ||
2617 task->get().state == production::TaskState::Cancelled || task->get().state == production::TaskState::Failed)
2618 continue;
2619 auto cancelled = cancelProduction(building, economy->second->account, record.taskId, record.orderId,
2620 record.refund, "building sold");
2621 if (!cancelled) {
2622 rollback();
2623 return Result<resource::Receipt>::failure(cancelled.status());
2624 }
2625 scriptRuntime_->pendingProductionSubjects.erase(record.taskId);
2626 std::erase_if(scriptRuntime_->paidProduction, [&](const auto& value) { return value.taskId == record.taskId; });
2627 }
2628 auto credited = economy->second->account.credit(saleRefund.value());
2629 if (!credited) {
2630 rollback();
2631 return Result<resource::Receipt>::failure(credited.status());
2632 }
2633 auto evacuated = evacuateBuilding(building, {building.placement()->worldX + 2.0f, building.placement()->worldY});
2634 if (!evacuated) {
2635 rollback();
2636 return Result<resource::Receipt>::failure(evacuated.status());
2637 }
2638
2639 const auto buildingHandle = ecs::handle_of(&building);
2640 for (const auto& factionHandle : factions_) {
2641 auto* owner = dynamic_cast<Faction*>(ecs::try_get(factionHandle));
2642 if (owner != nullptr)
2643 std::erase_if(owner->members()->buildings,
2644 [&](const auto& value) { return sameHandle(value, buildingHandle); });
2645 }
2646 const auto weaponHandle = building.weapon()->link.handle();
2647 if (auto* weapon = dynamic_cast<weapon::WeaponEntity*>(building.weapon()->link.resolve())) weapon->release();
2648 std::erase_if(weapons_, [&](const auto& value) { return sameHandle(value, weaponHandle); });
2649 std::erase_if(buildings_, [&](const auto& value) { return sameHandle(value, buildingHandle); });
2650 std::erase_if(scriptRuntime_->paidConstruction,
2651 [&](const auto& value) { return value.building == producerSubject; });
2652 building.release();
2653 return credited;
2654}
2655
2656Result<RTSBuildReceipt> RTS::queueScriptResearch(Building& producer, std::string upgrade, int priority) {
2657 if (!owns(buildings_, producer))
2659 DiagnosticCode::StaleHandle, "RTS research producer does not belong to this facade", "producer"));
2660 if (!scriptRuntime_ || !scriptRuntime_->configured || definitions_ == nullptr || upgrade.empty())
2662 DiagnosticCode::InvalidArgument, "RTS research requires a configured world and upgrade", "upgrade"));
2663 auto* faction = dynamic_cast<Faction*>(producer.faction()->link.resolve());
2664 if (faction == nullptr)
2666 DiagnosticCode::StaleHandle, "RTS research producer faction link is stale", "producer.faction"));
2667 if (std::binary_search(faction->technology()->unlocked.begin(), faction->technology()->unlocked.end(), upgrade))
2669 Diagnostic::error(DiagnosticCode::Conflict, "RTS upgrade is already unlocked", "upgrade"));
2670 auto researchBuildings = ecs::View<Building, Building::Faction, Building::Production>();
2671 for (auto it = researchBuildings.begin(); it != researchBuildings.end(); ++it) {
2672 auto [candidateFaction, candidateProduction] = *it;
2673 if (candidateFaction->link.resolve() != faction) continue;
2674 for (int index = 0; index < static_cast<int>(candidateProduction->values.taskCount()); ++index) {
2675 auto task = candidateProduction->values.taskAt(index);
2676 if (task && task->get().kind == "research" && task->get().product == upgrade &&
2677 task->get().state != production::TaskState::Cancelled &&
2678 task->get().state != production::TaskState::Failed &&
2679 task->get().state != production::TaskState::Completed)
2681 DiagnosticCode::Conflict, "RTS upgrade is already queued by this faction", "upgrade"));
2682 }
2683 }
2684 auto resolved = definitions_->resolve("upgrade", upgrade);
2685 if (!resolved) return Result<RTSBuildReceipt>::failure(resolved.status());
2686 auto definitionHandle = definitions_->handle("upgrade", upgrade);
2687 if (!definitionHandle) return Result<RTSBuildReceipt>::failure(definitionHandle.status());
2688 auto parsed = Value::fromJson(resolved.value().get().json);
2689 if (!parsed) return Result<RTSBuildReceipt>::failure(parsed.status());
2690 const auto* object = parsed.value().getIf<Value::Object>();
2691 if (object == nullptr)
2693 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS upgrade definition must be an object", "upgrade"));
2694 const auto textField = [object](std::string_view name) -> std::string {
2695 const auto found = object->find(std::string(name));
2696 if (found == object->end()) return {};
2697 const auto* value = found->second.getIf<std::string>();
2698 return value == nullptr ? std::string{} : *value;
2699 };
2700 const auto numberField = [object](std::string_view name) -> std::optional<double> {
2701 const auto found = object->find(std::string(name));
2702 if (found == object->end()) return std::nullopt;
2703 if (const auto* integer = found->second.getIf<std::int64_t>()) return static_cast<double>(*integer);
2704 if (const auto* real = found->second.getIf<double>()) return *real;
2705 return std::nullopt;
2706 };
2707 const std::string requiredProducer = textField("producer");
2708 const std::string prerequisite = textField("prerequisiteUpgrade");
2709 const std::string resourceName = textField("costResource");
2710 const auto costNumber = numberField("cost");
2711 const auto researchTime = numberField("researchTime");
2712 if (requiredProducer != producer.definition()->id.name())
2714 Diagnostic::error(DiagnosticCode::Conflict, "RTS building cannot research this upgrade", "producer"));
2715 if (!prerequisite.empty() && !std::binary_search(faction->technology()->unlocked.begin(),
2716 faction->technology()->unlocked.end(), prerequisite))
2718 DiagnosticCode::PreconditionViolation, "RTS research prerequisite is not unlocked", "upgrade"));
2719 if (resourceName.empty() || !costNumber || !researchTime || !std::isfinite(*costNumber) || *costNumber < 0.0 ||
2720 std::floor(*costNumber) != *costNumber || !std::isfinite(*researchTime) || *researchTime <= 0.0)
2722 DiagnosticCode::InvalidArgument, "RTS upgrade cost or research duration is invalid", "upgrade"));
2723 const auto economy = scriptRuntime_->economies.find(faction->identity()->subject.format());
2724 if (economy == scriptRuntime_->economies.end())
2726 Diagnostic::error(DiagnosticCode::NotFound, "RTS research economy was not found", "producer.faction"));
2727 auto cost = resource::CostSpec::single(resourceName, static_cast<std::int64_t>(*costNumber));
2728 auto duration = Duration::fromSeconds(*researchTime);
2729 if (!cost) return Result<RTSBuildReceipt>::failure(cost.status());
2730 if (!duration) return Result<RTSBuildReceipt>::failure(duration.status());
2731 const resource::CostSpec paidCost = cost.value();
2732 const std::string product = upgrade;
2733 auto receipt = build(producer, scriptRuntime_->actions, economy->second->account, std::move(cost).takeValue(),
2734 std::move(upgrade), std::move(duration).takeValue(), "research", priority, {},
2735 std::move(definitionHandle).takeValue());
2736 if (receipt)
2737 scriptRuntime_->paidProduction.push_back({producer.identity()->subject,
2738 {},
2739 "research",
2740 product,
2741 receipt.value().productionTaskId,
2742 receipt.value().orderId,
2743 paidCost});
2744 return receipt;
2745}
2746
2747Result<RTSCancelProductionReceipt> RTS::cancelScriptProduction(Building& producer, int queueIndex) {
2748 if (!owns(buildings_, producer))
2750 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS producer does not belong to this facade", "producer"));
2751 if (!scriptRuntime_ || !scriptRuntime_->configured || queueIndex < -1)
2754 "RTS production cancellation requires a configured world and valid queue index", "queueIndex"));
2755 std::vector<std::string> active;
2756 for (int index = 0; index < static_cast<int>(producer.production()->values.taskCount()); ++index) {
2757 auto task = producer.production()->values.taskAt(index);
2758 if (task && task->get().state != production::TaskState::Completed &&
2759 task->get().state != production::TaskState::Cancelled && task->get().state != production::TaskState::Failed)
2760 active.push_back(task->get().id);
2761 }
2762 if (active.empty())
2764 Diagnostic::error(DiagnosticCode::NotFound, "RTS producer has no cancellable production", "production"));
2765 if (queueIndex < 0) queueIndex = static_cast<int>(active.size()) - 1;
2766 if (queueIndex >= static_cast<int>(active.size()))
2768 Diagnostic::error(DiagnosticCode::NotFound, "RTS production queue index was not found", "queueIndex"));
2769 const auto record = std::find_if(scriptRuntime_->paidProduction.begin(), scriptRuntime_->paidProduction.end(),
2770 [&](const auto& value) {
2771 return value.taskId == active[static_cast<std::size_t>(queueIndex)] &&
2772 value.producer == producer.identity()->subject;
2773 });
2774 if (record == scriptRuntime_->paidProduction.end())
2776 DiagnosticCode::NotFound, "RTS production payment record was not found", "production.task"));
2777 auto* faction = dynamic_cast<Faction*>(producer.faction()->link.resolve());
2778 if (faction == nullptr)
2780 Diagnostic::error(DiagnosticCode::StaleHandle, "RTS producer faction link is stale", "producer.faction"));
2781 const auto economy = scriptRuntime_->economies.find(faction->identity()->subject.format());
2782 if (economy == scriptRuntime_->economies.end())
2784 Diagnostic::error(DiagnosticCode::NotFound, "RTS producer economy was not found", "producer.faction"));
2785 const std::string taskId = record->taskId;
2786 auto cancelled = cancelProduction(producer, economy->second->account, record->taskId, record->orderId,
2787 record->refund, "script production cancelled");
2788 if (!cancelled) return cancelled;
2789 scriptRuntime_->pendingProductionSubjects.erase(taskId);
2790 scriptRuntime_->paidProduction.erase(record);
2791 return cancelled;
2792}
2793
2794Result<void> RTS::setBuildingRally(Building& producer, CommandSpec command, bool groupedReinforcements) const {
2795 if (!owns(buildings_, producer))
2797 DiagnosticCode::StaleHandle, "RTS rally producer does not belong to this facade", "producer"));
2798 auto valid = command.validate();
2799 if (!valid) return valid;
2800 if (command.kind != OrderKind::Move && command.kind != OrderKind::AttackMove)
2802 "RTS rally command must be Move or AttackMove", "command.kind"));
2803 auto& rally = *producer.rally();
2804 rally = {};
2805 rally.enabled = true;
2806 rally.command = std::move(command);
2807 rally.combatGroup = groupedReinforcements ? stableRallyGroup(producer.identity()->subject) : 0;
2809}
2810
2811Result<void> RTS::linkBuildingRally(Building& producer, Building& source) const {
2812 if (!owns(buildings_, producer) || !owns(buildings_, source) || &producer == &source)
2814 DiagnosticCode::StaleHandle, "RTS rally link requires two distinct owned producers", "producer"));
2815 if (producer.faction()->link.resolve() != source.faction()->link.resolve() || !source.rally()->enabled ||
2816 source.rally()->combatGroup == 0)
2818 "RTS rally source must be a grouped friendly rally",
2819 "source.rally"));
2820 auto linked = *source.rally();
2821 linked.transport = {};
2822 linked.minimumTransportLoad = 1;
2823 linked.transportActive = false;
2824 linked.productionSpawnBlocked = false;
2825 linked.blockedProductionTask.clear();
2826 linked.settledProductionTasks.clear();
2827 linked.reinforcements.clear();
2828 linked.reinforcementCapped = false;
2829 linked.reinforcementPolicyPausedTask.clear();
2830 linked.reinforcementCappedSeconds = 0.0f;
2831 *producer.rally() = std::move(linked);
2833}
2834
2835Result<void> RTS::clearBuildingRally(Building& producer) const {
2836 if (!owns(buildings_, producer))
2838 DiagnosticCode::StaleHandle, "RTS rally producer does not belong to this facade", "producer"));
2839 *producer.rally() = {};
2841}
2842
2843Result<void> RTS::setReinforcementLimit(Building& producer, std::size_t maximum) const {
2844 if (!owns(buildings_, producer) || !producer.rally()->enabled || producer.rally()->combatGroup == 0)
2846 "RTS reinforcement limit requires an owned grouped rally",
2847 "producer.rally"));
2848 const auto group = producer.rally()->combatGroup;
2849 auto* faction = producer.faction()->link.resolve();
2850 for (const auto& handle : buildings_) {
2851 auto* building = tryGetCurrent<Building>(handle);
2852 if (building == nullptr || building->faction()->link.resolve() != faction ||
2853 building->rally()->combatGroup != group)
2854 continue;
2855 building->rally()->reinforcementLimit = maximum;
2856 building->rally()->reinforcementCapped = false;
2857 building->rally()->reinforcementCappedSeconds = 0.0f;
2858 }
2860}
2861
2862Result<void> RTS::setReinforcementTypeLimit(Building& producer, std::string unitType, std::size_t maximum) const {
2863 if (!owns(buildings_, producer) || !producer.rally()->enabled || producer.rally()->combatGroup == 0 ||
2864 unitType.empty() || definitions_ == nullptr || !definitions_->resolve("unit", unitType))
2865 return Result<void>::failure(
2867 "RTS reinforcement type limit requires a grouped rally and known unit type", "unitType"));
2868 const auto group = producer.rally()->combatGroup;
2869 auto* faction = producer.faction()->link.resolve();
2870 for (const auto& handle : buildings_) {
2871 auto* building = tryGetCurrent<Building>(handle);
2872 if (building == nullptr || building->faction()->link.resolve() != faction ||
2873 building->rally()->combatGroup != group)
2874 continue;
2875 if (maximum == 0)
2876 building->rally()->reinforcementTypeLimits.erase(unitType);
2877 else
2878 building->rally()->reinforcementTypeLimits[unitType] = maximum;
2879 building->rally()->reinforcementCapped = false;
2880 building->rally()->reinforcementCappedSeconds = 0.0f;
2881 }
2883}
2884
2885Result<void> RTS::setReinforcementTypePriority(Building& producer, std::string unitType, int priority) const {
2886 if (!owns(buildings_, producer) || !producer.rally()->enabled || producer.rally()->combatGroup == 0 ||
2887 unitType.empty() || priority < 0 || definitions_ == nullptr || !definitions_->resolve("unit", unitType))
2890 "RTS reinforcement priority requires a grouped rally, known unit type, and non-negative priority",
2891 "priority"));
2892 const auto group = producer.rally()->combatGroup;
2893 auto* faction = producer.faction()->link.resolve();
2894 for (const auto& handle : buildings_) {
2895 auto* building = tryGetCurrent<Building>(handle);
2896 if (building == nullptr || building->faction()->link.resolve() != faction ||
2897 building->rally()->combatGroup != group)
2898 continue;
2899 if (priority == 0)
2900 building->rally()->reinforcementTypePriorities.erase(unitType);
2901 else
2902 building->rally()->reinforcementTypePriorities[unitType] = priority;
2903 }
2905}
2906
2907Result<void> RTS::setReinforcementFallback(Building& producer, std::string preferred, std::string fallback) const {
2908 if (!owns(buildings_, producer) || !producer.rally()->enabled || producer.rally()->combatGroup == 0 ||
2909 preferred.empty() || preferred == fallback || definitions_ == nullptr ||
2910 !definitions_->resolve("unit", preferred) || (!fallback.empty() && !definitions_->resolve("unit", fallback)))
2913 "RTS reinforcement fallback requires a grouped rally and known distinct unit types", "fallback"));
2914 auto strategy = producer.rally()->reinforcementFallbacks;
2915 if (fallback.empty())
2916 strategy.erase(preferred);
2917 else
2918 strategy[preferred] = fallback;
2919 for (const auto& [start, ignored] : strategy) {
2920 (void)ignored;
2921 std::set<std::string> visited;
2922 std::string current = start;
2923 while (true) {
2924 const auto next = strategy.find(current);
2925 if (next == strategy.end()) break;
2926 if (!visited.insert(current).second)
2928 DiagnosticCode::Conflict, "RTS reinforcement fallback chain contains a cycle", "fallback"));
2929 current = next->second;
2930 }
2931 }
2932 const auto group = producer.rally()->combatGroup;
2933 auto* faction = producer.faction()->link.resolve();
2934 for (const auto& handle : buildings_) {
2935 auto* building = tryGetCurrent<Building>(handle);
2936 if (building != nullptr && building->faction()->link.resolve() == faction &&
2937 building->rally()->combatGroup == group)
2938 building->rally()->reinforcementFallbacks = strategy;
2939 }
2941}
2942
2943Result<void> RTS::setReinforcementAutoCancel(Building& producer, float seconds) const {
2944 if (!owns(buildings_, producer) || !producer.rally()->enabled || producer.rally()->combatGroup == 0 ||
2945 !std::isfinite(seconds) || seconds < 0.0f)
2948 "RTS reinforcement auto-cancel requires a grouped rally and non-negative delay", "seconds"));
2949 const auto group = producer.rally()->combatGroup;
2950 auto* faction = producer.faction()->link.resolve();
2951 for (const auto& handle : buildings_) {
2952 auto* building = tryGetCurrent<Building>(handle);
2953 if (building == nullptr || building->faction()->link.resolve() != faction ||
2954 building->rally()->combatGroup != group)
2955 continue;
2956 building->rally()->reinforcementAutoCancelDelay = seconds;
2957 building->rally()->reinforcementCappedSeconds = 0.0f;
2958 }
2960}
2961
2962Result<void> RTS::setReinforcementTransport(Building& producer, Unit* transport, std::size_t minimumLoad) const {
2963 if (!owns(buildings_, producer))
2965 DiagnosticCode::StaleHandle, "RTS rally producer does not belong to this facade", "producer"));
2966 if (transport == nullptr) {
2967 if (minimumLoad != 0)
2968 return Result<void>::failure(
2970 "RTS clearing a reinforcement transport requires zero minimum load", "minimumLoad"));
2971 producer.rally()->transport = {};
2972 producer.rally()->minimumTransportLoad = 1;
2973 producer.rally()->transportActive = false;
2975 }
2976 if (!owns(units_, *transport) || transport->faction()->link.resolve() != producer.faction()->link.resolve() ||
2977 transport->containment()->capacity == 0 || minimumLoad == 0 ||
2978 minimumLoad > transport->containment()->capacity || transport->containment()->container.isBound())
2981 "RTS reinforcement transport must be an uncontained friendly carrier with a valid minimum load",
2982 "transport"));
2983 const auto handle = ecs::handle_of(transport);
2984 for (const auto& buildingHandle : buildings_) {
2985 auto* building = dynamic_cast<Building*>(ecs::try_get(buildingHandle));
2986 const auto assigned = building == nullptr ? ecs::EntityHandle{} : building->rally()->transport;
2987 const bool sameTransport = assigned.table == handle.table && assigned.type == handle.type &&
2988 assigned.id == handle.id && assigned.generation == handle.generation;
2989 if (building != nullptr && building != &producer && sameTransport)
2990 return Result<void>::failure(
2992 "RTS reinforcement transport is already assigned to another producer", "transport"));
2993 }
2994 producer.rally()->transport = handle;
2995 producer.rally()->minimumTransportLoad = minimumLoad;
2996 producer.rally()->transportActive = false;
2998}
2999
3000Result<void> RTS::castScriptAbility(Unit& caster, std::string ability, SubjectRef target, WorldPosition point) {
3001 if (!owns(units_, caster))
3003 "RTS ability caster does not belong to this facade", "caster"));
3004 if (!scriptRuntime_ || !scriptRuntime_->configured || definitions_ == nullptr || ability.empty())
3006 "RTS ability requires configured script content", "ability"));
3007 auto resolved = definitions_->resolve("ability", ability);
3008 if (!resolved) return Result<void>::failure(resolved.status());
3009 auto parsed = Value::fromJson(resolved.value().get().json);
3010 if (!parsed) return Result<void>::failure(parsed.status());
3011 const auto* object = parsed.value().getIf<Value::Object>();
3012 if (object == nullptr)
3013 return Result<void>::failure(
3014 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS ability definition must be an object", "ability"));
3015 const auto textField = [object](std::string_view name, std::string fallback = {}) {
3016 const auto found = object->find(std::string(name));
3017 if (found == object->end()) return fallback;
3018 const auto* value = found->second.getIf<std::string>();
3019 return value == nullptr ? fallback : *value;
3020 };
3021 const auto numberField = [object](std::string_view name, double fallback) {
3022 const auto found = object->find(std::string(name));
3023 if (found == object->end()) return std::optional<double>{fallback};
3024 if (const auto* integer = found->second.getIf<std::int64_t>())
3025 return std::optional<double>{static_cast<double>(*integer)};
3026 if (const auto* real = found->second.getIf<double>()) return std::optional<double>{*real};
3027 return std::optional<double>{};
3028 };
3029 const auto boolField = [object](std::string_view name, bool fallback) {
3030 const auto found = object->find(std::string(name));
3031 if (found == object->end()) return std::optional<bool>{fallback};
3032 const auto* value = found->second.getIf<bool>();
3033 return value == nullptr ? std::optional<bool>{} : std::optional<bool>{*value};
3034 };
3035 AbilitySpec spec;
3036 spec.id = ability;
3037 const std::string casterDefinition = textField("casterUnit");
3038 if (!casterDefinition.empty()) {
3039 const auto id = LogicalId::fromParts("unit", casterDefinition);
3040 if (!id)
3042 "RTS ability caster definition is invalid", "casterUnit"));
3043 spec.casterDefinition = *id;
3044 }
3045 const std::string targetType = textField("targetType", "enemy");
3046 if (targetType == "self")
3047 spec.target = AbilityTarget::Self;
3048 else if (targetType == "ally")
3049 spec.target = AbilityTarget::Ally;
3050 else if (targetType == "enemy")
3051 spec.target = AbilityTarget::Enemy;
3052 else if (targetType == "point")
3053 spec.target = AbilityTarget::Point;
3054 else
3055 return Result<void>::failure(
3056 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS ability target type is invalid", "targetType"));
3057 const auto range = numberField("range", 0.0);
3058 const auto radius = numberField("radius", 0.0);
3059 const auto cooldown = numberField("cooldown", 0.0);
3060 const auto damage = numberField("damage", 0.0);
3061 const auto healing = numberField("healing", 0.0);
3062 const auto castTime = numberField("castTime", 0.0);
3063 const auto tickInterval = numberField("tickInterval", 0.0);
3064 const auto resourceCost = numberField("resourceCost", 0.0);
3065 const auto interrupt = boolField("interruptOnDamage", true);
3066 if (!range || !radius || !cooldown || !damage || !healing || !castTime || !tickInterval || !resourceCost ||
3067 !interrupt || std::floor(*resourceCost) != *resourceCost)
3069 "RTS ability definition contains an invalid field", "ability"));
3070 spec.range = static_cast<float>(*range);
3071 spec.radius = static_cast<float>(*radius);
3072 spec.cooldown = static_cast<float>(*cooldown);
3073 spec.damage = static_cast<float>(*damage);
3074 spec.healing = static_cast<float>(*healing);
3075 spec.castTime = static_cast<float>(*castTime);
3076 spec.channelTickInterval = static_cast<float>(*tickInterval);
3077 spec.resourceCost = static_cast<std::int64_t>(*resourceCost);
3078 spec.resourceType = textField("resourceType");
3079 spec.damageType = textField("damageType", "normal");
3080 spec.interruptOnDamage = *interrupt;
3081 const std::string statusEffect = textField("statusEffect");
3082 if (!statusEffect.empty()) {
3083 auto effectDefinition = resolveEffectDefinition(*definitions_, statusEffect, caster.identity()->subject);
3084 if (!effectDefinition) return Result<void>::failure(effectDefinition.status());
3085 spec.appliesEffect = true;
3086 spec.effect = std::move(effectDefinition).takeValue();
3087 }
3088 ecs::EntityHandle targetHandle{};
3089 if (target.isValid()) {
3090 ecs::Entity* entity = findUnit(target);
3091 if (entity == nullptr) entity = findBuilding(target);
3092 if (entity == nullptr)
3093 return Result<void>::failure(
3094 Diagnostic::error(DiagnosticCode::NotFound, "RTS ability target was not found", "target"));
3095 targetHandle = ecs::handle_of(entity);
3096 }
3098 auto* faction = dynamic_cast<Faction*>(unit.faction()->link.resolve());
3099 if (faction == nullptr || !scriptRuntime_)
3101 DiagnosticCode::StaleHandle, "RTS ability caster faction is stale", "caster.faction"));
3102 const auto economy = scriptRuntime_->economies.find(faction->identity()->subject.format());
3103 if (economy == scriptRuntime_->economies.end())
3105 Diagnostic::error(DiagnosticCode::NotFound, "RTS ability economy was not found", "caster.faction"));
3106 return economy->second->account.debit(cost);
3107 };
3108 const DamageEventSink damageEvents =
3110 DamageChannel channel) { recordDamageEvent(request, outcome, tick, channel); };
3111 const LifecycleEventSink lifecycleEvents = [this](const LifecycleEvent& event, SimulationTick tick) {
3112 recordLifecycleEvent(event, tick);
3113 };
3114 return AbilitySystem::cast(caster, spec, targetHandle, point, scriptRuntime_->damage, debit, damageEvents,
3115 SimulationTick(scriptTick()), lifecycleEvents);
3116}
3117
3118Result<void> RTS::cancelScriptAbility(Unit& caster) {
3119 if (!owns(units_, caster))
3121 "RTS ability caster does not belong to this facade", "caster"));
3122 if (!caster.abilities()->channel)
3123 return Result<void>::failure(
3124 Diagnostic::error(DiagnosticCode::NotFound, "RTS unit has no active ability channel", "ability.channel"));
3125 const auto channel = *caster.abilities()->channel;
3126 SubjectRef cancelledTarget;
3127 if (auto* unit = dynamic_cast<Unit*>(ecs::try_get(channel.target)))
3128 cancelledTarget = unit->identity()->subject;
3129 else if (auto* building = dynamic_cast<Building*>(ecs::try_get(channel.target)))
3130 cancelledTarget = building->identity()->subject;
3131 caster.abilities()->channel.reset();
3132 recordLifecycleEvent({LifecycleEventKind::AbilityChannelCancelled, caster.identity()->subject, cancelledTarget,
3133 channel.spec.id, channel.remaining},
3134 SimulationTick(scriptTick()));
3136}
3137
3138Result<std::size_t> RTS::requestFireSupport(Unit& requester, WorldPosition center, float radius, int shotsPerResponder,
3139 std::size_t maxResponders) const {
3140 if (!owns(units_, requester))
3142 DiagnosticCode::StaleHandle, "RTS fire-support requester does not belong to this facade", "requester"));
3143 return FireSupportSystem::request(requester, center, radius, shotsPerResponder, maxResponders);
3144}
3145
3146Result<std::size_t> RTS::cancelFireSupport(Unit& requester) const {
3147 if (!owns(units_, requester))
3149 DiagnosticCode::StaleHandle, "RTS fire-support requester does not belong to this facade", "requester"));
3150 return FireSupportSystem::cancel(requester);
3151}
3152
3153Result<std::size_t> RTS::unloadTransport(Unit& transport, WorldPosition destination) const {
3154 if (!transport.identity()->subject.isValid() || findUnit(transport.identity()->subject) != &transport)
3156 DiagnosticCode::InvalidArgument, "RTS transport is not owned by this composition root", "transport"));
3157 return ContainmentSystem::unload(transport, destination);
3158}
3159
3160Result<std::size_t> RTS::evacuateBuilding(Building& building, WorldPosition destination) const {
3161 if (!building.identity()->subject.isValid() || findBuilding(building.identity()->subject) != &building)
3163 DiagnosticCode::InvalidArgument, "RTS building is not owned by this composition root", "building"));
3164 return ContainmentSystem::evacuate(building, destination);
3165}
3166
3167Result<void> RTS::setUnitCloaked(Unit& unit, bool cloaked) const {
3168 if (!unit.identity()->subject.isValid() || findUnit(unit.identity()->subject) != &unit)
3170 "RTS unit is not owned by this composition root", "unit"));
3171 unit.vision()->cloaked = cloaked;
3173}
3174
3175Result<void> RTS::queueScriptCommand(RTSReplayCommand command) {
3176 if (!scriptRuntime_ || !scriptRuntime_->configured)
3178 DiagnosticCode::Conflict, "RTS script world must be configured before queuing commands", "scriptWorld"));
3179 return scriptRuntime_->commandLog.queue(std::move(command), SimulationTick{scriptRuntime_->nextTick});
3180}
3181
3182Result<std::string> RTS::exportScriptCommandLog() const {
3183 if (!scriptRuntime_)
3185 Diagnostic::error(DiagnosticCode::Conflict, "RTS script runtime is not configured", "scriptWorld"));
3186 return Result<std::string>::success(scriptRuntime_->commandLog.exportText(), Status::success(StatusCode::Applied));
3187}
3188
3189Result<void> RTS::importScriptCommandLog(std::string_view text, bool clearExisting) {
3190 if (!scriptRuntime_ || !scriptRuntime_->configured)
3192 DiagnosticCode::Conflict, "RTS script world must be configured before importing commands", "scriptWorld"));
3193 return scriptRuntime_->commandLog.importText(text, SimulationTick{scriptRuntime_->nextTick}, clearExisting);
3194}
3195
3196std::uint64_t RTS::scriptTick() const noexcept {
3197 return scriptRuntime_ == nullptr || scriptRuntime_->nextTick == 0 ? 0 : scriptRuntime_->nextTick - 1;
3198}
3199
3200Result<void> RTS::captureScriptCheckpoint(std::string name) {
3201 if (!scriptRuntime_ || !scriptRuntime_->configured)
3202 return Result<void>::failure(
3204 "RTS script world must be configured before capturing a checkpoint", "scriptWorld"));
3205 if (name.empty())
3206 return Result<void>::failure(
3207 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS checkpoint name must not be empty", "name"));
3208 auto roots = snapshotState();
3209 if (!roots) return Result<void>::failure(roots.status());
3210
3211 ScriptRuntime::Checkpoint checkpoint;
3212 checkpoint.roots = std::move(roots).takeValue();
3213 checkpoint.pendingProductionSubjects = scriptRuntime_->pendingProductionSubjects;
3214 checkpoint.paidProduction = scriptRuntime_->paidProduction;
3215 checkpoint.paidConstruction = scriptRuntime_->paidConstruction;
3216 checkpoint.commandLog = scriptRuntime_->commandLog;
3217 checkpoint.nextTick = scriptRuntime_->nextTick;
3218 checkpoint.aiProductionSequence = scriptRuntime_->aiProductionSequence;
3219 for (const auto& [key, slot] : scriptRuntime_->economies)
3220 checkpoint.economies.emplace(key, slot->ledger.snapshot());
3221 for (const auto& [key, fov] : scriptRuntime_->fovs)
3222 if (fov) checkpoint.fovs.emplace(key, fov->snapshot());
3223 checkpoint.navigationCosts.reserve(static_cast<std::size_t>(scriptRuntime_->width * scriptRuntime_->height));
3224 for (int y = 0; y < scriptRuntime_->height; ++y)
3225 for (int x = 0; x < scriptRuntime_->width; ++x)
3226 checkpoint.navigationCosts.push_back(scriptRuntime_->pathfinder.getCellCost(x, y));
3227 checkpoint.terrainElevations = scriptRuntime_->terrainElevations;
3228 scriptRuntime_->checkpoints.insert_or_assign(std::move(name), std::move(checkpoint));
3230}
3231
3232Result<void> RTS::restoreScriptCheckpoint(std::string_view name) {
3233 if (!scriptRuntime_ || !scriptRuntime_->configured)
3234 return Result<void>::failure(
3236 "RTS script world must be configured before restoring a checkpoint", "scriptWorld"));
3237 const auto found = scriptRuntime_->checkpoints.find(std::string(name));
3238 if (found == scriptRuntime_->checkpoints.end())
3239 return Result<void>::failure(
3240 Diagnostic::error(DiagnosticCode::NotFound, "RTS script checkpoint was not found", "name"));
3241 const auto& checkpoint = found->second;
3242 const auto expectedCells = static_cast<std::size_t>(scriptRuntime_->width * scriptRuntime_->height);
3243 if (checkpoint.navigationCosts.size() != expectedCells || checkpoint.terrainElevations.size() != expectedCells)
3244 return Result<void>::failure(
3245 Diagnostic::error(DiagnosticCode::Conflict, "RTS checkpoint grid dimensions do not match", "grid"));
3246 for (const auto& [key, snapshot] : checkpoint.fovs) {
3247 const auto current = scriptRuntime_->fovs.find(key);
3248 if (current == scriptRuntime_->fovs.end() || !current->second || snapshot.width != scriptRuntime_->width ||
3249 snapshot.height != scriptRuntime_->height)
3250 return Result<void>::failure(
3251 Diagnostic::error(DiagnosticCode::Conflict, "RTS checkpoint fog topology does not match", "fog"));
3252 }
3253 const auto containsPlayer = [this](SubjectRef subject) {
3254 return std::any_of(players_.begin(), players_.end(), [subject](const ecs::EntityHandle& handle) {
3255 auto* player = tryGetCurrent<Player>(handle);
3256 return player != nullptr && player->identity()->subject == subject;
3257 });
3258 };
3259 const bool exactTopology =
3260 checkpoint.roots.units.size() == unitCount() && checkpoint.roots.buildings.size() == buildingCount() &&
3261 checkpoint.roots.resourceNodes.size() == resourceNodeCount() &&
3262 checkpoint.roots.players.size() == playerCount() && checkpoint.roots.factions.size() == factionCount() &&
3263 checkpoint.roots.matches.size() == matchCount() &&
3264 std::all_of(checkpoint.roots.units.begin(), checkpoint.roots.units.end(),
3265 [this](const auto& value) { return findUnit(value.subject) != nullptr; }) &&
3266 std::all_of(checkpoint.roots.buildings.begin(), checkpoint.roots.buildings.end(),
3267 [this](const auto& value) { return findBuilding(value.subject) != nullptr; }) &&
3268 std::all_of(checkpoint.roots.resourceNodes.begin(), checkpoint.roots.resourceNodes.end(),
3269 [this](const auto& value) { return findResourceNode(value.subject) != nullptr; }) &&
3270 std::all_of(checkpoint.roots.players.begin(), checkpoint.roots.players.end(),
3271 [&](const auto& value) { return containsPlayer(value.subject); }) &&
3272 std::all_of(checkpoint.roots.factions.begin(), checkpoint.roots.factions.end(),
3273 [this](const auto& value) { return findFaction(value.subject) != nullptr; }) &&
3274 std::all_of(checkpoint.roots.matches.begin(), checkpoint.roots.matches.end(),
3275 [this](const auto& value) { return findMatch(value.subject) != nullptr; });
3276
3277 if (exactTopology) {
3278 auto restored = restoreState(checkpoint.roots);
3279 if (!restored) return restored;
3280 } else {
3281 // Validate the complete graph before touching the live roots. The staged
3282 // module also verifies definition-backed weapon materialization.
3283 {
3284 RTS validator;
3285 validator.setDefinitionRegistry(&scriptRuntime_->definitions);
3286 auto valid = validator.rebuildState(checkpoint.roots);
3287 if (!valid) return valid;
3288 }
3289 auto rollbackRoots = snapshotState();
3290 if (!rollbackRoots) return Result<void>::failure(rollbackRoots.status());
3291 setCrowdProvider(nullptr);
3292 setCombatProviders(nullptr, nullptr);
3293 clearOwnedRoots();
3294 auto rebuilt = rebuildState(checkpoint.roots);
3295 if (!rebuilt) {
3296 const Status failureStatus = rebuilt.status();
3297 clearOwnedRoots();
3298 auto rollback = rebuildState(rollbackRoots.value());
3299 setCrowdProvider(&scriptRuntime_->crowd);
3300 setCombatProviders(&scriptRuntime_->sensing, &scriptRuntime_->damage);
3301 auto rebound = rebindScriptRootProviders();
3302 if (!rollback || !rebound)
3303 return Result<void>::failure(
3305 "RTS checkpoint restore and live-state rollback both failed", "checkpoint"));
3306 return Result<void>::failure(failureStatus);
3307 }
3308 setCrowdProvider(&scriptRuntime_->crowd);
3309 setCombatProviders(&scriptRuntime_->sensing, &scriptRuntime_->damage);
3310 auto rebound = rebindScriptRootProviders();
3311 if (!rebound) return rebound;
3312 }
3313
3314 std::size_t cell = 0;
3315 for (int y = 0; y < scriptRuntime_->height; ++y) {
3316 for (int x = 0; x < scriptRuntime_->width; ++x, ++cell) {
3317 const float cost = checkpoint.navigationCosts[cell];
3318 const bool blocked = cost <= 0.0f;
3319 scriptRuntime_->pathfinder.setBlocked(x, y, blocked);
3320 scriptRuntime_->crowd.setBlocked(x, y, blocked);
3321 if (!blocked) {
3322 scriptRuntime_->pathfinder.setCellCost(x, y, cost);
3323 scriptRuntime_->crowd.setCellCost(x, y, cost);
3324 }
3325 for (auto& [faction, fov] : scriptRuntime_->fovs) {
3326 (void)faction;
3327 if (fov) fov->setOpaque(x, y, blocked);
3328 }
3329 }
3330 }
3331 scriptRuntime_->terrainElevations = checkpoint.terrainElevations;
3332 for (auto& [faction, fov] : scriptRuntime_->fovs) {
3333 (void)faction;
3334 if (!fov) continue;
3335 fov->setMode("heightmap");
3336 fov->setEyeOffset(1.0f);
3337 fov->setCliffBlock(0.0f);
3338 for (int y = 0; y < scriptRuntime_->height; ++y)
3339 for (int x = 0; x < scriptRuntime_->width; ++x)
3340 fov->setElevation(
3341 x, y, checkpoint.terrainElevations[static_cast<std::size_t>(y * scriptRuntime_->width + x)]);
3342 }
3343 std::erase_if(scriptRuntime_->economies,
3344 [&](const auto& entry) { return !checkpoint.economies.contains(entry.first); });
3345 std::erase_if(scriptRuntime_->fovs, [&](const auto& entry) { return !checkpoint.fovs.contains(entry.first); });
3346 for (const auto& [key, snapshot] : checkpoint.economies) {
3347 const auto current = scriptRuntime_->economies.find(key);
3348 if (current == scriptRuntime_->economies.end())
3350 DiagnosticCode::Conflict, "RTS checkpoint economy topology does not match", "economy"));
3351 current->second->ledger.restore(snapshot);
3352 }
3353 for (const auto& [key, snapshot] : checkpoint.fovs) {
3354 auto restored = scriptRuntime_->fovs.at(key)->restore(snapshot);
3355 if (!restored) return restored;
3356 }
3357 scriptRuntime_->pendingProductionSubjects = checkpoint.pendingProductionSubjects;
3358 scriptRuntime_->paidProduction = checkpoint.paidProduction;
3359 scriptRuntime_->paidConstruction = checkpoint.paidConstruction;
3360 scriptRuntime_->commandLog = checkpoint.commandLog;
3361 scriptRuntime_->nextTick = checkpoint.nextTick;
3362 scriptRuntime_->aiProductionSequence = checkpoint.aiProductionSequence;
3363 const SimulationTick restoredTick{checkpoint.nextTick == 0 ? 0 : checkpoint.nextTick - 1};
3364 for (const auto& handle : units_)
3365 if (auto* unit = tryGetCurrent<Unit>(handle)) unit->effects()->values.restoreSchedulerTick(restoredTick);
3366 for (const auto& handle : buildings_)
3367 if (auto* building = tryGetCurrent<Building>(handle))
3368 building->effects()->values.restoreSchedulerTick(restoredTick);
3369 scriptRuntime_->actions.clear();
3370 scriptRuntime_->adapter.clear();
3372}
3373
3374Result<void> RTS::removeScriptCheckpoint(std::string_view name) {
3375 if (!scriptRuntime_ || scriptRuntime_->checkpoints.erase(std::string(name)) == 0)
3376 return Result<void>::failure(
3377 Diagnostic::error(DiagnosticCode::NotFound, "RTS script checkpoint was not found", "name"));
3379}
3380
3381Result<bool> RTS::scriptCellVisible(Faction& faction, int x, int y) const {
3382 if (!scriptRuntime_ || !scriptRuntime_->configured || findFaction(faction.identity()->subject) != &faction)
3383 return Result<bool>::failure(
3385 "RTS fog query requires an owned faction in a configured script world", "faction"));
3386 if (x < 0 || y < 0 || x >= scriptRuntime_->width || y >= scriptRuntime_->height)
3387 return Result<bool>::failure(
3388 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS fog cell is outside the grid", "cell"));
3389 const auto found = scriptRuntime_->fovs.find(faction.identity()->subject.format());
3390 if (found == scriptRuntime_->fovs.end() || !found->second)
3391 return Result<bool>::failure(
3392 Diagnostic::error(DiagnosticCode::NotFound, "RTS faction fog provider was not found", "faction"));
3393 return Result<bool>::success(found->second->isVisible(x, y), Status::success(StatusCode::Applied));
3394}
3395
3396Result<bool> RTS::scriptCellExplored(Faction& faction, int x, int y) const {
3397 if (!scriptRuntime_ || !scriptRuntime_->configured || findFaction(faction.identity()->subject) != &faction)
3398 return Result<bool>::failure(
3400 "RTS fog query requires an owned faction in a configured script world", "faction"));
3401 if (x < 0 || y < 0 || x >= scriptRuntime_->width || y >= scriptRuntime_->height)
3402 return Result<bool>::failure(
3403 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS fog cell is outside the grid", "cell"));
3404 const auto found = scriptRuntime_->fovs.find(faction.identity()->subject.format());
3405 if (found == scriptRuntime_->fovs.end() || !found->second)
3406 return Result<bool>::failure(
3407 Diagnostic::error(DiagnosticCode::NotFound, "RTS faction fog provider was not found", "faction"));
3408 return Result<bool>::success(found->second->isExplored(x, y), Status::success(StatusCode::Applied));
3409}
3410
3411Result<Value> RTS::scriptContact(Faction& faction, SubjectRef target) const {
3412 if (findFaction(faction.identity()->subject) != &faction || !target.isValid())
3414 "RTS contact query requires an owned faction and valid target",
3415 "contact"));
3416 const auto* contact = FogOfWarSystem::contact(faction, target);
3417 if (contact == nullptr)
3419 Diagnostic::error(DiagnosticCode::NotFound, "RTS faction contact was not found", "contact"));
3420 return Result<Value>::success(Value(Value::Object{{"subject", contact->subject.format()},
3421 {"kind", contact->kind},
3422 {"x", contact->position.x},
3423 {"y", contact->position.y},
3424 {"age", contact->ageSeconds},
3425 {"visible", contact->visible},
3426 {"detected", contact->detected}}),
3428}
3429
3430void RTS::removeUnitRoot(Unit& unit, UnitRemovalReason reason) {
3431 const auto handle = ecs::handle_of(&unit);
3432 const SubjectRef subject = unit.identity()->subject;
3433 const auto passengers = unit.containment()->occupants;
3434 unit.containment()->occupants.clear();
3435 for (const auto& passengerHandle : passengers) {
3436 auto* passenger = dynamic_cast<Unit*>(ecs::try_get(passengerHandle));
3437 if (passenger != nullptr && passenger != &unit && owns(units_, *passenger)) removeUnitRoot(*passenger);
3438 }
3439
3440 if (reason == UnitRemovalReason::Gameplay && crowd_ != nullptr && unit.crowd()->link.isBound())
3441 crowd_->removeNamedAgent(unit.crowd()->link.key());
3442 const std::string subjectKey = subject.format();
3443 if (reason == UnitRemovalReason::Gameplay) {
3444 if (sensing_ != nullptr) sensing_->remove(subjectKey).ignore("best-effort RTS sensing cleanup");
3445 combatState_.mirroredSubjects.erase(subjectKey);
3446 combatState_.blockedSubjects.erase(subjectKey);
3447 }
3448
3449 for (const auto& nodeHandle : resourceNodes_) {
3450 if (auto* node = dynamic_cast<ResourceNode*>(ecs::try_get(nodeHandle)))
3451 std::erase_if(node->harvest()->workers, [&](const auto& value) { return sameHandle(value, handle); });
3452 }
3453 for (const auto& buildingHandle : buildings_) {
3454 auto* building = dynamic_cast<Building*>(ecs::try_get(buildingHandle));
3455 if (building == nullptr) continue;
3456 std::erase_if(building->construction()->builders, [&](const auto& value) { return sameHandle(value, handle); });
3457 std::erase_if(building->garrison()->occupants, [&](const auto& value) { return sameHandle(value, handle); });
3458 std::erase_if(building->rally()->reinforcements, [&](const auto& value) { return sameHandle(value, handle); });
3459 if (sameHandle(building->rally()->transport, handle)) {
3460 building->rally()->transport = {};
3461 building->rally()->transportActive = false;
3462 }
3463 building->capture()->blockedByGarrison = !building->garrison()->occupants.empty();
3464 }
3465 for (const auto& otherHandle : units_) {
3466 auto* other = dynamic_cast<Unit*>(ecs::try_get(otherHandle));
3467 if (other == nullptr || other == &unit) continue;
3468 std::erase_if(other->containment()->occupants, [&](const auto& value) { return sameHandle(value, handle); });
3469 if (other->containment()->container.isBound() && sameHandle(other->containment()->container.handle(), handle))
3470 other->containment()->container = {};
3471 if (sameHandle(other->combat()->target, handle)) other->combat()->target = {};
3472 if (sameHandle(other->tactics()->escortTarget, handle)) other->tactics()->escortTarget = {};
3473 if (sameHandle(other->supply()->assignedTarget, handle)) other->supply()->assignedTarget = {};
3474 if (sameHandle(other->supply()->convoyLeader, handle)) other->supply()->convoyLeader = {};
3475 }
3476 for (const auto& factionHandle : factions_) {
3477 if (auto* faction = dynamic_cast<Faction*>(ecs::try_get(factionHandle)))
3478 std::erase_if(faction->members()->units, [&](const auto& value) { return sameHandle(value, handle); });
3479 }
3480 for (const auto& playerHandle : players_) {
3481 if (auto* player = dynamic_cast<Player*>(ecs::try_get(playerHandle)))
3482 std::erase_if(player->selection()->units, [&](const auto& value) { return sameHandle(value, handle); });
3483 }
3484
3485 const auto weaponHandle = unit.weapon()->link.handle();
3486 if (auto* weapon = dynamic_cast<weapon::WeaponEntity*>(unit.weapon()->link.resolve())) weapon->release();
3487 std::erase_if(weapons_, [&](const auto& value) { return sameHandle(value, weaponHandle); });
3488 std::erase_if(units_, [&](const auto& value) { return sameHandle(value, handle); });
3489 if (scriptRuntime_ && reason != UnitRemovalReason::SpawnRollback)
3490 std::erase_if(scriptRuntime_->paidProduction,
3491 [&](const auto& value) { return value.resultSubject == subject; });
3492 unit.release();
3493}
3494
3495void RTS::removeBuildingRoot(Building& building, bool destroyOccupants) {
3496 const auto handle = ecs::handle_of(&building);
3497 const SubjectRef subject = building.identity()->subject;
3498 const std::string subjectKey = subject.format();
3499 if (sensing_ != nullptr) sensing_->remove(subjectKey).ignore("best-effort RTS sensing cleanup");
3500 combatState_.mirroredSubjects.erase(subjectKey);
3501 combatState_.blockedSubjects.erase(subjectKey);
3502 const auto occupants = building.garrison()->occupants;
3503 building.garrison()->occupants.clear();
3504 building.capture()->blockedByGarrison = false;
3505 for (const auto& occupantHandle : occupants) {
3506 auto* occupant = dynamic_cast<Unit*>(ecs::try_get(occupantHandle));
3507 if (occupant == nullptr || !owns(units_, *occupant)) continue;
3508 occupant->containment()->container = {};
3509 if (destroyOccupants) {
3510 occupant->durability()->alive = false;
3511 occupant->durability()->state.health = 0.0;
3512 removeUnitRoot(*occupant);
3513 } else {
3514 occupant->motion()->x = building.placement()->worldX;
3515 occupant->motion()->y = building.placement()->worldY;
3516 occupant->motion()->arrived = true;
3517 occupant->orders()->values.clear();
3518 }
3519 }
3520 for (const auto& unitHandle : units_) {
3521 auto* unit = dynamic_cast<Unit*>(ecs::try_get(unitHandle));
3522 if (unit == nullptr) continue;
3523 if (unit->containment()->container.isBound() && sameHandle(unit->containment()->container.handle(), handle))
3524 unit->containment()->container = {};
3525 if (sameHandle(unit->combat()->target, handle)) unit->combat()->target = {};
3526 if (sameHandle(unit->tactics()->escortTarget, handle)) unit->tactics()->escortTarget = {};
3527 if (unit->worker()->dropoff.isBound() && sameHandle(unit->worker()->dropoff.handle(), handle))
3528 unit->worker()->dropoff = {};
3529 }
3530 for (const auto& factionHandle : factions_) {
3531 if (auto* faction = dynamic_cast<Faction*>(ecs::try_get(factionHandle)))
3532 std::erase_if(faction->members()->buildings, [&](const auto& value) { return sameHandle(value, handle); });
3533 }
3534 for (const auto& playerHandle : players_) {
3535 if (auto* player = dynamic_cast<Player*>(ecs::try_get(playerHandle)))
3536 std::erase_if(player->selection()->buildings, [&](const auto& value) { return sameHandle(value, handle); });
3537 }
3538 const auto weaponHandle = building.weapon()->link.handle();
3539 if (auto* weapon = dynamic_cast<weapon::WeaponEntity*>(building.weapon()->link.resolve())) weapon->release();
3540 std::erase_if(weapons_, [&](const auto& value) { return sameHandle(value, weaponHandle); });
3541 std::erase_if(buildings_, [&](const auto& value) { return sameHandle(value, handle); });
3542 if (scriptRuntime_) {
3543 std::erase_if(scriptRuntime_->paidConstruction, [&](const auto& value) { return value.building == subject; });
3544 std::erase_if(scriptRuntime_->paidProduction, [&](const auto& value) {
3545 if (value.producer != subject) return false;
3546 scriptRuntime_->pendingProductionSubjects.erase(value.taskId);
3547 return true;
3548 });
3549 }
3550 building.release();
3551}
3552
3553void RTS::removeResourceNodeRoot(ResourceNode& node) {
3554 const auto handle = ecs::handle_of(&node);
3555 for (const auto& workerHandle : node.harvest()->workers) {
3556 auto* worker = dynamic_cast<Unit*>(ecs::try_get(workerHandle));
3557 if (worker != nullptr && worker->worker()->resourceNode.isBound() &&
3558 sameHandle(worker->worker()->resourceNode.handle(), handle)) {
3559 worker->worker()->resourceNode = {};
3560 worker->orders()->values.clear();
3561 }
3562 }
3563 node.harvest()->workers.clear();
3564 std::erase_if(resourceNodes_, [&](const auto& value) { return sameHandle(value, handle); });
3565 node.release();
3566}
3567
3569 if (!subject.isValid())
3571 "RTS removal requires a valid stable identity", "subject"));
3572 if (auto* unit = findUnit(subject)) {
3573 removeUnitRoot(*unit);
3575 }
3576 if (auto* building = findBuilding(subject)) {
3577 removeBuildingRoot(*building, false);
3579 }
3580 if (auto* node = findResourceNode(subject)) {
3581 removeResourceNodeRoot(*node);
3583 }
3584 return Result<void>::failure(
3585 Diagnostic::error(DiagnosticCode::NotFound, "RTS gameplay root identity was not found", "subject"));
3586}
3587
3588Result<std::size_t> RTS::cleanupDestroyed() {
3589 const std::size_t before = unitCount() + buildingCount();
3590 std::vector<ecs::EntityHandle> deadUnits;
3591 std::vector<ecs::EntityHandle> deadBuildings;
3592 for (const auto& handle : units_) {
3593 auto* unit = tryGetCurrent<Unit>(handle);
3594 if (unit != nullptr && (!unit->durability()->alive || unit->durability()->state.health <= 0.0))
3595 deadUnits.push_back(handle);
3596 }
3597 for (const auto& handle : buildings_) {
3598 auto* building = tryGetCurrent<Building>(handle);
3599 if (building != nullptr && (!building->integrity()->alive || building->integrity()->state.health <= 0.0))
3600 deadBuildings.push_back(handle);
3601 }
3602 for (const auto& handle : deadBuildings) {
3603 auto* building = tryGetCurrent<Building>(handle);
3604 if (building == nullptr || !owns(buildings_, *building)) continue;
3605 removeBuildingRoot(*building, true);
3606 }
3607 for (const auto& handle : deadUnits) {
3608 auto* unit = tryGetCurrent<Unit>(handle);
3609 if (unit == nullptr || !owns(units_, *unit)) continue;
3610 removeUnitRoot(*unit);
3611 }
3612 const std::size_t after = unitCount() + buildingCount();
3613 const std::size_t removed = before >= after ? before - after : 0;
3616}
3617
3618std::size_t RTS::unitCount() const noexcept { return countLive<Unit>(units_); }
3619std::size_t RTS::buildingCount() const noexcept { return countLive<Building>(buildings_); }
3620std::size_t RTS::resourceNodeCount() const noexcept { return countLive<ResourceNode>(resourceNodes_); }
3621std::size_t RTS::playerCount() const noexcept { return countLive<Player>(players_); }
3622std::size_t RTS::factionCount() const noexcept { return countLive<Faction>(factions_); }
3623std::size_t RTS::matchCount() const noexcept { return countLive<Match>(matches_); }
3624
3625Faction* RTS::findFaction(SubjectRef subject) const noexcept { return findSubject<Faction>(factions_, subject); }
3626Match* RTS::findMatch(SubjectRef subject) const noexcept { return findSubject<Match>(matches_, subject); }
3627
3628Player* RTS::resolvePlayer(SubjectRef subject) const noexcept {
3629 for (const auto& handle : players_) {
3630 auto* player = tryGetCurrent<Player>(handle);
3631 if (player && player->identity()->subject == subject) return player;
3632 }
3633 return nullptr;
3634}
3635
3636Unit* RTS::resolveUnit(SubjectRef subject) const noexcept {
3637 for (const auto& handle : units_) {
3638 auto* unit = tryGetCurrent<Unit>(handle);
3639 if (unit && unit->identity()->subject == subject) return unit;
3640 }
3641 return nullptr;
3642}
3643
3644void RTS::expose(ssq::Table& table) {
3645 auto cls = table.addClass(name, RTS::create, false);
3646 expose(cls);
3647}
3648
3649void RTS::expose(ssq::Class& cls) {
3650 cls.addFunc("getName", &RTS::getName);
3651 // simplesquirrel's member-function binder predates noexcept member
3652 // pointers; keep the C++ query APIs noexcept and adapt them at the script
3653 // boundary with the same object-pointer convention used by other modules.
3654 cls.addFunc("unitCount", [](RTS* self) { return self->unitCount(); });
3655 cls.addFunc("buildingCount", [](RTS* self) { return self->buildingCount(); });
3656 cls.addFunc("resourceNodeCount", [](RTS* self) { return self->resourceNodeCount(); });
3657 cls.addFunc("playerCount", [](RTS* self) { return self->playerCount(); });
3658 cls.addFunc("factionCount", [](RTS* self) { return self->factionCount(); });
3659 cls.addFunc("matchCount", [](RTS* self) { return self->matchCount(); });
3660 cls.addFunc("scriptTick", [](RTS* self) { return static_cast<std::int64_t>(self->scriptTick()); });
3661 const auto vm = cls.getHandle();
3662 cls.addFunc("configureSettlementRulesJson", [vm](RTS* self, const std::string& json) -> ssq::Table {
3663 if (self == nullptr)
3666 Diagnostic::error(DiagnosticCode::InvalidArgument, "RTS receiver must not be null", "rts")));
3667 auto rules = settlement::SettlementRuleSet::fromJson(json);
3668 if (!rules) return script::projectStatusResult(vm, rules.status());
3669 return script::projectResult(vm, self->configureSettlementRules(rules.value()));
3670 });
3671 cls.addFunc("removeSubject", [vm](RTS* self, const std::string& subjectText) -> ssq::Table {
3672 auto subject = parseScriptSubject(subjectText, "subject");
3673 if (!subject) return script::projectStatusResult(vm, subject.status());
3674 return script::projectResult(vm, self->remove(std::move(subject).takeValue()));
3675 });
3676 cls.addFunc("newFaction", [vm](RTS* self, const std::string& subjectText) -> ssq::Table {
3677 auto subject = parseScriptSubject(subjectText, "subject");
3678 if (!subject) return script::projectStatusResult(vm, subject.status());
3679 return script::projectResult(vm, self->newFaction(std::move(subject).takeValue()),
3680 [](Faction* faction) { return Value(faction->identity()->subject.format()); });
3681 });
3682 cls.addFunc("newMatch", [vm](RTS* self, const std::string& subjectText) -> ssq::Table {
3683 auto subjectValue = parseScriptSubject(subjectText, "subject");
3684 if (!subjectValue) return script::projectStatusResult(vm, subjectValue.status());
3685 return script::projectResult(vm, self->newMatch(std::move(subjectValue).takeValue()),
3686 [](Match* match) { return Value(match->identity()->subject.format()); });
3687 });
3688 cls.addFunc("configureMatch",
3689 [vm](RTS* self, const std::string& matchText, const std::string& ruleText, const std::string& archetype,
3690 float targetValue) -> ssq::Table {
3691 auto matchSubject = parseScriptSubject(matchText, "match");
3692 if (!matchSubject) return script::projectStatusResult(vm, matchSubject.status());
3693 Match* match = self->findMatch(matchSubject.value());
3694 if (match == nullptr)
3697 "RTS match identity was not found", "match")));
3698 VictoryRule rule;
3699 if (ruleText == "annihilation")
3700 rule = VictoryRule::Annihilation;
3701 else if (ruleText == "headquarters")
3702 rule = VictoryRule::DestroyHeadquarters;
3703 else if (ruleText == "resource")
3704 rule = VictoryRule::ResourceTarget;
3705 else
3708 "RTS victory rule is invalid", "rule")));
3709 return script::projectResult(
3710 vm, self->configureMatch(*match, rule, archetype, static_cast<double>(targetValue)));
3711 });
3712 cls.addFunc(
3713 "addMatchParticipant",
3714 [vm](RTS* self, const std::string& matchText, const std::string& factionText, int team) -> ssq::Table {
3715 auto matchSubject = parseScriptSubject(matchText, "match");
3716 if (!matchSubject) return script::projectStatusResult(vm, matchSubject.status());
3717 auto factionSubject = parseScriptSubject(factionText, "faction");
3718 if (!factionSubject) return script::projectStatusResult(vm, factionSubject.status());
3719 Match* match = self->findMatch(matchSubject.value());
3720 Faction* faction = self->findFaction(factionSubject.value());
3721 if (match == nullptr || faction == nullptr)
3724 DiagnosticCode::NotFound, "RTS match participant identity was not found", "participant")));
3725 return script::projectResult(vm, self->addMatchParticipant(*match, *faction, team));
3726 });
3727 cls.addFunc("startMatch", [vm](RTS* self, const std::string& matchText) -> ssq::Table {
3728 auto matchSubject = parseScriptSubject(matchText, "match");
3729 if (!matchSubject) return script::projectStatusResult(vm, matchSubject.status());
3730 Match* match = self->findMatch(matchSubject.value());
3731 if (match == nullptr)
3734 Diagnostic::error(DiagnosticCode::NotFound, "RTS match identity was not found", "match")));
3735 return script::projectResult(vm, self->startMatch(*match));
3736 });
3737 cls.addFunc(
3738 "surrenderMatch", [vm](RTS* self, const std::string& matchText, const std::string& factionText) -> ssq::Table {
3739 auto matchSubject = parseScriptSubject(matchText, "match");
3740 if (!matchSubject) return script::projectStatusResult(vm, matchSubject.status());
3741 auto factionSubject = parseScriptSubject(factionText, "faction");
3742 if (!factionSubject) return script::projectStatusResult(vm, factionSubject.status());
3743 Match* match = self->findMatch(matchSubject.value());
3744 Faction* faction = self->findFaction(factionSubject.value());
3745 if (match == nullptr || faction == nullptr)
3748 DiagnosticCode::NotFound, "RTS match participant identity was not found", "participant")));
3749 return script::projectResult(vm, self->surrenderMatch(*match, *faction));
3750 });
3751 cls.addFunc("inspectMatch", [vm](RTS* self, const std::string& matchText) -> ssq::Table {
3752 auto matchSubject = parseScriptSubject(matchText, "match");
3753 if (!matchSubject) return script::projectStatusResult(vm, matchSubject.status());
3754 Match* match = self->findMatch(matchSubject.value());
3755 if (match == nullptr)
3758 Diagnostic::error(DiagnosticCode::NotFound, "RTS match identity was not found", "match")));
3759 return script::projectResult(vm, self->inspectMatch(*match), [](Value value) { return value; });
3760 });
3761 cls.addFunc("newUnit",
3762 [vm](RTS* self, const std::string& subjectText, const std::string& definitionText,
3763 const std::string& factionText, float x, float y) -> ssq::Table {
3764 auto subject = parseScriptSubject(subjectText, "subject");
3765 if (!subject) return script::projectStatusResult(vm, subject.status());
3766 auto definition = parseScriptDefinition(definitionText);
3767 if (!definition) return script::projectStatusResult(vm, definition.status());
3768 auto factionSubject = parseScriptSubject(factionText, "faction");
3769 if (!factionSubject) return script::projectStatusResult(vm, factionSubject.status());
3770 Faction* faction = self->findFaction(std::move(factionSubject).takeValue());
3771 if (faction == nullptr)
3774 "RTS script faction was not found", "faction")));
3775 auto created = self->newFactionUnit(*faction, std::move(subject).takeValue(),
3776 std::move(definition).takeValue());
3777 if (!created) return script::projectStatusResult(vm, created.status());
3778 Unit* unit = std::move(created).takeValue();
3779 unit->motion()->x = x;
3780 unit->motion()->y = y;
3782 Value(unit->identity()->subject.format()));
3783 });
3784 cls.addFunc("newBuilding",
3785 [vm](RTS* self, const std::string& subjectText, const std::string& definitionText,
3786 const std::string& factionText, float x, float y) -> ssq::Table {
3787 auto subject = parseScriptSubject(subjectText, "subject");
3788 if (!subject) return script::projectStatusResult(vm, subject.status());
3789 auto definition = parseScriptDefinition(definitionText);
3790 if (!definition) return script::projectStatusResult(vm, definition.status());
3791 auto factionSubject = parseScriptSubject(factionText, "faction");
3792 if (!factionSubject) return script::projectStatusResult(vm, factionSubject.status());
3793 Faction* faction = self->findFaction(std::move(factionSubject).takeValue());
3794 if (faction == nullptr)
3797 "RTS script faction was not found", "faction")));
3798 auto created = self->newFactionBuilding(*faction, std::move(subject).takeValue(),
3799 std::move(definition).takeValue());
3800 if (!created) return script::projectStatusResult(vm, created.status());
3801 Building* building = std::move(created).takeValue();
3802 building->placement()->worldX = x;
3803 building->placement()->worldY = y;
3805 Value(building->identity()->subject.format()));
3806 });
3807 cls.addFunc(
3808 "newResourceNode",
3809 [vm](RTS* self, const std::string& subjectText, const std::string& resource, float amount, float x, float y,
3810 int capacity) -> ssq::Table {
3811 auto subject = parseScriptSubject(subjectText, "subject");
3812 if (!subject) return script::projectStatusResult(vm, subject.status());
3813 if (capacity <= 0)
3815 vm,
3817 "RTS script resource capacity must be positive", "capacity")));
3819 self->newResourceNode(std::move(subject).takeValue(), resource, amount, {x, y},
3820 static_cast<std::size_t>(capacity)),
3821 [](ResourceNode* node) { return Value(node->identity()->subject.format()); });
3822 });
3823 cls.addFunc("inspectState", [vm](RTS* self) -> ssq::Table {
3824 return script::projectStatusResult(vm, Status::success(), self->inspectState());
3825 });
3826 cls.addFunc("inspectFrameEvents", [vm](RTS* self) -> ssq::Table {
3827 return script::projectStatusResult(vm, Status::success(), self->inspectFrameEvents());
3828 });
3829 cls.addFunc("stepScript", [vm](RTS* self, float seconds) -> ssq::Table {
3830 return script::projectResult(vm, self->stepScript(static_cast<double>(seconds)),
3831 [](std::size_t processed) { return Value(static_cast<std::int64_t>(processed)); });
3832 });
3833 cls.addFunc("configureScriptWorld",
3834 [vm](RTS* self, int width, int height, float cellSize, float originX, float originY) -> ssq::Table {
3836 self->configureScriptWorld(width, height, cellSize, originX, originY));
3837 });
3838 cls.addFunc("captureScriptCheckpoint", [vm](RTS* self, const std::string& name) -> ssq::Table {
3839 return script::projectResult(vm, self->captureScriptCheckpoint(name));
3840 });
3841 cls.addFunc("restoreScriptCheckpoint", [vm](RTS* self, const std::string& name) -> ssq::Table {
3842 return script::projectResult(vm, self->restoreScriptCheckpoint(name));
3843 });
3844 cls.addFunc("removeScriptCheckpoint", [vm](RTS* self, const std::string& name) -> ssq::Table {
3845 return script::projectResult(vm, self->removeScriptCheckpoint(name));
3846 });
3847 cls.addFunc("loadScriptContent", [vm](RTS* self, const std::string& json) -> ssq::Table {
3848 return script::projectResult(vm, self->loadScriptContent(json), [](ContentImportReceipt receipt) {
3849 return Value(Value::Object{{"inserted", static_cast<std::int64_t>(receipt.inserted)},
3850 {"replaced", static_cast<std::int64_t>(receipt.replaced)}});
3851 });
3852 });
3853 cls.addFunc("setScriptNavigationBlocked", [vm](RTS* self, int x, int y, bool blocked) -> ssq::Table {
3854 return script::projectResult(vm, self->setScriptNavigationBlocked(x, y, blocked));
3855 });
3856 cls.addFunc("setScriptNavigationCost", [vm](RTS* self, int x, int y, float cost) -> ssq::Table {
3857 return script::projectResult(vm, self->setScriptNavigationCost(x, y, cost));
3858 });
3859 cls.addFunc("setScriptTerrainElevation", [vm](RTS* self, int x, int y, float elevation) -> ssq::Table {
3860 return script::projectResult(vm, self->setScriptTerrainElevation(x, y, elevation));
3861 });
3862 cls.addFunc("scriptTerrainElevation", [vm](RTS* self, int x, int y) -> ssq::Table {
3863 return script::projectResult(vm, self->scriptTerrainElevation(x, y),
3864 [](float elevation) { return Value(static_cast<double>(elevation)); });
3865 });
3866 cls.addFunc("scriptCellVisible", [vm](RTS* self, const std::string& factionText, int x, int y) -> ssq::Table {
3867 auto subjectValue = parseScriptSubject(factionText, "faction");
3868 if (!subjectValue) return script::projectStatusResult(vm, subjectValue.status());
3869 Faction* faction = self->findFaction(subjectValue.value());
3870 if (faction == nullptr)
3873 "RTS fog faction identity was not found", "faction")));
3874 return script::projectResult(vm, self->scriptCellVisible(*faction, x, y),
3875 [](bool visible) { return Value(visible); });
3876 });
3877 cls.addFunc("scriptCellExplored", [vm](RTS* self, const std::string& factionText, int x, int y) -> ssq::Table {
3878 auto subjectValue = parseScriptSubject(factionText, "faction");
3879 if (!subjectValue) return script::projectStatusResult(vm, subjectValue.status());
3880 Faction* faction = self->findFaction(subjectValue.value());
3881 if (faction == nullptr)
3884 "RTS fog faction identity was not found", "faction")));
3885 return script::projectResult(vm, self->scriptCellExplored(*faction, x, y),
3886 [](bool explored) { return Value(explored); });
3887 });
3888 cls.addFunc("scriptContact",
3889 [vm](RTS* self, const std::string& factionText, const std::string& targetText) -> ssq::Table {
3890 auto factionSubject = parseScriptSubject(factionText, "faction");
3891 if (!factionSubject) return script::projectStatusResult(vm, factionSubject.status());
3892 auto targetSubject = parseScriptSubject(targetText, "target");
3893 if (!targetSubject) return script::projectStatusResult(vm, targetSubject.status());
3894 Faction* faction = self->findFaction(factionSubject.value());
3895 if (faction == nullptr)
3898 DiagnosticCode::NotFound, "RTS fog faction identity was not found", "faction")));
3899 return script::projectResult(vm, self->scriptContact(*faction, targetSubject.value()),
3900 [](Value value) { return value; });
3901 });
3902 cls.addFunc("addScriptResource",
3903 [vm](RTS* self, const std::string& factionText, const std::string& resource,
3904 std::int64_t amount) -> ssq::Table {
3905 auto factionSubject = parseScriptSubject(factionText, "faction");
3906 if (!factionSubject) return script::projectStatusResult(vm, factionSubject.status());
3907 Faction* faction = self->findFaction(std::move(factionSubject).takeValue());
3908 if (faction == nullptr)
3911 "RTS script faction was not found", "faction")));
3912 return script::projectResult(vm, self->addScriptResource(*faction, resource, amount));
3913 });
3914 cls.addFunc("scriptResource",
3915 [vm](RTS* self, const std::string& factionText, const std::string& resource) -> ssq::Table {
3916 auto factionSubject = parseScriptSubject(factionText, "faction");
3917 if (!factionSubject) return script::projectStatusResult(vm, factionSubject.status());
3918 Faction* faction = self->findFaction(std::move(factionSubject).takeValue());
3919 if (faction == nullptr)
3922 "RTS script faction was not found", "faction")));
3923 return script::projectResult(vm, self->scriptResource(*faction, resource),
3924 [](std::int64_t amount) { return Value(amount); });
3925 });
3926 cls.addFunc("configureScriptAI",
3927 [vm](RTS* self, const std::string& factionText, const std::string& workerText,
3928 const std::string& armyText, const std::string& targetBuildingText, int desiredWorkers,
3929 int attackThreshold, float thinkInterval, float formationSpacing, bool enabled) -> ssq::Table {
3930 auto factionSubject = parseScriptSubject(factionText, "faction");
3931 auto worker = parseScriptDefinition(workerText);
3932 auto army = parseScriptDefinition(armyText);
3933 auto target = parseScriptDefinition(targetBuildingText);
3934 if (!factionSubject) return script::projectStatusResult(vm, factionSubject.status());
3935 if (!worker) return script::projectStatusResult(vm, worker.status());
3936 if (!army) return script::projectStatusResult(vm, army.status());
3937 if (!target) return script::projectStatusResult(vm, target.status());
3938 Faction* faction = self->findFaction(factionSubject.value());
3939 if (faction == nullptr)
3942 "RTS script AI faction was not found", "faction")));
3943 return script::projectResult(
3944 vm,
3945 self->configureScriptAI(*faction, worker.value(), army.value(), target.value(), desiredWorkers,
3946 attackThreshold, thinkInterval, formationSpacing, enabled));
3947 });
3948 cls.addFunc("queueScriptUnit",
3949 [vm](RTS* self, const std::string& producerText, const std::string& unitSubjectText,
3950 const std::string& definitionText, int priority) -> ssq::Table {
3951 auto producerSubject = parseScriptSubject(producerText, "producer");
3952 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
3953 Building* producer = self->findBuilding(producerSubject.value());
3954 if (producer == nullptr)
3957 "RTS script producer was not found", "producer")));
3958 auto unitSubject = parseScriptSubject(unitSubjectText, "unitSubject");
3959 if (!unitSubject) return script::projectStatusResult(vm, unitSubject.status());
3960 auto definition = parseScriptDefinition(definitionText);
3961 if (!definition) return script::projectStatusResult(vm, definition.status());
3962 return script::projectResult(
3963 vm, self->queueScriptUnit(*producer, unitSubject.value(), definition.value(), priority),
3964 [](RTSBuildReceipt receipt) {
3965 return Value(Value::Object{{"productionTaskId", std::move(receipt.productionTaskId)},
3966 {"orderId", std::move(receipt.orderId)}});
3967 });
3968 });
3969 cls.addFunc(
3970 "queueScriptReinforcement",
3971 [vm](RTS* self, const std::string& producerText, const std::string& unitSubjectText,
3972 const std::string& definitionText, int priority) -> ssq::Table {
3973 auto producerSubject = parseScriptSubject(producerText, "producer");
3974 auto unitSubject = parseScriptSubject(unitSubjectText, "unitSubject");
3975 auto definition = parseScriptDefinition(definitionText);
3976 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
3977 if (!unitSubject) return script::projectStatusResult(vm, unitSubject.status());
3978 if (!definition) return script::projectStatusResult(vm, definition.status());
3979 Building* producer = self->findBuilding(producerSubject.value());
3980 if (producer == nullptr)
3983 DiagnosticCode::NotFound, "RTS script reinforcement producer was not found", "producer")));
3984 return script::projectResult(
3985 vm, self->queueScriptReinforcement(*producer, unitSubject.value(), definition.value(), priority),
3986 [](ReinforcementRequestReceipt receipt) {
3987 return Value(Value::Object{{"requestedProduct", std::move(receipt.requestedProduct)},
3988 {"queuedProduct", std::move(receipt.queuedProduct)},
3989 {"productionTaskId", std::move(receipt.taskId)}});
3990 });
3991 });
3992 cls.addFunc(
3993 "startScriptConstruction",
3994 [vm](RTS* self, const std::string& factionText, const std::string& buildingSubjectText,
3995 const std::string& definitionText, float x, float y, const std::string& builderText) -> ssq::Table {
3996 auto factionSubject = parseScriptSubject(factionText, "faction");
3997 auto buildingSubject = parseScriptSubject(buildingSubjectText, "buildingSubject");
3998 auto definition = parseScriptDefinition(definitionText);
3999 auto builderSubject = parseScriptSubject(builderText, "builder");
4000 if (!factionSubject) return script::projectStatusResult(vm, factionSubject.status());
4001 if (!buildingSubject) return script::projectStatusResult(vm, buildingSubject.status());
4002 if (!definition) return script::projectStatusResult(vm, definition.status());
4003 if (!builderSubject) return script::projectStatusResult(vm, builderSubject.status());
4004 Faction* faction = self->findFaction(factionSubject.value());
4005 Unit* builder = self->findUnit(builderSubject.value());
4006 if (faction == nullptr || builder == nullptr)
4009 "RTS script construction faction or builder was not found",
4010 "construction")));
4011 return script::projectResult(
4012 vm,
4013 self->startScriptConstruction(*faction, buildingSubject.value(), definition.value(), {x, y}, *builder),
4014 [](Building* building) { return Value(building->identity()->subject.format()); });
4015 });
4016 const auto buildingLifecycle = [vm](RTS* self, const std::string& buildingText, bool sell) -> ssq::Table {
4017 auto subjectValue = parseScriptSubject(buildingText, "building");
4018 if (!subjectValue) return script::projectStatusResult(vm, subjectValue.status());
4019 Building* building = self->findBuilding(subjectValue.value());
4020 if (building == nullptr)
4023 Diagnostic::error(DiagnosticCode::NotFound, "RTS script building was not found", "building")));
4024 auto result = sell ? self->sellScriptBuilding(*building) : self->cancelScriptConstruction(*building);
4025 return script::projectResult(vm, std::move(result), [](resource::Receipt) { return Value(true); });
4026 };
4027 cls.addFunc("cancelScriptConstruction",
4028 [buildingLifecycle](RTS* self, const std::string& buildingText) -> ssq::Table {
4029 return buildingLifecycle(self, buildingText, false);
4030 });
4031 cls.addFunc("sellScriptBuilding", [buildingLifecycle](RTS* self, const std::string& buildingText) -> ssq::Table {
4032 return buildingLifecycle(self, buildingText, true);
4033 });
4034 cls.addFunc(
4035 "queueScriptResearch",
4036 [vm](RTS* self, const std::string& producerText, const std::string& upgrade, int priority) -> ssq::Table {
4037 auto producerSubject = parseScriptSubject(producerText, "producer");
4038 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
4039 Building* producer = self->findBuilding(producerSubject.value());
4040 if (producer == nullptr)
4043 "RTS script research producer was not found", "producer")));
4044 return script::projectResult(
4045 vm, self->queueScriptResearch(*producer, upgrade, priority), [](RTSBuildReceipt receipt) {
4046 return Value(Value::Object{{"productionTaskId", std::move(receipt.productionTaskId)},
4047 {"orderId", std::move(receipt.orderId)}});
4048 });
4049 });
4050 cls.addFunc(
4051 "cancelScriptProduction", [vm](RTS* self, const std::string& producerText, int queueIndex) -> ssq::Table {
4052 auto producerSubject = parseScriptSubject(producerText, "producer");
4053 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
4054 Building* producer = self->findBuilding(producerSubject.value());
4055 if (producer == nullptr)
4057 vm, Status::failure(Diagnostic::error(DiagnosticCode::NotFound, "RTS script producer was not found",
4058 "producer")));
4059 return script::projectResult(
4060 vm, self->cancelScriptProduction(*producer, queueIndex), [](RTSCancelProductionReceipt receipt) {
4061 return Value(Value::Object{{"productionTaskId", std::move(receipt.productionTaskId)},
4062 {"orderId", std::move(receipt.orderId)}});
4063 });
4064 });
4065 cls.addFunc("castScriptAbility",
4066 [vm](RTS* self, const std::string& casterText, const std::string& ability,
4067 const std::string& targetText, float x, float y) -> ssq::Table {
4068 auto casterSubject = parseScriptSubject(casterText, "caster");
4069 if (!casterSubject) return script::projectStatusResult(vm, casterSubject.status());
4070 Unit* caster = self->findUnit(casterSubject.value());
4071 if (caster == nullptr)
4074 DiagnosticCode::NotFound, "RTS script ability caster was not found", "caster")));
4076 if (!targetText.empty()) {
4077 auto parsed = parseScriptSubject(targetText, "target");
4078 if (!parsed) return script::projectStatusResult(vm, parsed.status());
4079 target = parsed.value();
4080 }
4081 return script::projectResult(vm, self->castScriptAbility(*caster, ability, target, {x, y}));
4082 });
4083 cls.addFunc("cancelScriptAbility", [vm](RTS* self, const std::string& casterText) -> ssq::Table {
4084 auto casterSubject = parseScriptSubject(casterText, "caster");
4085 if (!casterSubject) return script::projectStatusResult(vm, casterSubject.status());
4086 Unit* caster = self->findUnit(casterSubject.value());
4087 if (caster == nullptr)
4090 "RTS script ability caster was not found", "caster")));
4091 return script::projectResult(vm, self->cancelScriptAbility(*caster));
4092 });
4093 cls.addFunc("setBuildingRally",
4094 [vm](RTS* self, const std::string& producerText, float x, float y, bool attackMove,
4095 bool groupedReinforcements) -> ssq::Table {
4096 auto producerSubject = parseScriptSubject(producerText, "producer");
4097 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
4098 Building* producer = self->findBuilding(producerSubject.value());
4099 if (producer == nullptr)
4102 "RTS rally producer was not found", "producer")));
4103 CommandSpec command;
4104 command.kind = attackMove ? OrderKind::AttackMove : OrderKind::Move;
4105 command.target = {x, y};
4106 return script::projectResult(vm, self->setBuildingRally(*producer, command, groupedReinforcements));
4107 });
4108 cls.addFunc("linkBuildingRally",
4109 [vm](RTS* self, const std::string& producerText, const std::string& sourceText) -> ssq::Table {
4110 auto producerSubject = parseScriptSubject(producerText, "producer");
4111 auto sourceSubject = parseScriptSubject(sourceText, "source");
4112 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
4113 if (!sourceSubject) return script::projectStatusResult(vm, sourceSubject.status());
4114 Building* producer = self->findBuilding(producerSubject.value());
4115 Building* source = self->findBuilding(sourceSubject.value());
4116 if (producer == nullptr || source == nullptr)
4118 vm,
4120 DiagnosticCode::NotFound, "RTS rally producer or source was not found", "building")));
4121 return script::projectResult(vm, self->linkBuildingRally(*producer, *source));
4122 });
4123 cls.addFunc("clearBuildingRally", [vm](RTS* self, const std::string& producerText) -> ssq::Table {
4124 auto producerSubject = parseScriptSubject(producerText, "producer");
4125 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
4126 Building* producer = self->findBuilding(producerSubject.value());
4127 if (producer == nullptr)
4130 Diagnostic::error(DiagnosticCode::NotFound, "RTS rally producer was not found", "producer")));
4131 return script::projectResult(vm, self->clearBuildingRally(*producer));
4132 });
4133 cls.addFunc(
4134 "setReinforcementLimit", [vm](RTS* self, const std::string& producerText, std::int64_t maximum) -> ssq::Table {
4135 if (maximum < 0)
4138 "RTS reinforcement limit must be non-negative", "maximum")));
4139 auto producerSubject = parseScriptSubject(producerText, "producer");
4140 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
4141 Building* producer = self->findBuilding(producerSubject.value());
4142 if (producer == nullptr)
4145 "RTS reinforcement producer was not found", "producer")));
4146 return script::projectResult(vm, self->setReinforcementLimit(*producer, static_cast<std::size_t>(maximum)));
4147 });
4148 cls.addFunc("setReinforcementTypeLimit",
4149 [vm](RTS* self, const std::string& producerText, const std::string& unitType,
4150 std::int64_t maximum) -> ssq::Table {
4151 if (maximum < 0)
4154 "RTS reinforcement type limit must be non-negative",
4155 "maximum")));
4156 auto producerSubject = parseScriptSubject(producerText, "producer");
4157 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
4158 Building* producer = self->findBuilding(producerSubject.value());
4159 if (producer == nullptr)
4162 DiagnosticCode::NotFound, "RTS reinforcement producer was not found", "producer")));
4163 return script::projectResult(
4164 vm, self->setReinforcementTypeLimit(*producer, unitType, static_cast<std::size_t>(maximum)));
4165 });
4166 cls.addFunc(
4167 "setReinforcementTypePriority",
4168 [vm](RTS* self, const std::string& producerText, const std::string& unitType, int priority) -> ssq::Table {
4169 auto producerSubject = parseScriptSubject(producerText, "producer");
4170 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
4171 Building* producer = self->findBuilding(producerSubject.value());
4172 if (producer == nullptr)
4175 "RTS reinforcement producer was not found", "producer")));
4176 return script::projectResult(vm, self->setReinforcementTypePriority(*producer, unitType, priority));
4177 });
4178 cls.addFunc("setReinforcementFallback",
4179 [vm](RTS* self, const std::string& producerText, const std::string& preferred,
4180 const std::string& fallback) -> ssq::Table {
4181 auto producerSubject = parseScriptSubject(producerText, "producer");
4182 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
4183 Building* producer = self->findBuilding(producerSubject.value());
4184 if (producer == nullptr)
4187 DiagnosticCode::NotFound, "RTS reinforcement producer was not found", "producer")));
4188 return script::projectResult(vm, self->setReinforcementFallback(*producer, preferred, fallback));
4189 });
4190 cls.addFunc("setReinforcementAutoCancel",
4191 [vm](RTS* self, const std::string& producerText, float seconds) -> ssq::Table {
4192 auto producerSubject = parseScriptSubject(producerText, "producer");
4193 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
4194 Building* producer = self->findBuilding(producerSubject.value());
4195 if (producer == nullptr)
4198 DiagnosticCode::NotFound, "RTS reinforcement producer was not found", "producer")));
4199 return script::projectResult(vm, self->setReinforcementAutoCancel(*producer, seconds));
4200 });
4201 cls.addFunc(
4202 "setReinforcementTransport",
4203 [vm](RTS* self, const std::string& producerText, const std::string& transportText,
4204 std::int64_t minimumLoad) -> ssq::Table {
4205 if (minimumLoad < 0)
4208 "RTS reinforcement minimum load must be non-negative",
4209 "minimumLoad")));
4210 auto producerSubject = parseScriptSubject(producerText, "producer");
4211 if (!producerSubject) return script::projectStatusResult(vm, producerSubject.status());
4212 Building* producer = self->findBuilding(producerSubject.value());
4213 if (producer == nullptr)
4216 "RTS reinforcement producer was not found", "producer")));
4217 Unit* transport = nullptr;
4218 if (!transportText.empty()) {
4219 auto transportSubject = parseScriptSubject(transportText, "transport");
4220 if (!transportSubject) return script::projectStatusResult(vm, transportSubject.status());
4221 transport = self->findUnit(transportSubject.value());
4222 if (transport == nullptr)
4225 DiagnosticCode::NotFound, "RTS reinforcement transport was not found", "transport")));
4226 }
4227 return script::projectResult(
4228 vm, self->setReinforcementTransport(*producer, transport, static_cast<std::size_t>(minimumLoad)));
4229 });
4230 cls.addFunc("exportScriptCommandLog", [vm](RTS* self) -> ssq::Table {
4231 return script::projectResult(vm, self->exportScriptCommandLog(),
4232 [](std::string value) { return Value(std::move(value)); });
4233 });
4234 cls.addFunc("importScriptCommandLog", [vm](RTS* self, const std::string& text, bool clearExisting) -> ssq::Table {
4235 return script::projectResult(vm, self->importScriptCommandLog(text, clearExisting));
4236 });
4237 cls.addFunc("queueScriptConstructionCommand",
4238 [vm](RTS* self, std::int64_t tick, const std::string& factionText, const std::string& builderText,
4239 const std::string& buildingSubjectText, const std::string& definitionText, float x,
4240 float y) -> ssq::Table {
4241 if (tick < 0)
4244 "RTS replay tick must be non-negative", "tick")));
4245 auto faction = parseScriptSubject(factionText, "faction");
4246 auto builder = parseScriptSubject(builderText, "builder");
4247 auto result = parseScriptSubject(buildingSubjectText, "buildingSubject");
4248 auto definition = parseScriptDefinition(definitionText);
4249 if (!faction) return script::projectStatusResult(vm, faction.status());
4250 if (!builder) return script::projectStatusResult(vm, builder.status());
4251 if (!result) return script::projectStatusResult(vm, result.status());
4252 if (!definition) return script::projectStatusResult(vm, definition.status());
4253 RTSReplayCommand replay;
4254 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4255 replay.operation = RTSReplayOperation::Construction;
4256 replay.units = {builder.value()};
4257 replay.faction = faction.value();
4258 replay.resultSubject = result.value();
4259 replay.definition = definition.value();
4260 replay.point = {x, y};
4261 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4262 });
4263 cls.addFunc("queueScriptProductionCommand",
4264 [vm](RTS* self, std::int64_t tick, const std::string& producerText, const std::string& unitSubjectText,
4265 const std::string& definitionText, int priority) -> ssq::Table {
4266 if (tick < 0)
4269 "RTS replay tick must be non-negative", "tick")));
4270 auto producer = parseScriptSubject(producerText, "producer");
4271 auto result = parseScriptSubject(unitSubjectText, "unitSubject");
4272 auto definition = parseScriptDefinition(definitionText);
4273 if (!producer) return script::projectStatusResult(vm, producer.status());
4274 if (!result) return script::projectStatusResult(vm, result.status());
4275 if (!definition) return script::projectStatusResult(vm, definition.status());
4276 RTSReplayCommand replay;
4277 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4278 replay.operation = RTSReplayOperation::Production;
4279 replay.producer = producer.value();
4280 replay.resultSubject = result.value();
4281 replay.definition = definition.value();
4282 replay.priority = priority;
4283 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4284 });
4285 cls.addFunc("queueScriptReinforcementCommand",
4286 [vm](RTS* self, std::int64_t tick, const std::string& producerText, const std::string& unitSubjectText,
4287 const std::string& definitionText, int priority) -> ssq::Table {
4288 if (tick < 0)
4291 "RTS replay tick must be non-negative", "tick")));
4292 auto producer = parseScriptSubject(producerText, "producer");
4293 auto result = parseScriptSubject(unitSubjectText, "unitSubject");
4294 auto definition = parseScriptDefinition(definitionText);
4295 if (!producer) return script::projectStatusResult(vm, producer.status());
4296 if (!result) return script::projectStatusResult(vm, result.status());
4297 if (!definition) return script::projectStatusResult(vm, definition.status());
4298 RTSReplayCommand replay;
4299 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4300 replay.operation = RTSReplayOperation::ReinforcementProduction;
4301 replay.producer = producer.value();
4302 replay.resultSubject = result.value();
4303 replay.definition = definition.value();
4304 replay.priority = priority;
4305 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4306 });
4307 cls.addFunc("queueScriptResearchCommand",
4308 [vm](RTS* self, std::int64_t tick, const std::string& producerText, const std::string& upgrade,
4309 int priority) -> ssq::Table {
4310 if (tick < 0)
4313 "RTS replay tick must be non-negative", "tick")));
4314 auto producer = parseScriptSubject(producerText, "producer");
4315 if (!producer) return script::projectStatusResult(vm, producer.status());
4316 RTSReplayCommand replay;
4317 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4318 replay.operation = RTSReplayOperation::Research;
4319 replay.producer = producer.value();
4320 replay.value = upgrade;
4321 replay.priority = priority;
4322 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4323 });
4324 cls.addFunc("queueScriptAbilityCommand",
4325 [vm](RTS* self, std::int64_t tick, const std::string& casterText, const std::string& ability,
4326 const std::string& targetText, float x, float y) -> ssq::Table {
4327 if (tick < 0)
4330 "RTS replay tick must be non-negative", "tick")));
4331 auto caster = parseScriptSubject(casterText, "caster");
4332 if (!caster) return script::projectStatusResult(vm, caster.status());
4334 if (!targetText.empty()) {
4335 auto parsed = parseScriptSubject(targetText, "target");
4336 if (!parsed) return script::projectStatusResult(vm, parsed.status());
4337 target = parsed.value();
4338 }
4339 RTSReplayCommand replay;
4340 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4341 replay.operation = RTSReplayOperation::Ability;
4342 replay.units = {caster.value()};
4343 replay.value = ability;
4344 replay.targetEntity = target;
4345 replay.point = {x, y};
4346 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4347 });
4348 cls.addFunc("queueScriptCancelProductionCommand",
4349 [vm](RTS* self, std::int64_t tick, const std::string& producerText, int queueIndex) -> ssq::Table {
4350 if (tick < 0)
4353 "RTS replay tick must be non-negative", "tick")));
4354 auto producer = parseScriptSubject(producerText, "producer");
4355 if (!producer) return script::projectStatusResult(vm, producer.status());
4356 RTSReplayCommand replay;
4357 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4358 replay.operation = RTSReplayOperation::CancelProduction;
4359 replay.producer = producer.value();
4360 replay.priority = queueIndex;
4361 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4362 });
4363 cls.addFunc("queueScriptCancelAbilityCommand",
4364 [vm](RTS* self, std::int64_t tick, const std::vector<std::string>& subjectTexts) -> ssq::Table {
4365 if (tick < 0)
4368 "RTS replay tick must be non-negative", "tick")));
4369 auto subjects = parseScriptSubjects(subjectTexts);
4370 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4371 RTSReplayCommand replay;
4372 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4373 replay.operation = RTSReplayOperation::CancelAbility;
4374 replay.units = std::move(subjects).takeValue();
4375 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4376 });
4377 cls.addFunc("queueScriptFireSupportCommand",
4378 [vm](RTS* self, std::int64_t tick, const std::string& requesterText, float x, float y, float radius,
4379 int shotsPerResponder, int maxResponders) -> ssq::Table {
4380 if (tick < 0 || maxResponders < 0)
4384 "RTS fire-support tick and responder limit must be non-negative", "fireSupport")));
4385 auto requester = parseScriptSubject(requesterText, "requester");
4386 if (!requester) return script::projectStatusResult(vm, requester.status());
4387 RTSReplayCommand replay;
4388 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4389 replay.operation = RTSReplayOperation::RequestFireSupport;
4390 replay.producer = requester.value();
4391 replay.point = {x, y};
4392 replay.command.radius = radius;
4393 replay.priority = shotsPerResponder;
4394 replay.limit = static_cast<std::size_t>(maxResponders);
4395 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4396 });
4397 cls.addFunc("queueScriptCancelFireSupportCommand",
4398 [vm](RTS* self, std::int64_t tick, const std::string& requesterText) -> ssq::Table {
4399 if (tick < 0)
4402 "RTS replay tick must be non-negative", "tick")));
4403 auto requester = parseScriptSubject(requesterText, "requester");
4404 if (!requester) return script::projectStatusResult(vm, requester.status());
4405 RTSReplayCommand replay;
4406 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4407 replay.operation = RTSReplayOperation::CancelFireSupport;
4408 replay.producer = requester.value();
4409 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4410 });
4411 const auto queueBuildingLifecycle = [vm](RTS* self, std::int64_t tick, const std::string& buildingText,
4412 RTSReplayOperation operation) -> ssq::Table {
4413 if (tick < 0)
4416 "RTS replay tick must be non-negative", "tick")));
4417 auto building = parseScriptSubject(buildingText, "building");
4418 if (!building) return script::projectStatusResult(vm, building.status());
4419 RTSReplayCommand replay;
4420 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4421 replay.operation = operation;
4422 replay.producer = building.value();
4423 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4424 };
4425 cls.addFunc("queueScriptCancelConstructionCommand",
4426 [queueBuildingLifecycle](RTS* self, std::int64_t tick, const std::string& buildingText) -> ssq::Table {
4427 return queueBuildingLifecycle(self, tick, buildingText, RTSReplayOperation::CancelConstruction);
4428 });
4429 cls.addFunc("queueScriptSellBuildingCommand",
4430 [queueBuildingLifecycle](RTS* self, std::int64_t tick, const std::string& buildingText) -> ssq::Table {
4431 return queueBuildingLifecycle(self, tick, buildingText, RTSReplayOperation::SellBuilding);
4432 });
4433 cls.addFunc("queueScriptMove",
4434 [vm](RTS* self, std::int64_t tick, const std::vector<std::string>& subjectTexts, float x,
4435 float y) -> ssq::Table {
4436 if (tick < 0)
4439 "RTS replay tick must be non-negative", "tick")));
4440 auto subjects = parseScriptSubjects(subjectTexts);
4441 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4442 RTSReplayCommand replay;
4443 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4444 replay.units = std::move(subjects).takeValue();
4445 replay.command.kind = OrderKind::Move;
4446 replay.command.target = {x, y};
4447 replay.formation.kind = FormationKind::Grid;
4448 replay.formation.spacing = 1.0f;
4449 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4450 });
4451 cls.addFunc("queueScriptAttackMove",
4452 [vm](RTS* self, std::int64_t tick, const std::vector<std::string>& subjectTexts, float x,
4453 float y) -> ssq::Table {
4454 if (tick < 0)
4457 "RTS replay tick must be non-negative", "tick")));
4458 auto subjects = parseScriptSubjects(subjectTexts);
4459 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4460 RTSReplayCommand replay;
4461 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4462 replay.units = std::move(subjects).takeValue();
4463 replay.command.kind = OrderKind::AttackMove;
4464 replay.command.target = {x, y};
4465 replay.formation.kind = FormationKind::Grid;
4466 replay.formation.spacing = 1.0f;
4467 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4468 });
4469 cls.addFunc("queueScriptAttack",
4470 [vm](RTS* self, std::int64_t tick, const std::vector<std::string>& subjectTexts,
4471 const std::string& targetText) -> ssq::Table {
4472 if (tick < 0)
4475 "RTS replay tick must be non-negative", "tick")));
4476 auto subjects = parseScriptSubjects(subjectTexts);
4477 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4478 auto targetSubject = parseScriptSubject(targetText, "target");
4479 if (!targetSubject) return script::projectStatusResult(vm, targetSubject.status());
4480 ecs::Entity* target = self->findUnit(targetSubject.value());
4481 if (target == nullptr) target = self->findBuilding(targetSubject.value());
4482 if (target == nullptr)
4485 DiagnosticCode::NotFound, "RTS replay target identity was not found", "target")));
4486 RTSReplayCommand replay;
4487 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4488 replay.units = std::move(subjects).takeValue();
4489 replay.command.kind = OrderKind::Attack;
4490 replay.command.targetEntity = ecs::handle_of(target);
4491 replay.targetEntity = targetSubject.value();
4492 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4493 });
4494 cls.addFunc("queueScriptSuppressAreaCommand",
4495 [vm](RTS* self, std::int64_t tick, const std::vector<std::string>& subjectTexts, float startX,
4496 float startY, float endX, float endY, float width, int shotsPerUnit) -> ssq::Table {
4497 if (tick < 0)
4500 "RTS replay tick must be non-negative", "tick")));
4501 auto subjects = parseScriptSubjects(subjectTexts);
4502 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4503 RTSReplayCommand replay;
4504 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4505 replay.operation = RTSReplayOperation::SuppressArea;
4506 replay.units = std::move(subjects).takeValue();
4507 replay.command.kind = OrderKind::SuppressArea;
4508 replay.command.target = {startX, startY};
4509 replay.command.secondaryTarget = {endX, endY};
4510 replay.command.radius = width;
4511 replay.priority = shotsPerUnit;
4512 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4513 });
4514 cls.addFunc("queueScriptEscortCommand",
4515 [vm](RTS* self, std::int64_t tick, const std::vector<std::string>& subjectTexts,
4516 const std::string& targetText, float guardRadius, float spacing) -> ssq::Table {
4517 if (tick < 0)
4520 "RTS replay tick must be non-negative", "tick")));
4521 auto subjects = parseScriptSubjects(subjectTexts);
4522 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4523 auto target = parseScriptSubject(targetText, "target");
4524 if (!target) return script::projectStatusResult(vm, target.status());
4525 RTSReplayCommand replay;
4526 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4527 replay.operation = RTSReplayOperation::Escort;
4528 replay.units = std::move(subjects).takeValue();
4529 replay.targetEntity = target.value();
4530 replay.command.kind = OrderKind::Escort;
4531 replay.command.radius = guardRadius;
4532 replay.formation.spacing = spacing;
4533 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4534 });
4535 const auto queueUnitOrder = [vm](RTS* self, std::int64_t tick, const std::vector<std::string>& subjectTexts,
4536 OrderKind kind, WorldPosition point, SubjectRef target, bool append,
4537 float formationSpacing) -> ssq::Table {
4538 if (tick < 0)
4541 "RTS replay tick must be non-negative", "tick")));
4542 if (formationSpacing == 0.0f || !std::isfinite(formationSpacing))
4545 "RTS replay formation spacing must be positive", "spacing")));
4546 auto subjects = parseScriptSubjects(subjectTexts);
4547 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4548 RTSReplayCommand replay;
4549 replay.tick = SimulationTick{static_cast<std::uint64_t>(tick)};
4550 replay.units = std::move(subjects).takeValue();
4551 replay.command.kind = kind;
4552 replay.command.target = point;
4553 replay.command.append = append;
4554 replay.targetEntity = target;
4555 if (formationSpacing > 0.0f) {
4556 replay.formation.kind = FormationKind::Grid;
4557 replay.formation.spacing = formationSpacing;
4558 }
4559 return script::projectResult(vm, self->queueScriptCommand(std::move(replay)));
4560 };
4561 cls.addFunc("queueScriptStopCommand",
4562 [queueUnitOrder](RTS* self, std::int64_t tick, const std::vector<std::string>& subjects) -> ssq::Table {
4563 return queueUnitOrder(self, tick, subjects, OrderKind::Stop, {}, {}, false, -1.0f);
4564 });
4565 cls.addFunc("queueScriptHoldCommand",
4566 [queueUnitOrder](RTS* self, std::int64_t tick, const std::vector<std::string>& subjects) -> ssq::Table {
4567 return queueUnitOrder(self, tick, subjects, OrderKind::HoldPosition, {}, {}, false, -1.0f);
4568 });
4569 cls.addFunc("queueScriptPatrolCommand",
4570 [queueUnitOrder](RTS* self, std::int64_t tick, const std::vector<std::string>& subjects, float x,
4571 float y) -> ssq::Table {
4572 return queueUnitOrder(self, tick, subjects, OrderKind::Patrol, {x, y}, {}, false, -1.0f);
4573 });
4574 cls.addFunc("queueScriptAttackGroundCommand",
4575 [queueUnitOrder](RTS* self, std::int64_t tick, const std::vector<std::string>& subjects, float x,
4576 float y, bool append) -> ssq::Table {
4577 return queueUnitOrder(self, tick, subjects, OrderKind::AttackGround, {x, y}, {}, append, -1.0f);
4578 });
4579 cls.addFunc(
4580 "queueScriptAppendMoveCommand",
4581 [vm, queueUnitOrder](RTS* self, std::int64_t tick, const std::vector<std::string>& subjects, float x, float y,
4582 float spacing) -> ssq::Table {
4583 if (!std::isfinite(spacing) || spacing <= 0.0f)
4586 "RTS replay formation spacing must be positive", "spacing")));
4587 return queueUnitOrder(self, tick, subjects, OrderKind::Move, {x, y}, {}, true, spacing);
4588 });
4589 cls.addFunc(
4590 "queueScriptAppendAttackMoveCommand",
4591 [vm, queueUnitOrder](RTS* self, std::int64_t tick, const std::vector<std::string>& subjects, float x, float y,
4592 float spacing) -> ssq::Table {
4593 if (!std::isfinite(spacing) || spacing <= 0.0f)
4596 "RTS replay formation spacing must be positive", "spacing")));
4597 return queueUnitOrder(self, tick, subjects, OrderKind::AttackMove, {x, y}, {}, true, spacing);
4598 });
4599 const auto queueEntityOrder = [vm, queueUnitOrder](RTS* self, std::int64_t tick,
4600 const std::vector<std::string>& subjects,
4601 const std::string& targetText, OrderKind kind) -> ssq::Table {
4602 auto target = parseScriptSubject(targetText, "target");
4603 if (!target) return script::projectStatusResult(vm, target.status());
4604 return queueUnitOrder(self, tick, subjects, kind, {}, target.value(), false, -1.0f);
4605 };
4606 cls.addFunc("queueScriptRepairCommand",
4607 [queueEntityOrder](RTS* self, std::int64_t tick, const std::vector<std::string>& subjects,
4608 const std::string& target) -> ssq::Table {
4609 return queueEntityOrder(self, tick, subjects, target, OrderKind::Repair);
4610 });
4611 cls.addFunc("queueScriptCaptureCommand",
4612 [queueEntityOrder](RTS* self, std::int64_t tick, const std::vector<std::string>& subjects,
4613 const std::string& target) -> ssq::Table {
4614 return queueEntityOrder(self, tick, subjects, target, OrderKind::Capture);
4615 });
4616 cls.addFunc("queueScriptGarrisonCommand",
4617 [queueEntityOrder](RTS* self, std::int64_t tick, const std::vector<std::string>& subjects,
4618 const std::string& target) -> ssq::Table {
4619 return queueEntityOrder(self, tick, subjects, target, OrderKind::Garrison);
4620 });
4621 cls.addFunc("queueScriptBoardTransportCommand",
4622 [queueEntityOrder](RTS* self, std::int64_t tick, const std::vector<std::string>& subjects,
4623 const std::string& target) -> ssq::Table {
4624 return queueEntityOrder(self, tick, subjects, target, OrderKind::BoardTransport);
4625 });
4626 script_internal::exposeFormationBindings(cls);
4627 cls.addFunc("attackUnits",
4628 [vm](RTS* self, const std::vector<std::string>& subjectTexts, const std::string& targetText,
4629 bool append) -> ssq::Table {
4630 auto subjects = parseScriptSubjects(subjectTexts);
4631 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4632 auto targetSubject = parseScriptSubject(targetText, "target");
4633 if (!targetSubject) return script::projectStatusResult(vm, targetSubject.status());
4634 ecs::Entity* target = self->findUnit(targetSubject.value());
4635 if (target == nullptr) target = self->findBuilding(targetSubject.value());
4636 if (target == nullptr)
4639 DiagnosticCode::NotFound, "RTS attack target identity was not found", "target")));
4640 CommandSpec command;
4641 command.kind = OrderKind::Attack;
4642 command.targetEntity = ecs::handle_of(target);
4643 command.append = append;
4644 return script::projectResult(vm, self->commandUnits(std::move(subjects).takeValue(), command),
4645 fanOutValue);
4646 });
4647 cls.addFunc(
4648 "attackGroundUnits",
4649 [vm](RTS* self, const std::vector<std::string>& subjectTexts, float x, float y, bool append) -> ssq::Table {
4650 auto subjects = parseScriptSubjects(subjectTexts);
4651 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4652 CommandSpec command;
4653 command.kind = OrderKind::AttackGround;
4654 command.target = {x, y};
4655 command.append = append;
4656 return script::projectResult(vm, self->commandUnits(std::move(subjects).takeValue(), command), fanOutValue);
4657 });
4658 cls.addFunc("suppressAreaUnits",
4659 [vm](RTS* self, const std::vector<std::string>& subjectTexts, float startX, float startY, float endX,
4660 float endY, float width, int shotsPerUnit) -> ssq::Table {
4661 auto subjects = parseScriptSubjects(subjectTexts);
4662 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4664 self->suppressArea(std::move(subjects).takeValue(), {startX, startY},
4665 {endX, endY}, width, shotsPerUnit),
4666 fanOutValue);
4667 });
4668 cls.addFunc("escortUnits",
4669 [vm](RTS* self, const std::vector<std::string>& subjectTexts, const std::string& targetText,
4670 float guardRadius, float spacing) -> ssq::Table {
4671 auto subjects = parseScriptSubjects(subjectTexts);
4672 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4673 auto target = parseScriptSubject(targetText, "target");
4674 if (!target) return script::projectStatusResult(vm, target.status());
4675 return script::projectResult(
4676 vm, self->escortUnits(std::move(subjects).takeValue(), target.value(), guardRadius, spacing),
4677 fanOutValue);
4678 });
4679 cls.addFunc("setUnitStance",
4680 [vm](RTS* self, const std::vector<std::string>& subjectTexts, const std::string& stanceText,
4681 float leashRange) -> ssq::Table {
4682 auto subjects = parseScriptSubjects(subjectTexts);
4683 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4684 auto stance = parseCombatStance(stanceText);
4685 if (!stance) return script::projectStatusResult(vm, stance.status());
4686 return script::projectResult(
4687 vm, self->setUnitStance(std::move(subjects).takeValue(), stance.value(), leashRange));
4688 });
4689 cls.addFunc("setUnitMovementPriority",
4690 [vm](RTS* self, const std::vector<std::string>& subjectTexts, int priority) -> ssq::Table {
4691 auto subjects = parseScriptSubjects(subjectTexts);
4692 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4693 return script::projectResult(
4694 vm, self->setUnitMovementPriority(std::move(subjects).takeValue(), priority));
4695 });
4696 cls.addFunc("assignWorker",
4697 [vm](RTS* self, const std::string& workerText, const std::string& nodeText,
4698 const std::string& dropoffText) -> ssq::Table {
4699 auto workerSubject = parseScriptSubject(workerText, "worker");
4700 if (!workerSubject) return script::projectStatusResult(vm, workerSubject.status());
4701 auto nodeSubject = parseScriptSubject(nodeText, "node");
4702 if (!nodeSubject) return script::projectStatusResult(vm, nodeSubject.status());
4703 auto dropoffSubject = parseScriptSubject(dropoffText, "dropoff");
4704 if (!dropoffSubject) return script::projectStatusResult(vm, dropoffSubject.status());
4705 auto* worker = self->findUnit(workerSubject.value());
4706 auto* node = self->findResourceNode(nodeSubject.value());
4707 auto* dropoff = self->findBuilding(dropoffSubject.value());
4708 if (worker == nullptr || node == nullptr || dropoff == nullptr)
4710 vm,
4712 DiagnosticCode::NotFound, "RTS worker assignment root was not found", "assignment")));
4713 return script::projectResult(vm, self->assignWorker(*worker, *node, *dropoff));
4714 });
4715 cls.addFunc("setWorkerAutoAssignment",
4716 [vm](RTS* self, const std::vector<std::string>& subjectTexts, bool enabled) -> ssq::Table {
4717 auto subjects = parseScriptSubjects(subjectTexts);
4718 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4719 return script::projectResult(
4720 vm, self->setWorkerAutoAssignment(std::move(subjects).takeValue(), enabled));
4721 });
4722 cls.addFunc("configureAutoConstruction",
4723 [vm](RTS* self, const std::string& factionText, bool enabled, int maxBuildersPerSite,
4724 int reserveWorkers) -> ssq::Table {
4725 auto subject = parseScriptSubject(factionText, "faction");
4726 if (!subject) return script::projectStatusResult(vm, subject.status());
4727 auto* faction = self->findFaction(subject.value());
4728 if (faction == nullptr)
4731 "RTS workforce faction was not found", "faction")));
4732 const auto workforce = faction->workforce();
4733 return script::projectResult(
4734 vm,
4735 self->configureWorkforce(*faction, enabled, maxBuildersPerSite, workforce->autoRepair,
4736 static_cast<int>(workforce->maxRepairersPerBuilding), reserveWorkers));
4737 });
4738 cls.addFunc("configureAutoRepair",
4739 [vm](RTS* self, const std::string& factionText, bool enabled, int maxRepairersPerBuilding,
4740 int reserveWorkers) -> ssq::Table {
4741 auto subject = parseScriptSubject(factionText, "faction");
4742 if (!subject) return script::projectStatusResult(vm, subject.status());
4743 auto* faction = self->findFaction(subject.value());
4744 if (faction == nullptr)
4747 "RTS workforce faction was not found", "faction")));
4748 const auto workforce = faction->workforce();
4749 return script::projectResult(
4750 vm, self->configureWorkforce(*faction, workforce->autoConstruction,
4751 static_cast<int>(workforce->maxBuildersPerSite), enabled,
4752 maxRepairersPerBuilding, reserveWorkers));
4753 });
4754 cls.addFunc(
4755 "assignBuilder", [vm](RTS* self, const std::string& workerText, const std::string& buildingText) -> ssq::Table {
4756 auto workerSubject = parseScriptSubject(workerText, "worker");
4757 if (!workerSubject) return script::projectStatusResult(vm, workerSubject.status());
4758 auto buildingSubject = parseScriptSubject(buildingText, "building");
4759 if (!buildingSubject) return script::projectStatusResult(vm, buildingSubject.status());
4760 auto* worker = self->findUnit(workerSubject.value());
4761 auto* building = self->findBuilding(buildingSubject.value());
4762 if (worker == nullptr || building == nullptr)
4765 "RTS builder assignment root was not found", "assignment")));
4766 return script::projectResult(vm, self->assignBuilder(*worker, *building),
4767 [](std::string id) { return Value(std::move(id)); });
4768 });
4769 cls.addFunc("addUnitReserveAmmo", [vm](RTS* self, const std::string& subjectText, int rounds) -> ssq::Table {
4770 auto subject = parseScriptSubject(subjectText, "unit");
4771 if (!subject) return script::projectStatusResult(vm, subject.status());
4772 auto* unit = self->findUnit(subject.value());
4773 if (unit == nullptr)
4776 "RTS reserve-ammunition unit was not found", "unit")));
4777 return script::projectResult(vm, self->addUnitReserveAmmo(*unit, rounds),
4778 [](int value) { return Value(static_cast<std::int64_t>(value)); });
4779 });
4780 cls.addFunc("addUnitAmmoSupply", [vm](RTS* self, const std::string& subjectText, float rounds) -> ssq::Table {
4781 auto subject = parseScriptSubject(subjectText, "unit");
4782 if (!subject) return script::projectStatusResult(vm, subject.status());
4783 auto* unit = self->findUnit(subject.value());
4784 if (unit == nullptr)
4787 "RTS ammunition-supply unit was not found", "unit")));
4788 return script::projectResult(vm, self->addUnitAmmoSupply(*unit, rounds),
4789 [](float value) { return Value(static_cast<double>(value)); });
4790 });
4791 cls.addFunc("addBuildingAmmoSupply", [vm](RTS* self, const std::string& subjectText, float rounds) -> ssq::Table {
4792 auto subject = parseScriptSubject(subjectText, "building");
4793 if (!subject) return script::projectStatusResult(vm, subject.status());
4794 auto* building = self->findBuilding(subject.value());
4795 if (building == nullptr)
4798 "RTS ammunition-supply building was not found", "building")));
4799 return script::projectResult(vm, self->addBuildingAmmoSupply(*building, rounds),
4800 [](float value) { return Value(static_cast<double>(value)); });
4801 });
4802 cls.addFunc("setUnitAutoResupply", [vm](RTS* self, const std::string& subjectText, bool enabled) -> ssq::Table {
4803 auto subject = parseScriptSubject(subjectText, "unit");
4804 if (!subject) return script::projectStatusResult(vm, subject.status());
4805 auto* unit = self->findUnit(subject.value());
4806 if (unit == nullptr)
4809 "RTS automatic-resupply unit was not found", "unit")));
4810 return script::projectResult(vm, self->setUnitAutoResupply(*unit, enabled));
4811 });
4812 cls.addFunc(
4813 "heal",
4814 [vm](RTS* self, const std::string& sourceText, const std::string& targetText, float amount) -> ssq::Table {
4815 auto source = parseScriptSubject(sourceText, "source");
4816 if (!source) return script::projectStatusResult(vm, source.status());
4817 auto target = parseScriptSubject(targetText, "target");
4818 if (!target) return script::projectStatusResult(vm, target.status());
4819 return script::projectResult(vm, self->heal(source.value(), target.value(), static_cast<double>(amount)),
4820 [](double applied) { return Value(applied); });
4821 });
4822 cls.addFunc(
4823 "applyStatusEffect",
4824 [vm](RTS* self, const std::string& sourceText, const std::string& targetText, const std::string& effect,
4825 float durationOverride) -> ssq::Table {
4826 auto source = parseScriptSubject(sourceText, "source");
4827 if (!source) return script::projectStatusResult(vm, source.status());
4828 auto target = parseScriptSubject(targetText, "target");
4829 if (!target) return script::projectStatusResult(vm, target.status());
4830 return script::projectResult(
4831 vm,
4832 self->applyStatusEffect(source.value(), target.value(), effect, static_cast<double>(durationOverride)),
4833 [](effects::EffectHandle handle) {
4834 return Value(Value::Object{{"instanceId", std::move(handle.instanceId)},
4835 {"generation", static_cast<std::int64_t>(handle.containerGeneration)}});
4836 });
4837 });
4838 cls.addFunc(
4839 "requestFireSupport",
4840 [vm](RTS* self, const std::string& requesterText, float x, float y, float radius, int shotsPerResponder,
4841 int maxResponders) -> ssq::Table {
4842 if (maxResponders < 0)
4845 "RTS fire-support responder limit must be non-negative",
4846 "maxResponders")));
4847 auto requesterSubject = parseScriptSubject(requesterText, "requester");
4848 if (!requesterSubject) return script::projectStatusResult(vm, requesterSubject.status());
4849 auto* requester = self->findUnit(requesterSubject.value());
4850 if (requester == nullptr)
4853 "RTS fire-support requester was not found", "requester")));
4855 self->requestFireSupport(*requester, {x, y}, radius, shotsPerResponder,
4856 static_cast<std::size_t>(maxResponders)),
4857 [](std::size_t count) { return Value(static_cast<std::int64_t>(count)); });
4858 });
4859 cls.addFunc("cancelFireSupport", [vm](RTS* self, const std::string& requesterText) -> ssq::Table {
4860 auto requesterSubject = parseScriptSubject(requesterText, "requester");
4861 if (!requesterSubject) return script::projectStatusResult(vm, requesterSubject.status());
4862 auto* requester = self->findUnit(requesterSubject.value());
4863 if (requester == nullptr)
4866 "RTS fire-support requester was not found", "requester")));
4867 return script::projectResult(vm, self->cancelFireSupport(*requester),
4868 [](std::size_t count) { return Value(static_cast<std::int64_t>(count)); });
4869 });
4870 cls.addFunc(
4871 "patrolUnits",
4872 [vm](RTS* self, const std::vector<std::string>& subjectTexts, float x, float y, float spacing) -> ssq::Table {
4873 auto subjects = parseScriptSubjects(subjectTexts);
4874 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4875 CommandSpec command;
4876 command.kind = OrderKind::Patrol;
4877 command.target = {x, y};
4878 FormationSpec formation;
4879 formation.kind = FormationKind::Grid;
4880 formation.spacing = spacing;
4881 return script::projectResult(vm, self->commandUnits(std::move(subjects).takeValue(), command, formation),
4882 fanOutValue);
4883 });
4884 cls.addFunc(
4885 "repairUnits",
4886 [vm](RTS* self, const std::vector<std::string>& subjectTexts, const std::string& buildingText,
4887 bool append) -> ssq::Table {
4888 auto subjects = parseScriptSubjects(subjectTexts);
4889 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4890 auto targetSubject = parseScriptSubject(buildingText, "building");
4891 if (!targetSubject) return script::projectStatusResult(vm, targetSubject.status());
4892 Building* building = self->findBuilding(targetSubject.value());
4893 if (building == nullptr)
4896 "RTS repair building identity was not found", "building")));
4897 CommandSpec command;
4898 command.kind = OrderKind::Repair;
4899 command.targetEntity = ecs::handle_of(building);
4900 command.target = {building->placement()->worldX, building->placement()->worldY};
4901 command.append = append;
4902 return script::projectResult(vm, self->commandUnits(std::move(subjects).takeValue(), command), fanOutValue);
4903 });
4904 cls.addFunc(
4905 "captureUnits",
4906 [vm](RTS* self, const std::vector<std::string>& subjectTexts, const std::string& buildingText,
4907 bool append) -> ssq::Table {
4908 auto subjects = parseScriptSubjects(subjectTexts);
4909 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4910 auto targetSubject = parseScriptSubject(buildingText, "building");
4911 if (!targetSubject) return script::projectStatusResult(vm, targetSubject.status());
4912 Building* building = self->findBuilding(targetSubject.value());
4913 if (building == nullptr)
4916 "RTS capture building identity was not found", "building")));
4917 CommandSpec command;
4918 command.kind = OrderKind::Capture;
4919 command.targetEntity = ecs::handle_of(building);
4920 command.target = {building->placement()->worldX, building->placement()->worldY};
4921 command.append = append;
4922 return script::projectResult(vm, self->commandUnits(std::move(subjects).takeValue(), command), fanOutValue);
4923 });
4924 cls.addFunc(
4925 "garrisonUnits",
4926 [vm](RTS* self, const std::vector<std::string>& subjectTexts, const std::string& buildingText,
4927 bool append) -> ssq::Table {
4928 auto subjects = parseScriptSubjects(subjectTexts);
4929 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4930 auto targetSubject = parseScriptSubject(buildingText, "building");
4931 if (!targetSubject) return script::projectStatusResult(vm, targetSubject.status());
4932 Building* building = self->findBuilding(targetSubject.value());
4933 if (building == nullptr)
4936 "RTS garrison building identity was not found", "building")));
4937 CommandSpec command;
4938 command.kind = OrderKind::Garrison;
4939 command.targetEntity = ecs::handle_of(building);
4940 command.target = {building->placement()->worldX, building->placement()->worldY};
4941 command.append = append;
4942 return script::projectResult(vm, self->commandUnits(std::move(subjects).takeValue(), command), fanOutValue);
4943 });
4944 cls.addFunc("boardTransportUnits",
4945 [vm](RTS* self, const std::vector<std::string>& subjectTexts, const std::string& transportText,
4946 bool append) -> ssq::Table {
4947 auto subjects = parseScriptSubjects(subjectTexts);
4948 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4949 auto targetSubject = parseScriptSubject(transportText, "transport");
4950 if (!targetSubject) return script::projectStatusResult(vm, targetSubject.status());
4951 Unit* transport = self->findUnit(targetSubject.value());
4952 if (transport == nullptr)
4955 DiagnosticCode::NotFound, "RTS transport identity was not found", "transport")));
4956 CommandSpec command;
4957 command.kind = OrderKind::BoardTransport;
4958 command.targetEntity = ecs::handle_of(transport);
4959 command.target = {transport->motion()->x, transport->motion()->y};
4960 command.append = append;
4961 return script::projectResult(vm, self->commandUnits(std::move(subjects).takeValue(), command),
4962 fanOutValue);
4963 });
4964 cls.addFunc("unloadTransport", [vm](RTS* self, const std::string& transportText, float x, float y) -> ssq::Table {
4965 auto subjectValue = parseScriptSubject(transportText, "transport");
4966 if (!subjectValue) return script::projectStatusResult(vm, subjectValue.status());
4967 Unit* transport = self->findUnit(subjectValue.value());
4968 if (transport == nullptr)
4970 vm, Status::failure(Diagnostic::error(DiagnosticCode::NotFound, "RTS transport identity was not found",
4971 "transport")));
4972 return script::projectResult(vm, self->unloadTransport(*transport, {x, y}),
4973 [](std::size_t released) { return Value(static_cast<std::int64_t>(released)); });
4974 });
4975 cls.addFunc("evacuateBuilding", [vm](RTS* self, const std::string& buildingText, float x, float y) -> ssq::Table {
4976 auto subjectValue = parseScriptSubject(buildingText, "building");
4977 if (!subjectValue) return script::projectStatusResult(vm, subjectValue.status());
4978 Building* building = self->findBuilding(subjectValue.value());
4979 if (building == nullptr)
4981 vm, Status::failure(Diagnostic::error(DiagnosticCode::NotFound, "RTS building identity was not found",
4982 "building")));
4983 return script::projectResult(vm, self->evacuateBuilding(*building, {x, y}),
4984 [](std::size_t released) { return Value(static_cast<std::int64_t>(released)); });
4985 });
4986 cls.addFunc("setUnitCloaked", [vm](RTS* self, const std::string& unitText, bool cloaked) -> ssq::Table {
4987 auto subjectValue = parseScriptSubject(unitText, "unit");
4988 if (!subjectValue) return script::projectStatusResult(vm, subjectValue.status());
4989 Unit* unit = self->findUnit(subjectValue.value());
4990 if (unit == nullptr)
4993 Diagnostic::error(DiagnosticCode::NotFound, "RTS unit identity was not found", "unit")));
4994 return script::projectResult(vm, self->setUnitCloaked(*unit, cloaked));
4995 });
4996 cls.addFunc("stopUnits", [vm](RTS* self, const std::vector<std::string>& subjectTexts) -> ssq::Table {
4997 auto subjects = parseScriptSubjects(subjectTexts);
4998 if (!subjects) return script::projectStatusResult(vm, subjects.status());
4999 CommandSpec command;
5000 command.kind = OrderKind::Stop;
5001 return script::projectResult(vm, self->commandUnits(std::move(subjects).takeValue(), command), fanOutValue);
5002 });
5003 cls.addFunc("holdUnits", [vm](RTS* self, const std::vector<std::string>& subjectTexts) -> ssq::Table {
5004 auto subjects = parseScriptSubjects(subjectTexts);
5005 if (!subjects) return script::projectStatusResult(vm, subjects.status());
5006 CommandSpec command;
5007 command.kind = OrderKind::HoldPosition;
5008 return script::projectResult(vm, self->commandUnits(std::move(subjects).takeValue(), command), fanOutValue);
5009 });
5010}
5011
5012} // namespace eve::rts
LogicalId target
ActionParameterOperation operation
double value
Duration start
Renderer- and ruleset-neutral gameplay action pipeline.
bool & active
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float duration
int subject
Definition AnimSmr.cpp:163
std::string from
std::vector< QuestEvent > pending
building::EdgeCurveGroup group
int priority
float phase
Definition CaveMesh.cpp:58
eve::resource::CostSpec cost
eve::resource::IResourceAccount * account
HSQUIRRELVM vm
Definition ECS.cpp:20
HSQOBJECT cls
Definition ECS.cpp:21
Resource-account adapter backed by an EconomyLedger.
std::array< std::uint8_t, 32 > hash
Definition Evpack.cpp:172
float maximum[3]
const GltfImportRequest & request
std::uint32_t alive
std::uint32_t capacity
std::uint32_t key
wgpu::PopErrorScopeStatus status
HexVec3 left
HexVec3 right
float elevation
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
std::uint32_t height
std::uint32_t width
JobScope scope
std::string text
TokenKind kind
Range range
std::array< float, 3 > position
std::uint64_t bytes
std::string name
bool valid
float distance
std::vector< std::int32_t > order
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:26
graphics::Canvas * previous
size_t queued
Definition OnnxGpgpu.cpp:60
std::unique_ptr< gpgpu::Sequence > sequence
Definition OnnxGpgpu.cpp:43
std::unique_ptr< ParticleEffect > effect
float radius
std::string action
Definition PlayHost.cpp:117
std::vector< ActionSpec > actions
Definition PlayHost.cpp:126
std::string id
Definition PlayHost.cpp:108
std::uint32_t seed
Definition PointSet.cpp:807
PrimitiveHandle handle
std::string taskId
float t
Selective combat-stat adapter for RTS units.
RTS module owner and phase-one composition profile entry point.
const RoadNode * node
double number
bool found
int created
int removed
double current
std::string resource
float dy
float dx
std::uint32_t count
The single Squirrel projection for common Result, Status and Value.
Cell cell
TacticalUnit * unit
bool placed
Battle::Events events
std::vector< UnitCandidate > units
SimulationTick tick
std::optional< InteractionSession > session
Definition Tactics.cpp:39
int spacing
bool visible
Json object
float step
Definition TreeMesh.cpp:314
uint32_t index
const UnitySourceAsset & source
std::vector< VegetationPresetCommand > commands
glm::vec3 point
Strongly typed Weapon definition/runtime adapter.
float angle
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
Signed, fixed-resolution simulation duration in nanoseconds.
Definition Time.h:43
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
Definition Time.cpp:16
Human-readable, scoped identifier in the form namespace:name.
Definition Identity.h:411
static std::optional< LogicalId > fromParts(std::string_view namespaceName, std::string_view name)
Builds a logical ID from separate namespace and name components.
Definition Identity.cpp:48
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
bool isValid() const noexcept
Returns whether this value contains a valid namespace and name.
Definition Identity.h:433
std::string_view name() const noexcept
Returns the name component as a view into this object.
Definition Identity.cpp:63
void value() const
Mark a successful void operation as checked.
Definition Result.h:526
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
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
static Status failure(StatusCode code, Diagnostic diagnostic)
Construct a failed status with one diagnostic.
Definition Status.h:84
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
static SubjectRef fromPersistentId(PersistentId id) noexcept
Wrap a persistent identity without changing its bytes.
Definition SubjectRef.h:32
std::string format() const
Return the canonical lower-case UUID spelling.
Definition SubjectRef.h:44
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
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
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
Type type() const noexcept
Return the active value kind.
Definition Value.h:83
Deterministic action lifecycle coordinator.
Definition Action.h:485
Owner-thread deterministic damage coordinator.
Definition Damage.h:125
Result< void > configureSettlementRules(const settlement::SettlementRuleSet &rules)
Replace the optional declarative buff/effect rules used by subsequent damage settlements.
Definition Damage.cpp:279
Result< settlement::SettlementResult > heal(CombatState &target, SubjectRef source, double amount) const
Resolve and atomically commit health restoration through the configured settlement pipeline.
Definition Damage.cpp:262
群体行为模块:连续流场寻路 + 海量单位移动/转向/行动 + Boids 鸟群。
Definition Crowd.h:116
Deterministic registry of topic-neutral, versioned JSON definitions.
Definition Definitions.h:58
eve::ResultRef< const Definition > resolve(const std::string &type, const std::string &id) const
Resolves a definition through the common registry API.
Id128 public API.
Definition Identity.h:112
std::array< std::uint8_t, 16 > Bytes
Definition Identity.h:114
Adapts one caller-owned EconomyLedger to IResourceAccount.
单个玩家的资源账本:当前量、上限、收支与浪费统计。
Dynamic field-of-view / fog-of-war facade. Phase A: 2D shadowcast + multi-revealer + explored memory....
Definition Fov.h:27
Pathfinding facade. Single-agent: A*. Group (same goal): Flow Field + follow. Grid/topology internals...
Definition Pathfinder.h:20
Immutable canonical multi-resource cost.
static eve::Result< CostSpec > create(std::vector< ResourceCost > items)
Validate and canonicalize resource items.
static eve::Result< CostSpec > single(std::string_view resource, std::int64_t amount)
Build a one-resource cost.
Provider-neutral resource account contract.
virtual eve::Result< Receipt > credit(const CostSpec &cost)=0
Atomically credit all items immediately.
virtual eve::Result< Receipt > debit(const CostSpec &cost)=0
Atomically debit all items immediately.
Adapter that maps RTS orders to the existing action::ActionRuntime.
Definition RTSAction.h:61
Canonical attributes component adapter, backed by attributes::AttributeSet.
Definition RTSTypes.h:323
RTS building domain root with placement and production composition.
Definition RTSTypes.h:840
void release() override
Entity.
Definition RTSTypes.h:846
static Building * createBuilding(SubjectRef subject={}, LogicalId definition={})
Create a building and initialize its self handle and identity.
Definition RTSTypes.cpp:656
Faction domain root; membership is a set of typed runtime handles.
Definition RTSTypes.h:1178
static Faction * createFaction(SubjectRef subject={})
Create a faction and initialize its self handle and identity.
Definition RTSTypes.cpp:684
void release() override
Entity.
Definition RTSTypes.h:1184
static void clear(State &state) noexcept
Remove all revealers previously installed by this state.
Interface isolating RTS orders from the shared action runtime.
Definition RTSAction.h:38
static Result< void > start(Match &match)
Validate rules/participants and transition Setup to Running.
Definition RTSMatch.cpp:106
static Result< void > addParticipant(Match &match, Faction &faction, int team)
Add one live faction before match start.
Definition RTSMatch.cpp:93
static Result< void > surrender(Match &match, Faction &faction)
Eliminate a participant and all of its live RTS entities.
Definition RTSMatch.cpp:132
Independent match composition root; factions may participate in different matches.
Definition RTSTypes.h:1295
static Match * createMatch(SubjectRef subject={})
Create a match and initialize every composition component. @ownership The active ECS table owns the e...
Definition RTSTypes.cpp:692
Player domain root; selection is RTS-local, authority/economy/social are links.
Definition RTSTypes.h:1119
static Player * createPlayer(SubjectRef subject={})
Create a player and initialize its self handle and identity.
Definition RTSTypes.cpp:676
static Result< void > apply(definitions::DefinitionRegistry &registry, Unit &unit, const ArchetypeWeaponFactory &weaponFactory={})
Apply the Unit root's logical definition atomically.
Deterministic lockstep input buffer and portable command log.
Definition RTSReplay.h:63
static eve::Result< void > ensure(Unit &unit)
Bind and seed default optional combat stats exactly once.
Owns RTS root entities and coordinates the phase-one systems.
Definition RTS.h:63
Result< GameplayObservation > observeGameplay(const GameplaySession &session, SubjectRef instance) const override
Observe one provider-owned gameplay instance without mutation.
Definition RTS.cpp:357
Result< Player * > newPlayer(SubjectRef subject)
Create and own one Player root with a valid subject identity.
Definition RTS.cpp:595
Result< GameplayCommandReceipt > submitGameplay(const GameplaySession &session, SubjectRef instance, const GameplayCommand &command) override
Validate authority and atomically submit one domain command.
Definition RTS.cpp:421
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.
Definition RTS.cpp:502
Result< Building * > newBuilding(SubjectRef subject, LogicalId definition={})
Create and own one Building root.
Definition RTS.cpp:550
Result< Value > inspectMatch(Match &match) const
Return an owning deterministic projection of one match lifecycle.
Definition RTS.cpp:854
~RTS() override
Destroy only live entities created through this module.
Definition RTS.cpp:285
void setCombatProviders(sensing::SensingWorld *sensing, combat::DamageRuntime *damage) noexcept
Attach canonical sensing and damage coordinators used by automatic combat.
Definition RTS.cpp:312
Result< std::vector< GameplayActionDescriptor > > availableGameplayActions(const GameplaySession &session, SubjectRef instance, SubjectRef subject) const override
Discover currently legal player-semantic actions for one subject.
Definition RTS.cpp:393
Result< Building * > newFactionBuilding(Faction &faction, SubjectRef subject, LogicalId definition={})
Atomically create a Building, bind its owning faction and register canonical membership.
Definition RTS.cpp:763
Result< Match * > newMatch(SubjectRef subject)
Create and own one independent RTS match composition root.
Definition RTS.cpp:798
void setDefinitionRegistry(definitions::DefinitionRegistry *registry) noexcept
Attach the canonical definition registry used to settle completed research.
Definition RTS.h:306
Result< void > addMatchParticipant(Match &match, Faction &faction, int team) const
Add one owned faction to one owned setup-phase match.
Definition RTS.cpp:833
Result< void > configureSettlementRules(const settlement::SettlementRuleSet &rules)
Replace declarative settlement rules on the active canonical damage provider.
Definition RTS.cpp:323
Result< void > surrenderMatch(Match &match, Faction &faction) const
Surrender one owned faction from one owned running match.
Definition RTS.cpp:847
Result< Faction * > newFaction(SubjectRef subject)
Create and own one Faction root with a valid subject identity.
Definition RTS.cpp:610
std::string_view gameplayDomain() const noexcept override
Stable provider domain used for discovery and routing.
Definition RTS.cpp:342
Result< ResourceNode * > newResourceNode(SubjectRef subject, std::string resourceType, float amount, WorldPosition position, std::size_t workerCapacity=1)
Create and own one harvestable resource node.
Definition RTS.cpp:571
Result< void > configureMatch(Match &match, VictoryRule rule, std::string archetype={}, double targetValue=0.0) const
Configure one owned setup-phase match victory rule.
Definition RTS.cpp:813
Result< void > rebuildState(const RTSStateSnapshot &snapshot)
Materialize a complete snapshot into an empty RTS module.
Result< Unit * > newUnit(SubjectRef subject, LogicalId definition={})
Create and own one Unit root.
Definition RTS.cpp:519
std::vector< SubjectRef > gameplayInstances() const override
Instances currently served, in publication order.
Definition RTS.cpp:344
Faction * findFaction(SubjectRef subject) const noexcept
Resolve a module-owned faction by persistent subject identity.
Definition RTS.cpp:3625
RTS()
Construct an empty RTS composition profile.
Definition RTS.cpp:280
Result< FanOutReceipt > fanOut(Player::Selection &selection, const CommandSpec &command, const FormationSpec &formation) const
Fan one common command out through a player's selected unit handles.
void setFogProvider(FogProvider provider) noexcept
Install the faction-to-canonical-FOV resolver used by fog-of-war projection.
Definition RTS.cpp:305
Result< Unit * > newFactionUnit(Faction &faction, SubjectRef subject, LogicalId definition={})
Atomically create a Unit, bind its owning faction and register canonical membership.
Definition RTS.cpp:713
void setCrowdProvider(crowd::Crowd *crowd) noexcept
Attach the canonical Crowd provider used by Units with a bound Unit::Crowd link.
Definition RTS.cpp:330
Result< void > startMatch(Match &match) const
Start one owned match after participant/rule validation.
Definition RTS.cpp:840
Result< GameplayObservation > advanceGameplay(const GameplaySession &session, SubjectRef instance, const SimulationStep &step) override
Advance one provider-owned instance using injected deterministic time.
Definition RTS.cpp:482
Harvestable RTS resource node; deposited balances are owned by an external resource account.
Definition RTSTypes.h:1071
static ResourceNode * createResourceNode(SubjectRef subject={})
Create a resource node and initialize all composition components. @ownership The active ECS table own...
Definition RTSTypes.cpp:668
RTS unit domain root.
Definition RTSTypes.h:495
static Unit * createUnit(SubjectRef subject={}, LogicalId definition={})
Create a unit and initialize its self handle and identity.
Definition RTSTypes.cpp:644
Gameplay-facing 2D candidate query service; it never values or selects targets.
Definition Sensing.h:151
Validated immutable-style rule collection installable into pipelines.
static eve::Result< SettlementRuleSet > fromJson(std::string_view json)
Decode and compile a strict versioned JSON rule document without mutating an existing rule set.
static eve::Result< WeaponDefinitionRuntime > create(eve::definitions::DefinitionRegistry &registry, eve::DefinitionRef definition, eve::PersistentId instanceId, eve::definition::ReloadPolicy policy=eve::definition::ReloadPolicy::RebuildInstance)
Create from a common weapon:<name> definition reference.
武器实体:数据全部在组件里,行为在 WeaponSystem。
static WeaponEntity * createWeapon()
创建并触摸全部组件。
Definition Weapon.cpp:129
Result< void > replay(const Trace &trace, IEnvironment &environment, double tolerance)
Reset and replay actual actions, checking all observations and failure evidence.
Definition Agent.cpp:258
HitReaction
Typed reaction selected after health and poise are resolved.
Definition Damage.h:26
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value
Definition Database.h:26
eve::StatusCode Status
Result< T > applied(T value, std::vector< Diagnostic > diagnostics={})
Construct an Applied result with an owning payload and optional diagnostics.
std::unordered_map< std::string, SkillDefinition > & table()
Definition Skill.cpp:65
Value fanOutValue(FanOutReceipt receipt)
Result< std::vector< SubjectRef > > parseScriptSubjects(const std::vector< std::string > &texts)
Parse script UUID strings into stable subjects. @cost Linear in subject count; allocates one output v...
Result< SubjectRef > parseScriptSubject(std::string_view text, std::string_view path)
std::function< Result< resource::Receipt >(Unit &, const resource::CostSpec &)> AbilityResourceDebit
Debits an ability resource cost through the game-owned canonical economy boundary.
Definition RTSSystems.h:492
DamageChannel
Authoritative damage route that produced one transient RTS frame event.
Definition RTSSystems.h:67
std::function< map::Fov *(Faction &)> FogProvider
Resolves the canonical FOV instance used by a faction; allies may deliberately share one instance.
Definition RTSSystems.h:310
std::function< void(const LifecycleEvent &, SimulationTick)> LifecycleEventSink
Read-only observer invoked after a non-combat lifecycle transition commits.
Definition RTSSystems.h:127
std::function< void(const CombatFireEvent &, SimulationTick)> CombatFireEventSink
Read-only observer invoked after a weapon consumes one authoritative shot.
Definition RTSSystems.h:91
const char * combatStanceName(CombatStance stance) noexcept
Return the stable lower-case spelling of a combat stance. @ownership Returns a borrowed pointer to im...
Definition RTSTypes.cpp:168
OrderKind
Commands shared by movement, combat, construction and gathering.
Definition RTSTypes.h:64
RTSReplayOperation
Stable operation kinds accepted by the shared deterministic RTS input stream.
Definition RTSReplay.h:20
CombatFireEventKind
Transient weapon lifecycle event emitted after an authoritative firing attempt commits.
Definition RTSSystems.h:74
std::function< Result< weapon::WeaponEntity * >(std::string_view definitionId, PersistentId instanceId)> ArchetypeWeaponFactory
Factory that creates one canonical weapon instance from a registry definition.
LifecycleEventKind
Non-combat RTS state transition exposed to presentation without entering authoritative state.
Definition RTSSystems.h:94
std::function< void(const combat::DamageRequest &request, const combat::DamageOutcome &outcome, SimulationTick tick, DamageChannel channel)> DamageEventSink
Read-only observer invoked after canonical damage has committed.
Definition RTSSystems.h:71
CombatStance
Automatic target-acquisition policy for an RTS unit.
Definition RTSTypes.h:54
const char * orderKindName(OrderKind kind) noexcept
Return the stable lower-case spelling of an RTS order kind.
Definition RTSTypes.cpp:177
VictoryRule
Supported deterministic RTS victory policies.
Definition RTSTypes.h:1290
ssq::Table projectStatusResult(HSQUIRRELVM vm, const Status &status)
Project a checked native status that carries no payload.
ssq::Table projectResult(HSQUIRRELVM vm, Result< void > &&result)
Consume and project a void native Result using the common schema.
::eve::SubjectRef SubjectRef
Strong reference to a runtime subject.
Definition Targeting.h:76
StrongUuid< detail::PersistentIdTag > PersistentId
Stable instance identity for persistence, networking and process boundaries.
Definition Identity.h:254
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31
bool enabled
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
SimulationTick tick
Tick reached after this step is applied.
Definition Time.h:160
Mutable combat state owned by the gameplay domain for one subject.
Definition Damage.h:37
Complete owning audit result of one committed damage transaction.
Definition Damage.h:98
Owning input for one damage transaction.
Definition Damage.h:49
Optional bounded velocity sampling for RTS local avoidance.
Definition Crowd.h:48
bool enabled
Opt in independently of legacy Boids steering.
Definition Crowd.h:49
A generation-qualified reference to one definition incarnation.
A subject-agnostic continuous task retained for audit and save games.
Definition Production.h:158
static eve::Result< ResourceCost > create(std::string_view resourceId, std::int64_t amountValue)
Construct and validate one cost item.
Typed ability policy; lifecycle effects remain owned by the canonical effect container.
Definition RTSTypes.h:133
std::string damageType
Definition RTSTypes.h:138
AbilityTarget target
Definition RTSTypes.h:136
std::int64_t resourceCost
Definition RTSTypes.h:142
std::string resourceType
Definition RTSTypes.h:137
RTSEffectDefinition effect
Definition RTSTypes.h:149
LogicalId casterDefinition
Definition RTSTypes.h:135
Stable presentation payload for firing, launch, and miss feedback.
Definition RTSSystems.h:84
Absolute muzzle and target heights resolved by the authoritative map/game adapter.
Definition RTSSystems.h:586
One domain-neutral command submitted to a unit's generic order queue.
Definition RTSTypes.h:101
std::string definitionId
Definition RTSTypes.h:104
ecs::EntityHandle targetEntity
Definition RTSTypes.h:107
Result< void > validate() const
Validate coordinates and command metadata without mutating state.
Definition RTSTypes.cpp:201
WorldPosition target
Definition RTSTypes.h:103
WorldPosition secondaryTarget
Definition RTSTypes.h:108
Input to the pure formation planner.
Definition RTSSystems.h:34
Stable payload for one committed RTS lifecycle transition.
Definition RTSSystems.h:119
Earliest canonical collision found along one projectile sweep segment.
Definition RTSSystems.h:612
Typed RTS definition projected to the common lifecycle schema.
Definition RTSEffects.h:35
One process-independent command using stable subjects instead of ECS handles.
Definition RTSReplay.h:39
Complete RTS-owned state; v2 adds movement groups, v1 imports with no groups. @cost Linear in saved e...
action::ActionRuntime action
Definition RTS.cpp:276
std::vector< PaidConstruction > paidConstruction
Definition RTS.cpp:241
std::vector< float > terrainElevations
Definition RTS.cpp:244
std::map< std::string, economy::EconomyLedger::Snapshot > economies
Definition RTS.cpp:237
std::map< std::string, SubjectRef > pendingProductionSubjects
Definition RTS.cpp:239
std::vector< PaidProduction > paidProduction
Definition RTS.cpp:240
std::map< std::string, map::Fov::Snapshot > fovs
Definition RTS.cpp:238
std::vector< float > navigationCosts
Definition RTS.cpp:243
economy::EconomyLedgerResourceAccount account
Definition RTS.cpp:216
economy::EconomyLedger ledger
Definition RTS.cpp:215
std::map< std::string, Checkpoint > checkpoints
Definition RTS.cpp:262
ActionAdapter adapter
Definition RTS.cpp:250
action::ActionRuntime actions
Definition RTS.cpp:249
std::vector< PaidConstruction > paidConstruction
Definition RTS.cpp:261
map::Pathfinder pathfinder
Definition RTS.cpp:251
std::vector< PaidProduction > paidProduction
Definition RTS.cpp:260
std::map< std::string, SubjectRef > pendingProductionSubjects
Definition RTS.cpp:259
std::uint64_t nextTick
Definition RTS.cpp:263
std::map< std::string, std::unique_ptr< EconomySlot > > economies
Definition RTS.cpp:257
std::vector< float > terrainElevations
Definition RTS.cpp:271
std::map< std::string, std::unique_ptr< map::Fov > > fovs
Definition RTS.cpp:258
RTSCommandLog commandLog
Definition RTS.cpp:256
definitions::DefinitionRegistry definitions
Definition RTS.cpp:255
sensing::SensingWorld sensing
Definition RTS.cpp:253
std::uint64_t aiProductionSequence
Definition RTS.cpp:264
combat::DamageRuntime damage
Definition RTS.cpp:254
Two-dimensional deterministic RTS world position.
Definition RTSTypes.h:48
武器模板(registerWeaponsFromJson 注册,进程级注册表)。