mission_script_runtime_dispatch.cpp 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  1. #include <algorithm>
  2. #include <array>
  3. #include <cstddef>
  4. #include <cstdint>
  5. #include <cstdio>
  6. #include <limits>
  7. #include <span>
  8. #include <string_view>
  9. #include "../../../middleware/content/packages/tables/scenario_reader.h"
  10. #include "../../../state/activity/membership/activity_membership_query.h"
  11. #include "../../../state/build_data/runtime.h"
  12. #include "../../../state/build_data/spawn_sets/spawn_set_catalog.h"
  13. #include "../activity_sdk_device_runtime.h"
  14. #include "../activity_sdk_lifetime_runtime.h"
  15. #include "../activity_sdk_mission_runtime.h"
  16. #include "../activity_sdk_squad_runtime.h"
  17. #include "mission_script_runtime.h"
  18. #include "mission_script_runtime_internal.h"
  19. // The intent fan-out reserves one Host output revision, then asks one typed adapter to encode it.
  20. namespace sunrise::server::activity::mission {
  21. namespace {
  22. namespace devices = activity_sdk_devices;
  23. namespace lifetimes = activity_sdk_lifetime;
  24. namespace scenes = activity_sdk_mission;
  25. namespace squads = activity_sdk_squads;
  26. namespace scriptable_auth = middleware::bap::activity_message::scriptable_auth;
  27. namespace squad_auth = middleware::bap::activity_message::squad_auth;
  28. const void* g_actorCommandPolicyContext{};
  29. ActorCommandPolicy g_actorCommandPolicy{};
  30. /** Resolves one durable selector against the current pinned SDK. */
  31. [[nodiscard]] ActorCommandPolicyStatus
  32. dispatch_actor_command(const RuntimeInstance& instance, const lua_vm::Intent& intent) noexcept {
  33. if (instance.view.catalog == nullptr) {
  34. return ActorCommandPolicyStatus::refused;
  35. }
  36. const sdk::Snapshot published = sdk::snapshot();
  37. if (published == nullptr) {
  38. return ActorCommandPolicyStatus::refused;
  39. }
  40. const std::span<const std::byte> sdkBuild = published->sdk_build_sha256();
  41. if (sdkBuild.size() != intent.sdkBuildSha256.size()
  42. || !std::equal(sdkBuild.begin(), sdkBuild.end(), intent.sdkBuildSha256.begin())
  43. || instance.view.catalog->sdk_build_sha256().size() != sdkBuild.size()
  44. || !std::equal(
  45. sdkBuild.begin(), sdkBuild.end(), instance.view.catalog->sdk_build_sha256().begin())) {
  46. return ActorCommandPolicyStatus::refused;
  47. }
  48. const auto squads = published->squads();
  49. if (intent.firstRow >= squads.size()
  50. || (squads[intent.firstRow].flags & format::kSquadRunnableMask)
  51. != format::kSquadRunnableMask) {
  52. return ActorCommandPolicyStatus::refused;
  53. }
  54. const auto commands = published->actor_command_definitions();
  55. for (std::size_t index = 0; index < commands.size(); ++index) {
  56. const format::ActorCommandDefinition& command = commands[index];
  57. if (command.selector != intent.actorCommandSelector) {
  58. continue;
  59. }
  60. const bool valueValid = command.effect == format::ActorCommandEffect::setFaction
  61. && (intent.actorCommandValue == command.factionNone
  62. || intent.actorCommandValue == command.factionRemoved
  63. || intent.actorCommandValue == command.factionHostileToAll);
  64. if (command.payloadHandle == 0
  65. || command.provenance != format::ActorSemanticProvenance::executableStatic
  66. || command.flags != format::kActorCommandDefinitionExact || !valueValid) {
  67. return ActorCommandPolicyStatus::refused;
  68. }
  69. if (g_actorCommandPolicy == nullptr) {
  70. return ActorCommandPolicyStatus::unavailable;
  71. }
  72. const ActorCommandPolicyRequest request{
  73. .binding = instance.view.binding,
  74. .sdkBuildSha256 = intent.sdkBuildSha256,
  75. .squadRow = intent.firstRow,
  76. .commandRow = static_cast<std::uint32_t>(index),
  77. .commandSelector = command.selector,
  78. .value = intent.actorCommandValue,
  79. };
  80. return g_actorCommandPolicy(g_actorCommandPolicyContext, request);
  81. }
  82. return ActorCommandPolicyStatus::refused;
  83. }
  84. /** Records the one deterministic execution attempt for diagnostics. */
  85. void begin_intent_attempt(RuntimeInstance& instance, std::uint64_t now) noexcept {
  86. if (instance.intentAttempts == 0) {
  87. instance.firstIntentAttempt = now;
  88. }
  89. ++instance.intentAttempts;
  90. }
  91. /** Assigns one exact Host output revision without clearing the durable intent. */
  92. [[nodiscard]] bool assign_delivery_state(RuntimeInstance& instance,
  93. std::uint64_t hostOutputRevision) noexcept {
  94. mission_state::Snapshot snapshot{};
  95. const mission_state::Status status =
  96. mission_state::assign_intent_output(instance.view.binding,
  97. instance.programKey,
  98. instance.missionStateRevision,
  99. instance.durableIntentSequence,
  100. hostOutputRevision,
  101. snapshot);
  102. if (status != mission_state::Status::ready) {
  103. lua_vm::fault(instance.vm, "durable mission intent assignment compare was refused");
  104. instance.programStatus = ProgramStatus::programError;
  105. log_line(
  106. core::log::Level::warn, &instance, "intent_assign", mission_state::status_name(status));
  107. return false;
  108. }
  109. accept_mission_state(instance, snapshot);
  110. return instance.durableHostOutputRevision == hostOutputRevision;
  111. }
  112. /** Reserves one exact Host revision, then makes the durable head own it before enqueue. */
  113. [[nodiscard]] bool reserve_delivery(RuntimeInstance& instance,
  114. host::ScriptableOutputReservation& reservation) noexcept {
  115. if (!host::reserve_scriptable_output(
  116. instance.view.binding, reservation, instance.durableIntentSequence)) {
  117. return false;
  118. }
  119. if (!assign_delivery_state(instance, reservation.revision)) {
  120. if (!host::release_scriptable_output(reservation)) {
  121. log_line(core::log::Level::warn, &instance, "intent_reserve", "release_refused");
  122. }
  123. persist_mission_fault(instance);
  124. clear_pending_events(instance.view.binding);
  125. clear_delivery(instance);
  126. return false;
  127. }
  128. instance.expectedScriptableRevision = reservation.revision;
  129. return true;
  130. }
  131. /** Releases State before opening the reserved Host lane after an adapter refusal. */
  132. [[nodiscard]] bool
  133. abandon_reserved_delivery(RuntimeInstance& instance,
  134. const host::ScriptableOutputReservation& reservation) noexcept {
  135. if (!release_delivery_state(instance)) {
  136. // Keep the Host reservation fail-closed when State cannot remove its ownership.
  137. persist_mission_fault(instance);
  138. clear_pending_events(instance.view.binding);
  139. clear_delivery(instance);
  140. return false;
  141. }
  142. if (!host::release_scriptable_output(reservation)) {
  143. fault_instance(instance, "Host mission output reservation release was refused");
  144. instance.programStatus = ProgramStatus::programError;
  145. log_line(core::log::Level::warn, &instance, "intent_reserve", "release_refused");
  146. clear_pending_events(instance.view.binding);
  147. clear_delivery(instance);
  148. return false;
  149. }
  150. return true;
  151. }
  152. /**
  153. * Enters the first delivery stage once an adapter has queued the request.
  154. * @param now Service tick the commit deadline is measured from.
  155. * @param result Diagnostic name of the queued request.
  156. */
  157. void await_host_commit(RuntimeInstance& instance,
  158. std::uint64_t now,
  159. std::string_view result) noexcept {
  160. if (instance.expectedScriptableRevision == 0
  161. || instance.durableHostOutputRevision != instance.expectedScriptableRevision) {
  162. fault_delivery(instance,
  163. "revision_unavailable",
  164. "queued intent did not match durable Host revision ownership");
  165. return;
  166. }
  167. instance.lastIntentStatus = (std::numeric_limits<std::uint16_t>::max)();
  168. instance.deliveryStage = DeliveryStage::awaitingHostCommit;
  169. instance.deliveryDeadline = deadline_after(now, kHostCommitTimeoutMs);
  170. log_line(core::log::Level::info, &instance, "intent", result);
  171. }
  172. } // namespace
  173. /** Installs the gameplay policy seam. */
  174. void install_actor_command_policy(const void* context, ActorCommandPolicy policy) noexcept {
  175. g_actorCommandPolicyContext = context;
  176. g_actorCommandPolicy = policy;
  177. }
  178. /**
  179. * Moves the client to the region a freshly selected mission state belongs to.
  180. * The client picks its object registry from the loaded slice-set entry, so a state in another
  181. * region has no findable objects until it transitions. Message 12 is the only mid-activity move.
  182. * @param instance Runtime instance whose state selection just published.
  183. * @param plan Published plan naming the target region and its bubble.
  184. */
  185. void arm_state_region_teleport(RuntimeInstance& instance,
  186. const server::bap::ActivityMissionSeedPlan& plan) noexcept {
  187. namespace membership = ::sunrise::state::activity::membership;
  188. const auto& destination = instance.view.binding.destination;
  189. if (destination.packageNameLength == 0
  190. || destination.packageNameLength > destination.packageName.size()
  191. || plan.effectiveRegion > static_cast<std::uint32_t>(membership::kMaximumSliceSetIndex)) {
  192. return;
  193. }
  194. const std::int32_t reported = membership::player_region(instance.view.binding.sessionId);
  195. // The client answers a slice-set transition by de-instantiating its slice set and building the
  196. // target, so a move inside one bubble leaves both copies alive and doubles its content. The arm
  197. // must stay anyway: it is what orders the spawn, and without it the client never spawns in.
  198. if (reported == static_cast<std::int32_t>(plan.effectiveRegion)) {
  199. // Already there. Clear any earlier arm so the mirror owns the block again.
  200. static_cast<void>(membership::arm_host_teleport(
  201. instance.view.binding.sessionId, membership::kAbsentSliceSetIndex, 0));
  202. return;
  203. }
  204. // Native 4C8E40 registers alternate entries as distinct packed regions. A sibling
  205. // bookend still needs travel: its cinematic controller is absent in gameplay's world.
  206. const std::string_view name(reinterpret_cast<const char*>(destination.packageName.data()),
  207. destination.packageNameLength);
  208. ::sunrise::state::build_data::scenarios::Definition layout{};
  209. if (!::sunrise::state::build_data::find_scenario_layout(name, layout)
  210. || plan.bubbleOrdinal >= layout.bubbleHashes.size()) {
  211. return;
  212. }
  213. const std::uint32_t hash = layout.bubbleHashes[plan.bubbleOrdinal];
  214. if (hash == 0) {
  215. // Without the slice-set name hash the client cannot resolve the target, and a zero would
  216. // arm a move it can never finish.
  217. return;
  218. }
  219. const bool armed = membership::arm_host_teleport(
  220. instance.view.binding.sessionId, static_cast<std::int32_t>(plan.effectiveRegion), hash);
  221. log_line(core::log::Level::info,
  222. &instance,
  223. "state_region",
  224. armed ? "teleport_armed" : "teleport_unchanged");
  225. }
  226. /**
  227. * @return True while a scene-family request is valid but its mission-seed lease has not published.
  228. * The request stays queued: refusing it drops an answer the client is still owed.
  229. */
  230. [[nodiscard]] bool scene_lease_still_publishing(scenes::SceneStatus status) noexcept {
  231. return status == scenes::SceneStatus::missionSeedPending
  232. || status == scenes::SceneStatus::outputBusy;
  233. }
  234. /** Raises one queued intent, or advances the delivery already in flight. */
  235. void dispatch_intent(RuntimeInstance& instance, std::uint64_t now) noexcept {
  236. if (instance.programStatus != ProgramStatus::loaded) {
  237. return;
  238. }
  239. lua_vm::Intent intent{};
  240. if (!lua_vm::pending_intent(instance.vm, intent)) {
  241. if (instance.deliveryStage != DeliveryStage::idle) {
  242. fault_delivery(instance, "missing_intent", "delivery lost its pending Lua intent");
  243. } else {
  244. clear_delivery(instance);
  245. }
  246. return;
  247. }
  248. if (intent_lifetime_expired(instance, now)) {
  249. if (reconcile_expired_delivery(instance)) {
  250. return;
  251. }
  252. refuse_delivery(instance,
  253. "intent_timeout",
  254. "intent exceeded its delivery lifetime",
  255. host::EffectOutcome::expired);
  256. return;
  257. }
  258. if (instance.deliveryStage != DeliveryStage::idle) {
  259. if (reconcile_transport_stage(instance)) {
  260. return;
  261. }
  262. static_cast<void>(service_delivery_timeout(instance, now));
  263. return;
  264. }
  265. begin_intent_attempt(instance, now);
  266. switch (intent.kind) {
  267. case lua_vm::IntentKind::selectMissionState: {
  268. scenes::Snapshot selected{};
  269. const scenes::Status status =
  270. scenes::select_state(instance.view,
  271. intent.effectiveRegion,
  272. std::span(intent.seedOmissions).first(intent.seedOmissionCount),
  273. selected);
  274. if (status == scenes::Status::outputBusy) {
  275. report_intent_status(
  276. instance, kIntentStatusSceneOutputBusy, scenes::status_name(status));
  277. return;
  278. }
  279. if (status != scenes::Status::ready || !selected.configured
  280. || selected.plan.effectiveRegion
  281. != static_cast<std::uint32_t>(intent.effectiveRegion)) {
  282. refuse_delivery(instance,
  283. "state_refused",
  284. scenes::status_name(status),
  285. host::EffectOutcome::refused);
  286. return;
  287. }
  288. // The selected effective region is an authored-state key, not a region the client reports.
  289. // Publishing this lease revision is the completion edge, except when publication waits for
  290. // arrival: there the teleport is armed first, because arrival closes that window.
  291. if (!selected.regionArrivalPending
  292. && (selected.publicationPending || selected.revision == 0
  293. || selected.publishedRevision != selected.revision)) {
  294. report_intent_status(
  295. instance, kIntentStatusStateTransitionPending, "state_transition_pending");
  296. return;
  297. }
  298. // A selected state names its own slice-set region. Until the client transitions there its
  299. // object registry comes from the loaded slice-set entry, so the new state's objects stay
  300. // unfindable. Arming the host teleport is the only mid-activity move.
  301. arm_state_region_teleport(instance, selected.plan);
  302. static_cast<void>(complete_local_effect(instance, "state_selected"));
  303. return;
  304. }
  305. case lua_vm::IntentKind::restartCheckpoint: {
  306. if (intent.checkpointReleaseRequest) {
  307. if (::sunrise::state::activity::membership::release_hard_wipe(instance.view.binding,
  308. intent.checkpointReleaseRequest))
  309. static_cast<void>(complete_local_effect(instance,"checkpoint_reset_ready"));
  310. else refuse_delivery(instance,"checkpoint_refused","wipe_not_active",host::EffectOutcome::refused);
  311. return;
  312. }
  313. namespace data=::sunrise::state::build_data;
  314. const auto& destination=instance.view.binding.destination;
  315. const std::string_view package(reinterpret_cast<const char*>(destination.packageName.data()),
  316. destination.packageNameLength);
  317. data::scenarios::Definition layout{};
  318. data::spawn_sets::NameHash spawn{};
  319. if (instance.publicTarget || !instance.lastFireteamLife.all_dead()
  320. || instance.activeRegion!=intent.effectiveRegion
  321. || !data::find_scenario_layout(package,layout)
  322. || !data::spawn_sets::find_hash({layout.spawnStem.data(),layout.spawnStemLength},
  323. intent.checkpointSpawnHash,spawn) || !spawn.pointCount) {
  324. refuse_delivery(instance,"checkpoint_refused","party_or_spawn_unavailable",host::EffectOutcome::refused);
  325. return;
  326. }
  327. if (::sunrise::state::activity::membership::arm_hard_wipe(instance.view.binding,
  328. intent.requestKey,intent.effectiveRegion,intent.checkpointSpawnHash))
  329. static_cast<void>(complete_local_effect(instance,"checkpoint_wipe_armed"));
  330. else refuse_delivery(instance,"checkpoint_refused","membership_busy",host::EffectOutcome::refused);
  331. return;
  332. }
  333. case lua_vm::IntentKind::placeSquad: {
  334. const std::span<const std::int32_t> counts(intent.squadCounts.data(), intent.squadCount);
  335. const auto mode = static_cast<squad_auth::Mode>(intent.squadMode);
  336. host::ScriptableOutputReservation reservation{};
  337. if (!reserve_delivery(instance, reservation)) {
  338. if (instance.programStatus == ProgramStatus::loaded) {
  339. refuse_delivery(instance,
  340. "squad_refused",
  341. "host_reservation_unavailable",
  342. host::EffectOutcome::refused);
  343. }
  344. return;
  345. }
  346. const squads::Status status = squads::place_reserved(instance.view,
  347. intent.firstRow,
  348. counts,
  349. mode,
  350. reservation,
  351. std::nullopt,
  352. intent.squadRetireOnReturn);
  353. if (status == squads::Status::queued) {
  354. await_host_commit(instance, now, "squad_enqueued");
  355. } else if (!abandon_reserved_delivery(instance, reservation)) {
  356. return;
  357. } else {
  358. refuse_delivery(instance,
  359. "squad_refused",
  360. squads::status_name(status),
  361. host::EffectOutcome::refused);
  362. }
  363. return;
  364. }
  365. case lua_vm::IntentKind::actorCommand: {
  366. const ActorCommandPolicyStatus status = dispatch_actor_command(instance, intent);
  367. if (status == ActorCommandPolicyStatus::queued) {
  368. static_cast<void>(complete_local_effect(instance, "actor_command_queued"));
  369. } else {
  370. refuse_delivery(instance,
  371. "actor_command_refused",
  372. status == ActorCommandPolicyStatus::unavailable
  373. ? "gameplay_policy_unavailable"
  374. : "sdk_command_refused",
  375. host::EffectOutcome::refused);
  376. }
  377. return;
  378. }
  379. case lua_vm::IntentKind::bindCombatantToSquad: {
  380. host::ScriptableOutputReservation reservation{};
  381. if (!reserve_delivery(instance, reservation)) {
  382. if (instance.programStatus == ProgramStatus::loaded) {
  383. refuse_delivery(instance,
  384. "combatant_binding_refused",
  385. "host_reservation_unavailable",
  386. host::EffectOutcome::refused);
  387. }
  388. return;
  389. }
  390. const devices::Status status =
  391. devices::bind_combatant_to_squad_reserved(instance.view, intent.firstRow, reservation);
  392. if (status == devices::Status::queued) {
  393. await_host_commit(instance, now, "combatant_binding_enqueued");
  394. } else if (!abandon_reserved_delivery(instance, reservation)) {
  395. return;
  396. } else {
  397. refuse_delivery(instance,
  398. "combatant_binding_refused",
  399. devices::status_name(status),
  400. host::EffectOutcome::refused);
  401. }
  402. return;
  403. }
  404. case lua_vm::IntentKind::setObjectActive: {
  405. host::ScriptableOutputReservation reservation{};
  406. if (!reserve_delivery(instance, reservation)) {
  407. if (instance.programStatus == ProgramStatus::loaded) {
  408. refuse_delivery(instance,
  409. "object_refused",
  410. "host_reservation_unavailable",
  411. host::EffectOutcome::refused);
  412. }
  413. return;
  414. }
  415. const devices::Status status = devices::set_objects_active_reserved(
  416. instance.view,
  417. intent.firstRow,
  418. std::span(intent.burstRows).first(intent.burstRowCount),
  419. intent.entryIndex,
  420. intent.active,
  421. reservation);
  422. if (status == devices::Status::queued) {
  423. await_host_commit(instance, now, "object_enqueued");
  424. } else if (!abandon_reserved_delivery(instance, reservation)) {
  425. return;
  426. } else {
  427. refuse_delivery(instance,
  428. "object_refused",
  429. devices::status_name(status),
  430. host::EffectOutcome::refused);
  431. }
  432. return;
  433. }
  434. case lua_vm::IntentKind::setDeviceChannel: {
  435. const auto channel = static_cast<scriptable_auth::Type23Channel>(intent.deviceChannel);
  436. host::ScriptableOutputReservation reservation{};
  437. if (!reserve_delivery(instance, reservation)) {
  438. if (instance.programStatus == ProgramStatus::loaded) {
  439. refuse_delivery(instance,
  440. "device_refused",
  441. "host_reservation_unavailable",
  442. host::EffectOutcome::refused);
  443. }
  444. return;
  445. }
  446. const devices::Status status = devices::set_channel_reserved(instance.view,
  447. intent.firstRow,
  448. channel,
  449. intent.deviceValue,
  450. intent.deviceSnap,
  451. reservation);
  452. if (status == devices::Status::queued) {
  453. await_host_commit(instance, now, "device_enqueued");
  454. } else if (!abandon_reserved_delivery(instance, reservation)) {
  455. return;
  456. } else {
  457. refuse_delivery(instance,
  458. "device_refused",
  459. devices::status_name(status),
  460. host::EffectOutcome::refused);
  461. }
  462. return;
  463. }
  464. case lua_vm::IntentKind::fireTrigger: {
  465. host::ScriptableOutputReservation reservation{};
  466. if (!reserve_delivery(instance, reservation)) {
  467. if (instance.programStatus == ProgramStatus::loaded) {
  468. refuse_delivery(instance,
  469. "trigger_refused",
  470. "host_reservation_unavailable",
  471. host::EffectOutcome::refused);
  472. }
  473. return;
  474. }
  475. const devices::Status status =
  476. devices::fire_trigger_reserved(instance.view, intent.firstRow, reservation);
  477. if (status == devices::Status::queued) {
  478. await_host_commit(instance, now, "trigger_enqueued");
  479. } else if (!abandon_reserved_delivery(instance, reservation)) {
  480. return;
  481. } else {
  482. refuse_delivery(instance,
  483. "trigger_refused",
  484. devices::status_name(status),
  485. host::EffectOutcome::refused);
  486. }
  487. return;
  488. }
  489. case lua_vm::IntentKind::playSequence:
  490. case lua_vm::IntentKind::setCinematicActive: {
  491. const bool sequence = intent.kind == lua_vm::IntentKind::playSequence;
  492. host::ScriptableOutputReservation reservation{};
  493. if (!reserve_delivery(instance, reservation)) {
  494. if (instance.programStatus == ProgramStatus::loaded) {
  495. refuse_delivery(instance,
  496. sequence ? "sequence_refused" : "cinematic_refused",
  497. "host_reservation_unavailable",
  498. host::EffectOutcome::refused);
  499. }
  500. return;
  501. }
  502. const scenes::SceneStatus status =
  503. sequence
  504. ? scenes::play_sequence_slot_reserved(instance.view, intent.firstRow, reservation)
  505. : scenes::set_cinematic_slot_active_reserved(
  506. instance.view, intent.firstRow, intent.active, reservation);
  507. if (status == scenes::SceneStatus::queued) {
  508. await_host_commit(instance, now, sequence ? "sequence_enqueued" : "cinematic_enqueued");
  509. } else if (!abandon_reserved_delivery(instance, reservation)) {
  510. return;
  511. } else if (scene_lease_still_publishing(status)) {
  512. // A lease that never publishes stalls the intent until its lifetime expires, and the
  513. // status name alone does not say which side moved. The dispatcher gate that lets a
  514. // state selection complete tests exactly `revision == publishedRevision`, so a lease
  515. // pending immediately afterwards means one of them changed between the two checks.
  516. // report_intent_status dedups on the status, so this records the numbers once.
  517. std::array<char, 176> detail{};
  518. scenes::Snapshot lease{};
  519. const scenes::Status leaseStatus = scenes::query(instance.view, lease);
  520. const int written =
  521. std::snprintf(detail.data(),
  522. detail.size(),
  523. "%s lease=%s configured=%d revision=%llu published=%llu "
  524. "pending=%d arrival=%d region=%d plan_region=%u state_row=%u",
  525. scenes::status_name(status),
  526. scenes::status_name(leaseStatus),
  527. lease.configured ? 1 : 0,
  528. static_cast<unsigned long long>(lease.revision),
  529. static_cast<unsigned long long>(lease.publishedRevision),
  530. lease.publicationPending ? 1 : 0,
  531. lease.regionArrivalPending ? 1 : 0,
  532. lease.effectiveRegion,
  533. static_cast<unsigned>(lease.plan.effectiveRegion),
  534. static_cast<unsigned>(lease.plan.stateRow));
  535. report_intent_status(
  536. instance,
  537. kIntentStatusSceneLeasePending,
  538. written > 0
  539. ? std::string_view{detail.data(), static_cast<std::size_t>(written)}
  540. : scenes::status_name(status));
  541. } else {
  542. refuse_delivery(instance,
  543. sequence ? "sequence_refused" : "cinematic_refused",
  544. scenes::status_name(status),
  545. host::EffectOutcome::refused);
  546. }
  547. return;
  548. }
  549. case lua_vm::IntentKind::playPerformance: {
  550. host::ScriptableOutputReservation reservation{};
  551. if (!reserve_delivery(instance, reservation)) {
  552. if (instance.programStatus == ProgramStatus::loaded) {
  553. refuse_delivery(instance,
  554. "performance_refused",
  555. "host_reservation_unavailable",
  556. host::EffectOutcome::refused);
  557. }
  558. return;
  559. }
  560. const scenes::SceneStatus status = scenes::play_performance_slot_reserved(
  561. instance.view, intent.firstRow, intent.secondRow, reservation);
  562. if (status == scenes::SceneStatus::queued) {
  563. await_host_commit(instance, now, "performance_enqueued");
  564. } else if (!abandon_reserved_delivery(instance, reservation)) {
  565. return;
  566. } else if (scene_lease_still_publishing(status)) {
  567. report_intent_status(
  568. instance, kIntentStatusSceneLeasePending, scenes::status_name(status));
  569. } else {
  570. refuse_delivery(instance,
  571. "performance_refused",
  572. scenes::status_name(status),
  573. host::EffectOutcome::refused);
  574. }
  575. return;
  576. }
  577. case lua_vm::IntentKind::resetObjectives: {
  578. host::ScriptableOutputReservation reservation{};
  579. if (!reserve_delivery(instance, reservation)) {
  580. if (instance.programStatus == ProgramStatus::loaded) {
  581. refuse_delivery(instance,
  582. "objective_reset_refused",
  583. "host_reservation_unavailable",
  584. host::EffectOutcome::refused);
  585. }
  586. return;
  587. }
  588. const scenes::SceneStatus status =
  589. scenes::reset_objectives_slot_reserved(instance.view, intent.firstRow, reservation);
  590. if (status == scenes::SceneStatus::queued) {
  591. await_host_commit(instance, now, "objective_reset_enqueued");
  592. } else if (!abandon_reserved_delivery(instance, reservation)) {
  593. return;
  594. } else if (scene_lease_still_publishing(status)) {
  595. report_intent_status(
  596. instance, kIntentStatusSceneLeasePending, scenes::status_name(status));
  597. } else {
  598. refuse_delivery(instance,
  599. "objective_reset_refused",
  600. scenes::status_name(status),
  601. host::EffectOutcome::refused);
  602. }
  603. return;
  604. }
  605. case lua_vm::IntentKind::advanceTask:
  606. case lua_vm::IntentKind::playDialogueCue: {
  607. const bool dialogue = intent.kind == lua_vm::IntentKind::playDialogueCue;
  608. const std::uint16_t cueIndex = static_cast<std::uint16_t>(intent.secondRow);
  609. host::ScriptableOutputReservation reservation{};
  610. if (!reserve_delivery(instance, reservation)) {
  611. if (instance.programStatus == ProgramStatus::loaded) {
  612. refuse_delivery(instance,
  613. dialogue ? "dialogue_refused" : "task_refused",
  614. "host_reservation_unavailable",
  615. host::EffectOutcome::refused);
  616. }
  617. return;
  618. }
  619. const scenes::SceneStatus status =
  620. dialogue
  621. ? scenes::play_dialogue_cue_slot_reserved(
  622. instance.view, intent.firstRow, cueIndex, reservation)
  623. : scenes::activate_task_slot_reserved(instance.view, intent.firstRow, reservation);
  624. if (status == scenes::SceneStatus::queued) {
  625. await_host_commit(instance, now, dialogue ? "dialogue_enqueued" : "task_enqueued");
  626. } else if (!abandon_reserved_delivery(instance, reservation)) {
  627. return;
  628. } else if (scene_lease_still_publishing(status)) {
  629. report_intent_status(
  630. instance, kIntentStatusSceneLeasePending, scenes::status_name(status));
  631. } else {
  632. refuse_delivery(instance,
  633. dialogue ? "dialogue_refused" : "task_refused",
  634. scenes::status_name(status),
  635. host::EffectOutcome::refused);
  636. }
  637. return;
  638. }
  639. case lua_vm::IntentKind::applySlotAuth: {
  640. if (intent.authByteCount == 0 || intent.authByteCount != intent.authBody.size()) {
  641. refuse_delivery(
  642. instance, "slot_auth_refused", "invalid_body", host::EffectOutcome::refused);
  643. return;
  644. }
  645. const std::span<const std::byte> body(intent.authBody.data(), intent.authByteCount);
  646. const std::span<const std::byte> sdkBuild(intent.sdkBuildSha256);
  647. host::ScriptableOutputReservation reservation{};
  648. if (!reserve_delivery(instance, reservation)) {
  649. if (instance.programStatus == ProgramStatus::loaded) {
  650. refuse_delivery(instance,
  651. "slot_auth_refused",
  652. "host_reservation_unavailable",
  653. host::EffectOutcome::refused);
  654. }
  655. return;
  656. }
  657. const devices::Status status = devices::apply_auth_reserved(instance.view,
  658. intent.firstRow,
  659. intent.objectTag,
  660. intent.registryKey,
  661. intent.authSchema,
  662. intent.slotIndex,
  663. intent.slotType,
  664. body,
  665. intent.authBitCount,
  666. sdkBuild,
  667. reservation);
  668. if (status == devices::Status::queued) {
  669. await_host_commit(instance, now, "slot_auth_enqueued");
  670. } else if (!abandon_reserved_delivery(instance, reservation)) {
  671. return;
  672. } else {
  673. refuse_delivery(instance,
  674. "slot_auth_refused",
  675. devices::status_name(status),
  676. host::EffectOutcome::refused);
  677. }
  678. return;
  679. }
  680. case lua_vm::IntentKind::setLifetime: {
  681. host::ScriptableOutputReservation reservation{};
  682. if (!reserve_delivery(instance, reservation)) {
  683. if (instance.programStatus == ProgramStatus::loaded) {
  684. refuse_delivery(instance,
  685. "lifetime_refused",
  686. "host_reservation_unavailable",
  687. host::EffectOutcome::refused);
  688. }
  689. return;
  690. }
  691. const devices::Status status =
  692. lifetimes::set_reserved(instance.view, intent.lifetimeState, reservation);
  693. if (status == devices::Status::queued) {
  694. await_host_commit(instance, now, "lifetime_enqueued");
  695. } else if (!abandon_reserved_delivery(instance, reservation)) {
  696. return;
  697. } else {
  698. refuse_delivery(instance,
  699. "lifetime_refused",
  700. devices::status_name(status),
  701. host::EffectOutcome::refused);
  702. }
  703. return;
  704. }
  705. case lua_vm::IntentKind::activateAuthoredScene: {
  706. host::ScriptableOutputReservation reservation{};
  707. if (!reserve_delivery(instance, reservation)) {
  708. if (instance.programStatus == ProgramStatus::loaded) {
  709. refuse_delivery(instance,
  710. "scene_refused",
  711. "host_reservation_unavailable",
  712. host::EffectOutcome::refused);
  713. }
  714. return;
  715. }
  716. const scenes::SceneStatus status = scenes::activate_authored_scene_reserved(
  717. instance.view, intent.firstRow, intent.secondRow, reservation);
  718. if (status == scenes::SceneStatus::queued) {
  719. await_host_commit(instance, now, "scene_enqueued");
  720. } else if (!abandon_reserved_delivery(instance, reservation)) {
  721. return;
  722. } else if (scene_lease_still_publishing(status)) {
  723. report_intent_status(
  724. instance, kIntentStatusSceneLeasePending, scenes::status_name(status));
  725. } else {
  726. refuse_delivery(instance,
  727. "scene_refused",
  728. scenes::status_name(status),
  729. host::EffectOutcome::refused);
  730. }
  731. return;
  732. }
  733. }
  734. }
  735. } // namespace sunrise::server::activity::mission