mission_script_runtime_dispatch.cpp 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834
  1. #include <algorithm>
  2. #include <array>
  3. #include <cstddef>
  4. #include <cstdint>
  5. #include <cstdio>
  6. #include <limits>
  7. #include <optional>
  8. #include <span>
  9. #include <string_view>
  10. #include "../../../middleware/content/packages/tables/scenario_reader.h"
  11. #include "../../../state/activity/membership/activity_membership_query.h"
  12. #include "../../../state/build_data/runtime.h"
  13. #include "../../../state/build_data/spawn_sets/spawn_set_catalog.h"
  14. #include "../../gameplay/squad_entity_retirement.h"
  15. #include "../activity_sdk_device_runtime.h"
  16. #include "../activity_sdk_lifetime_runtime.h"
  17. #include "../activity_sdk_mission_runtime.h"
  18. #include "../activity_sdk_scene_spawn.h"
  19. #include "../activity_sdk_squad_runtime.h"
  20. #include "mission_script_runtime.h"
  21. #include "mission_script_runtime_internal.h"
  22. // The intent fan-out reserves one Host output revision, then asks one typed adapter to encode it.
  23. namespace sunrise::server::activity::mission {
  24. namespace {
  25. namespace devices = activity_sdk_devices;
  26. namespace lifetimes = activity_sdk_lifetime;
  27. namespace scenes = activity_sdk_mission;
  28. namespace squads = activity_sdk_squads;
  29. namespace scriptable_auth = middleware::bap::activity_message::scriptable_auth;
  30. namespace squad_auth = middleware::bap::activity_message::squad_auth;
  31. /** Records the one deterministic execution attempt for diagnostics. */
  32. void begin_intent_attempt(RuntimeInstance& instance, std::uint64_t now) noexcept {
  33. if (instance.intentAttempts == 0) {
  34. instance.firstIntentAttempt = now;
  35. }
  36. ++instance.intentAttempts;
  37. }
  38. /** Assigns one exact Host output revision without clearing the durable intent. */
  39. [[nodiscard]] bool assign_delivery_state(RuntimeInstance& instance,
  40. std::uint64_t hostOutputRevision) noexcept {
  41. mission_state::Snapshot snapshot{};
  42. const mission_state::Status status =
  43. mission_state::assign_intent_output(instance.view.binding,
  44. instance.programKey,
  45. instance.missionStateRevision,
  46. instance.durableIntentSequence,
  47. hostOutputRevision,
  48. snapshot);
  49. if (status != mission_state::Status::ready) {
  50. lua_vm::fault(instance.vm, "durable mission intent assignment compare was refused");
  51. instance.programStatus = ProgramStatus::programError;
  52. log_line(
  53. core::log::Level::warn, &instance, "intent_assign", mission_state::status_name(status));
  54. return false;
  55. }
  56. accept_mission_state(instance, snapshot);
  57. return instance.durableHostOutputRevision == hostOutputRevision;
  58. }
  59. /** Reserves one exact Host revision, then makes the durable head own it before enqueue. */
  60. [[nodiscard]] bool reserve_delivery(RuntimeInstance& instance,
  61. host::ScriptableOutputReservation& reservation) noexcept {
  62. if (!host::reserve_scriptable_output(
  63. instance.view.binding, reservation, instance.durableIntentSequence)) {
  64. return false;
  65. }
  66. if (!assign_delivery_state(instance, reservation.revision)) {
  67. if (!host::release_scriptable_output(reservation)) {
  68. log_line(core::log::Level::warn, &instance, "intent_reserve", "release_refused");
  69. }
  70. persist_mission_fault(instance);
  71. clear_pending_events(instance.view.binding);
  72. clear_delivery(instance);
  73. return false;
  74. }
  75. instance.expectedScriptableRevision = reservation.revision;
  76. return true;
  77. }
  78. /** Releases State before opening the reserved Host lane after an adapter refusal. */
  79. [[nodiscard]] bool
  80. abandon_reserved_delivery(RuntimeInstance& instance,
  81. const host::ScriptableOutputReservation& reservation) noexcept {
  82. if (!release_delivery_state(instance)) {
  83. // Keep the Host reservation fail-closed when State cannot remove its ownership.
  84. persist_mission_fault(instance);
  85. clear_pending_events(instance.view.binding);
  86. clear_delivery(instance);
  87. return false;
  88. }
  89. if (!host::release_scriptable_output(reservation)) {
  90. fault_instance(instance, "Host mission output reservation release was refused");
  91. instance.programStatus = ProgramStatus::programError;
  92. log_line(core::log::Level::warn, &instance, "intent_reserve", "release_refused");
  93. clear_pending_events(instance.view.binding);
  94. clear_delivery(instance);
  95. return false;
  96. }
  97. return true;
  98. }
  99. /**
  100. * Enters the first delivery stage once an adapter has queued the request.
  101. * @param now Service tick the commit deadline is measured from.
  102. * @param result Diagnostic name of the queued request.
  103. */
  104. void await_host_commit(RuntimeInstance& instance,
  105. std::uint64_t now,
  106. std::string_view result) noexcept {
  107. if (instance.expectedScriptableRevision == 0
  108. || instance.durableHostOutputRevision != instance.expectedScriptableRevision) {
  109. fault_delivery(instance,
  110. "revision_unavailable",
  111. "queued intent did not match durable Host revision ownership");
  112. return;
  113. }
  114. instance.lastIntentStatus = (std::numeric_limits<std::uint16_t>::max)();
  115. instance.deliveryStage = DeliveryStage::awaitingHostCommit;
  116. instance.deliveryDeadline = deadline_after(now, kHostCommitTimeoutMs);
  117. log_line(core::log::Level::info, &instance, "intent", result);
  118. }
  119. } // namespace
  120. /**
  121. * @return True while a scene-family request is valid but its mission-seed lease has not published.
  122. * The request stays queued: refusing it drops an answer the client is still owed.
  123. */
  124. [[nodiscard]] bool scene_lease_still_publishing(scenes::SceneStatus status) noexcept {
  125. return status == scenes::SceneStatus::missionSeedPending
  126. || status == scenes::SceneStatus::outputBusy;
  127. }
  128. /** Raises one queued intent, or advances the delivery already in flight. */
  129. void dispatch_intent(RuntimeInstance& instance, std::uint64_t now) noexcept {
  130. if (instance.programStatus != ProgramStatus::loaded) {
  131. return;
  132. }
  133. lua_vm::Intent intent{};
  134. if (!lua_vm::pending_intent(instance.vm, intent)) {
  135. if (instance.deliveryStage != DeliveryStage::idle) {
  136. fault_delivery(instance, "missing_intent", "delivery lost its pending Lua intent");
  137. } else {
  138. clear_delivery(instance);
  139. }
  140. return;
  141. }
  142. if (intent_lifetime_expired(instance, now)) {
  143. if (reconcile_expired_delivery(instance)) {
  144. return;
  145. }
  146. refuse_delivery(instance,
  147. "intent_timeout",
  148. "intent exceeded its delivery lifetime",
  149. host::EffectOutcome::expired);
  150. return;
  151. }
  152. if (instance.deliveryStage != DeliveryStage::idle) {
  153. if (reconcile_transport_stage(instance)) {
  154. return;
  155. }
  156. static_cast<void>(service_delivery_timeout(instance, now));
  157. return;
  158. }
  159. begin_intent_attempt(instance, now);
  160. if (intent.attemptGeneration != instance.attempt.generation) {
  161. refuse_delivery(instance, "attempt_refused", "stale_attempt", host::EffectOutcome::refused);
  162. return;
  163. }
  164. switch (intent.kind) {
  165. case lua_vm::IntentKind::selectMissionState: {
  166. if (intent.retirePlacedProps) {
  167. namespace retirement = server::gameplay::squad_entity_retirement;
  168. const auto* world = instance.worldView.snapshot();
  169. const auto placement =
  170. state::activity::membership::reported_placement(instance.view.binding.sessionId);
  171. const auto status =
  172. world == nullptr || instance.publicTarget
  173. ? retirement::TransitionStatus::refused
  174. : retirement::begin_placed_transition(
  175. instance.view,
  176. *world,
  177. intent.requestKey,
  178. state::activity::membership::instantiated_region(placement),
  179. intent.effectiveRegion);
  180. if (status == retirement::TransitionStatus::pending) {
  181. report_intent_status(
  182. instance, kIntentStatusStateTransitionPending, "placed_retirement_pending");
  183. return;
  184. }
  185. if (status != retirement::TransitionStatus::ready) {
  186. refuse_delivery(instance,
  187. "state_refused",
  188. "placed_lifetimes_unavailable",
  189. host::EffectOutcome::refused);
  190. return;
  191. }
  192. }
  193. scenes::Snapshot selected{};
  194. const scenes::Status status =
  195. scenes::select_state(instance.view,
  196. intent.effectiveRegion,
  197. std::span(intent.seedOmissions).first(intent.seedOmissionCount),
  198. selected);
  199. if (status == scenes::Status::outputBusy) {
  200. report_intent_status(
  201. instance, kIntentStatusSceneOutputBusy, scenes::status_name(status));
  202. return;
  203. }
  204. if (status != scenes::Status::ready || !selected.configured
  205. || selected.plan.effectiveRegion
  206. != static_cast<std::uint32_t>(intent.effectiveRegion)) {
  207. refuse_delivery(instance,
  208. "state_refused",
  209. scenes::status_name(status),
  210. host::EffectOutcome::refused);
  211. return;
  212. }
  213. // The selected effective region is an authored-state key, not a region the client reports.
  214. // Publishing this lease revision is the completion edge, except when publication waits for
  215. // arrival: there the teleport is armed first, because arrival closes that window.
  216. if (!selected.regionArrivalPending
  217. && (selected.publicationPending || selected.revision == 0
  218. || selected.publishedRevision != selected.revision)) {
  219. report_intent_status(
  220. instance, kIntentStatusStateTransitionPending, "state_transition_pending");
  221. return;
  222. }
  223. // A selected state names its own slice-set region. Until the client transitions there its
  224. // object registry comes from the loaded slice-set entry, so the new state's objects stay
  225. // unfindable. Arming the host teleport is the only mid-activity move.
  226. arm_state_region_teleport(instance, selected.plan);
  227. static_cast<void>(complete_local_effect(instance, "state_selected"));
  228. return;
  229. }
  230. case lua_vm::IntentKind::holdSpawn: {
  231. // The hold rides the participation record, so the next roster publication carries it.
  232. ::sunrise::state::activity::membership::set_program_spawn_hold(
  233. instance.view.binding.sessionId, intent.active);
  234. static_cast<void>(
  235. complete_local_effect(instance, intent.active ? "spawn_held" : "spawn_released"));
  236. return;
  237. }
  238. case lua_vm::IntentKind::restartCheckpoint: {
  239. if (intent.checkpointReleaseRequest == 0
  240. && instance.attempt.generation == (std::numeric_limits<std::uint64_t>::max)()) {
  241. refuse_delivery(instance,
  242. "checkpoint_refused",
  243. "attempt_generation_exhausted",
  244. host::EffectOutcome::refused);
  245. return;
  246. }
  247. if (intent.checkpointReleaseRequest != 0) {
  248. if (::sunrise::state::activity::membership::release_hard_wipe(
  249. instance.view.binding, intent.checkpointReleaseRequest)) {
  250. static_cast<void>(complete_local_effect(instance, "checkpoint_reset_ready"));
  251. } else {
  252. refuse_delivery(instance,
  253. "checkpoint_refused",
  254. "wipe_not_active",
  255. host::EffectOutcome::refused);
  256. }
  257. return;
  258. }
  259. namespace data = ::sunrise::state::build_data;
  260. const auto& destination = instance.view.binding.destination;
  261. const std::string_view package(
  262. reinterpret_cast<const char*>(destination.packageName.data()),
  263. destination.packageNameLength);
  264. data::scenarios::Definition layout{};
  265. data::spawn_sets::NameHash spawn{};
  266. // A wipe restarts the whole party at an authored spawn set of the region they are in.
  267. if (instance.publicTarget || !instance.lastFireteamLife.all_dead()
  268. || instance.activeRegion != intent.effectiveRegion
  269. || !data::find_scenario_layout(package, layout)
  270. || !data::spawn_sets::find_hash({layout.spawnStem.data(), layout.spawnStemLength},
  271. intent.checkpointSpawnHash,
  272. spawn)
  273. || spawn.pointCount == 0) {
  274. refuse_delivery(instance,
  275. "checkpoint_refused",
  276. "party_or_spawn_unavailable",
  277. host::EffectOutcome::refused);
  278. return;
  279. }
  280. if (::sunrise::state::activity::membership::arm_hard_wipe(instance.view.binding,
  281. intent.requestKey,
  282. intent.effectiveRegion,
  283. intent.checkpointSpawnHash)) {
  284. static_cast<void>(complete_local_effect(instance, "checkpoint_wipe_armed"));
  285. } else {
  286. refuse_delivery(
  287. instance, "checkpoint_refused", "membership_busy", host::EffectOutcome::refused);
  288. }
  289. return;
  290. }
  291. case lua_vm::IntentKind::placeSquad: {
  292. const std::span<const std::int32_t> counts(intent.squadCounts.data(), intent.squadCount);
  293. const auto mode = static_cast<squad_auth::Mode>(intent.squadMode);
  294. host::ScriptableOutputReservation reservation{};
  295. if (!reserve_delivery(instance, reservation)) {
  296. if (instance.programStatus == ProgramStatus::loaded) {
  297. refuse_delivery(instance,
  298. "squad_refused",
  299. "host_reservation_unavailable",
  300. host::EffectOutcome::refused);
  301. }
  302. return;
  303. }
  304. const squads::Status status = squads::place_reserved(
  305. instance.view,
  306. intent.firstRow,
  307. counts,
  308. mode,
  309. reservation,
  310. std::nullopt,
  311. intent.squadRetireOnReturn,
  312. std::nullopt,
  313. intent.squadSpawnRuleRow < 0 ? std::nullopt
  314. : std::optional<std::uint32_t>{static_cast<std::uint32_t>(
  315. intent.squadSpawnRuleRow)});
  316. if (status == squads::Status::queued) {
  317. await_host_commit(instance, now, "squad_enqueued");
  318. } else if (!abandon_reserved_delivery(instance, reservation)) {
  319. return;
  320. } else {
  321. refuse_delivery(instance,
  322. "squad_refused",
  323. squads::status_name(status),
  324. host::EffectOutcome::refused);
  325. }
  326. return;
  327. }
  328. case lua_vm::IntentKind::actorCommand: {
  329. const ActorCommandPolicyStatus status = dispatch_actor_command(instance, intent);
  330. if (status == ActorCommandPolicyStatus::queued) {
  331. static_cast<void>(complete_local_effect(instance, "actor_command_queued"));
  332. } else {
  333. refuse_delivery(instance,
  334. "actor_command_refused",
  335. status == ActorCommandPolicyStatus::unavailable
  336. ? "gameplay_policy_unavailable"
  337. : "sdk_command_refused",
  338. host::EffectOutcome::refused);
  339. }
  340. return;
  341. }
  342. case lua_vm::IntentKind::setSquadAttachment: {
  343. host::ScriptableOutputReservation reservation{};
  344. if (!reserve_delivery(instance, reservation)) {
  345. if (instance.programStatus == ProgramStatus::loaded) {
  346. refuse_delivery(instance,
  347. "attachment_refused",
  348. "host_reservation_unavailable",
  349. host::EffectOutcome::refused);
  350. }
  351. return;
  352. }
  353. const devices::Status status =
  354. devices::set_squad_attachment_reserved(instance.view,
  355. intent.firstRow,
  356. intent.secondRow,
  357. intent.sourceSpawnGeneration,
  358. intent.active,
  359. reservation);
  360. if (status == devices::Status::queued) {
  361. await_host_commit(instance, now, "attachment_enqueued");
  362. } else if (!abandon_reserved_delivery(instance, reservation)) {
  363. return;
  364. } else {
  365. refuse_delivery(instance,
  366. "attachment_refused",
  367. devices::status_name(status),
  368. host::EffectOutcome::refused);
  369. }
  370. return;
  371. }
  372. case lua_vm::IntentKind::bindCombatantToSquad: {
  373. host::ScriptableOutputReservation reservation{};
  374. if (!reserve_delivery(instance, reservation)) {
  375. if (instance.programStatus == ProgramStatus::loaded) {
  376. refuse_delivery(instance,
  377. "combatant_binding_refused",
  378. "host_reservation_unavailable",
  379. host::EffectOutcome::refused);
  380. }
  381. return;
  382. }
  383. const devices::Status status =
  384. devices::bind_combatant_to_squad_reserved(instance.view, intent.firstRow, reservation);
  385. if (status == devices::Status::queued) {
  386. await_host_commit(instance, now, "combatant_binding_enqueued");
  387. } else if (!abandon_reserved_delivery(instance, reservation)) {
  388. return;
  389. } else {
  390. refuse_delivery(instance,
  391. "combatant_binding_refused",
  392. devices::status_name(status),
  393. host::EffectOutcome::refused);
  394. }
  395. return;
  396. }
  397. case lua_vm::IntentKind::setObjectActive: {
  398. host::ScriptableOutputReservation reservation{};
  399. if (!reserve_delivery(instance, reservation)) {
  400. if (instance.programStatus == ProgramStatus::loaded) {
  401. refuse_delivery(instance,
  402. "object_refused",
  403. "host_reservation_unavailable",
  404. host::EffectOutcome::refused);
  405. }
  406. return;
  407. }
  408. const devices::Status status = devices::set_objects_active_reserved(
  409. instance.view,
  410. intent.firstRow,
  411. std::span(intent.burstRows).first(intent.burstRowCount),
  412. intent.entryIndex,
  413. intent.active,
  414. reservation);
  415. if (status == devices::Status::queued) {
  416. await_host_commit(instance, now, "object_enqueued");
  417. } else if (!abandon_reserved_delivery(instance, reservation)) {
  418. return;
  419. } else {
  420. refuse_delivery(instance,
  421. "object_refused",
  422. devices::status_name(status),
  423. host::EffectOutcome::refused);
  424. }
  425. return;
  426. }
  427. case lua_vm::IntentKind::setDeviceChannel: {
  428. const auto channel = static_cast<scriptable_auth::Type23Channel>(intent.deviceChannel);
  429. host::ScriptableOutputReservation reservation{};
  430. if (!reserve_delivery(instance, reservation)) {
  431. if (instance.programStatus == ProgramStatus::loaded) {
  432. refuse_delivery(instance,
  433. "device_refused",
  434. "host_reservation_unavailable",
  435. host::EffectOutcome::refused);
  436. }
  437. return;
  438. }
  439. const devices::Status status = devices::set_channel_reserved(instance.view,
  440. intent.firstRow,
  441. channel,
  442. intent.deviceValue,
  443. intent.deviceSnap,
  444. reservation);
  445. if (status == devices::Status::queued) {
  446. await_host_commit(instance, now, "device_enqueued");
  447. } else if (!abandon_reserved_delivery(instance, reservation)) {
  448. return;
  449. } else {
  450. refuse_delivery(instance,
  451. "device_refused",
  452. devices::status_name(status),
  453. host::EffectOutcome::refused);
  454. }
  455. return;
  456. }
  457. case lua_vm::IntentKind::fireTrigger: {
  458. host::ScriptableOutputReservation reservation{};
  459. if (!reserve_delivery(instance, reservation)) {
  460. if (instance.programStatus == ProgramStatus::loaded) {
  461. refuse_delivery(instance,
  462. "trigger_refused",
  463. "host_reservation_unavailable",
  464. host::EffectOutcome::refused);
  465. }
  466. return;
  467. }
  468. const devices::Status status = devices::fire_trigger_reserved(
  469. instance.view, intent.firstRow, reservation, intent.active);
  470. if (status == devices::Status::queued) {
  471. await_host_commit(instance, now, "trigger_enqueued");
  472. } else if (!abandon_reserved_delivery(instance, reservation)) {
  473. return;
  474. } else {
  475. refuse_delivery(instance,
  476. "trigger_refused",
  477. devices::status_name(status),
  478. host::EffectOutcome::refused);
  479. }
  480. return;
  481. }
  482. case lua_vm::IntentKind::playSequence:
  483. case lua_vm::IntentKind::setCinematicActive: {
  484. const bool sequence = intent.kind == lua_vm::IntentKind::playSequence;
  485. host::ScriptableOutputReservation reservation{};
  486. if (!reserve_delivery(instance, reservation)) {
  487. if (instance.programStatus == ProgramStatus::loaded) {
  488. refuse_delivery(instance,
  489. sequence ? "sequence_refused" : "cinematic_refused",
  490. "host_reservation_unavailable",
  491. host::EffectOutcome::refused);
  492. }
  493. return;
  494. }
  495. const scenes::SceneStatus status =
  496. sequence
  497. ? scenes::play_sequence_slot_reserved(instance.view, intent.firstRow, reservation)
  498. : scenes::set_cinematic_slot_active_reserved(
  499. instance.view, intent.firstRow, intent.active, reservation);
  500. if (status == scenes::SceneStatus::queued) {
  501. await_host_commit(instance, now, sequence ? "sequence_enqueued" : "cinematic_enqueued");
  502. } else if (!abandon_reserved_delivery(instance, reservation)) {
  503. return;
  504. } else if (scene_lease_still_publishing(status)) {
  505. // The lease numbers say which side moved. The status dedup reports them once.
  506. std::array<char, 176> detail{};
  507. scenes::Snapshot lease{};
  508. const scenes::Status leaseStatus = scenes::query(instance.view, lease);
  509. const int written =
  510. std::snprintf(detail.data(),
  511. detail.size(),
  512. "%s lease=%s configured=%d revision=%llu published=%llu "
  513. "pending=%d arrival=%d region=%d plan_region=%u state_row=%u",
  514. scenes::status_name(status),
  515. scenes::status_name(leaseStatus),
  516. lease.configured ? 1 : 0,
  517. static_cast<unsigned long long>(lease.revision),
  518. static_cast<unsigned long long>(lease.publishedRevision),
  519. lease.publicationPending ? 1 : 0,
  520. lease.regionArrivalPending ? 1 : 0,
  521. lease.effectiveRegion,
  522. static_cast<unsigned>(lease.plan.effectiveRegion),
  523. static_cast<unsigned>(lease.plan.stateRow));
  524. report_intent_status(
  525. instance,
  526. kIntentStatusSceneLeasePending,
  527. written > 0 ? std::string_view{detail.data(), static_cast<std::size_t>(written)}
  528. : scenes::status_name(status));
  529. } else {
  530. refuse_delivery(instance,
  531. sequence ? "sequence_refused" : "cinematic_refused",
  532. scenes::status_name(status),
  533. host::EffectOutcome::refused);
  534. }
  535. return;
  536. }
  537. case lua_vm::IntentKind::playActorSequence: {
  538. host::ScriptableOutputReservation reservation{};
  539. if (!reserve_delivery(instance, reservation)) {
  540. if (instance.programStatus == ProgramStatus::loaded) {
  541. refuse_delivery(instance,
  542. "actor_sequence_refused",
  543. "host_reservation_unavailable",
  544. host::EffectOutcome::refused);
  545. }
  546. return;
  547. }
  548. const devices::Status status =
  549. intent.firstRow == intent.sequenceOwner.slotRow
  550. ? devices::play_combatant_sequence_reserved(
  551. instance.view, intent.sequenceOwner, intent.secondRow, reservation)
  552. : devices::Status::invalidSlot;
  553. if (status == devices::Status::queued) {
  554. await_host_commit(instance, now, "actor_sequence_enqueued");
  555. } else if (abandon_reserved_delivery(instance, reservation)) {
  556. refuse_delivery(instance,
  557. "actor_sequence_refused",
  558. devices::status_name(status),
  559. host::EffectOutcome::refused);
  560. }
  561. return;
  562. }
  563. case lua_vm::IntentKind::playPerformance: {
  564. host::ScriptableOutputReservation reservation{};
  565. if (!reserve_delivery(instance, reservation)) {
  566. if (instance.programStatus == ProgramStatus::loaded) {
  567. refuse_delivery(instance,
  568. "performance_refused",
  569. "host_reservation_unavailable",
  570. host::EffectOutcome::refused);
  571. }
  572. return;
  573. }
  574. const scenes::SceneStatus status = scenes::play_performance_slot_reserved(
  575. instance.view, intent.firstRow, intent.secondRow, reservation);
  576. if (status == scenes::SceneStatus::queued) {
  577. await_host_commit(instance, now, "performance_enqueued");
  578. } else if (!abandon_reserved_delivery(instance, reservation)) {
  579. return;
  580. } else if (scene_lease_still_publishing(status)) {
  581. report_intent_status(
  582. instance, kIntentStatusSceneLeasePending, scenes::status_name(status));
  583. } else {
  584. refuse_delivery(instance,
  585. "performance_refused",
  586. scenes::status_name(status),
  587. host::EffectOutcome::refused);
  588. }
  589. return;
  590. }
  591. case lua_vm::IntentKind::resetObjectives: {
  592. host::ScriptableOutputReservation reservation{};
  593. if (!reserve_delivery(instance, reservation)) {
  594. if (instance.programStatus == ProgramStatus::loaded) {
  595. refuse_delivery(instance,
  596. "objective_reset_refused",
  597. "host_reservation_unavailable",
  598. host::EffectOutcome::refused);
  599. }
  600. return;
  601. }
  602. const scenes::SceneStatus status =
  603. scenes::reset_objectives_slot_reserved(instance.view, intent.firstRow, reservation);
  604. if (status == scenes::SceneStatus::queued) {
  605. await_host_commit(instance, now, "objective_reset_enqueued");
  606. } else if (!abandon_reserved_delivery(instance, reservation)) {
  607. return;
  608. } else if (scene_lease_still_publishing(status)) {
  609. report_intent_status(
  610. instance, kIntentStatusSceneLeasePending, scenes::status_name(status));
  611. } else {
  612. refuse_delivery(instance,
  613. "objective_reset_refused",
  614. scenes::status_name(status),
  615. host::EffectOutcome::refused);
  616. }
  617. return;
  618. }
  619. case lua_vm::IntentKind::advanceTask:
  620. case lua_vm::IntentKind::playDialogueCue: {
  621. const bool dialogue = intent.kind == lua_vm::IntentKind::playDialogueCue;
  622. const std::uint16_t cueIndex = static_cast<std::uint16_t>(intent.secondRow);
  623. host::ScriptableOutputReservation reservation{};
  624. if (!reserve_delivery(instance, reservation)) {
  625. if (instance.programStatus == ProgramStatus::loaded) {
  626. refuse_delivery(instance,
  627. dialogue ? "dialogue_refused" : "task_refused",
  628. "host_reservation_unavailable",
  629. host::EffectOutcome::refused);
  630. }
  631. return;
  632. }
  633. const scenes::SceneStatus status =
  634. dialogue
  635. ? scenes::play_dialogue_cue_slot_reserved(
  636. instance.view,
  637. intent.firstRow,
  638. cueIndex,
  639. reservation,
  640. intent.dialogueFilterRow < 0
  641. ? std::nullopt
  642. : std::optional<std::uint32_t>{static_cast<std::uint32_t>(
  643. intent.dialogueFilterRow)})
  644. : scenes::activate_task_slot_reserved(instance.view, intent.firstRow, reservation);
  645. if (status == scenes::SceneStatus::queued) {
  646. await_host_commit(instance, now, dialogue ? "dialogue_enqueued" : "task_enqueued");
  647. } else if (!abandon_reserved_delivery(instance, reservation)) {
  648. return;
  649. } else if (scene_lease_still_publishing(status)) {
  650. report_intent_status(
  651. instance, kIntentStatusSceneLeasePending, scenes::status_name(status));
  652. } else {
  653. refuse_delivery(instance,
  654. dialogue ? "dialogue_refused" : "task_refused",
  655. scenes::status_name(status),
  656. host::EffectOutcome::refused);
  657. }
  658. return;
  659. }
  660. case lua_vm::IntentKind::runActorProgram:
  661. case lua_vm::IntentKind::retireActor:
  662. case lua_vm::IntentKind::assignCombatObjective: {
  663. host::ScriptableOutputReservation reservation{};
  664. if (!reserve_delivery(instance, reservation)) {
  665. if (instance.programStatus == ProgramStatus::loaded) {
  666. refuse_delivery(instance,
  667. "native_decision_refused",
  668. "output_busy",
  669. host::EffectOutcome::refused);
  670. }
  671. return;
  672. }
  673. const auto status =
  674. intent.kind != lua_vm::IntentKind::assignCombatObjective
  675. ? devices::run_actor_program_reserved(instance.view, intent, reservation)
  676. : devices::assign_combat_objective_reserved(instance.view, intent, reservation);
  677. if (status == devices::Status::queued) {
  678. await_host_commit(instance, now, "native_decision_enqueued");
  679. } else if (!abandon_reserved_delivery(instance, reservation)) {
  680. return;
  681. } else {
  682. refuse_delivery(instance,
  683. "native_decision_refused",
  684. devices::status_name(status),
  685. host::EffectOutcome::refused);
  686. }
  687. return;
  688. }
  689. case lua_vm::IntentKind::setInteractableObject:
  690. case lua_vm::IntentKind::setGhostLink:
  691. case lua_vm::IntentKind::watchDamage:
  692. case lua_vm::IntentKind::applySlotAuth: {
  693. if (intent.authByteCount == 0 || intent.authByteCount != intent.authBody.size()) {
  694. refuse_delivery(
  695. instance, "slot_auth_refused", "invalid_body", host::EffectOutcome::refused);
  696. return;
  697. }
  698. const std::span<const std::byte> body(intent.authBody.data(), intent.authByteCount);
  699. const std::span<const std::byte> sdkBuild(intent.sdkBuildSha256);
  700. host::ScriptableOutputReservation reservation{};
  701. if (!reserve_delivery(instance, reservation)) {
  702. if (instance.programStatus == ProgramStatus::loaded) {
  703. refuse_delivery(instance,
  704. "slot_auth_refused",
  705. "host_reservation_unavailable",
  706. host::EffectOutcome::refused);
  707. }
  708. return;
  709. }
  710. const devices::Status status =
  711. devices::apply_auth_reserved(instance.view,
  712. intent.firstRow,
  713. intent.objectTag,
  714. intent.registryKey,
  715. intent.authSchema,
  716. intent.slotIndex,
  717. intent.slotType,
  718. body,
  719. intent.authBitCount,
  720. sdkBuild,
  721. reservation,
  722. intent.kind == lua_vm::IntentKind::setInteractableObject
  723. ? host::ScriptableOverrideKind::interactableObject
  724. : intent.kind == lua_vm::IntentKind::setGhostLink
  725. ? host::ScriptableOverrideKind::ghostLink
  726. : intent.kind == lua_vm::IntentKind::watchDamage
  727. ? host::ScriptableOverrideKind::damageWatch
  728. : host::ScriptableOverrideKind::sdkAuth);
  729. if (status == devices::Status::queued) {
  730. await_host_commit(instance, now, "slot_auth_enqueued");
  731. } else if (!abandon_reserved_delivery(instance, reservation)) {
  732. return;
  733. } else {
  734. refuse_delivery(instance,
  735. "slot_auth_refused",
  736. devices::status_name(status),
  737. host::EffectOutcome::refused);
  738. }
  739. return;
  740. }
  741. case lua_vm::IntentKind::setLifetime: {
  742. if (intent.lifetimeState == mission_state::kCompletedLifetimeState
  743. && instance.attempt.complete) {
  744. refuse_delivery(instance,
  745. "lifetime_refused",
  746. "mission_already_complete",
  747. host::EffectOutcome::refused);
  748. return;
  749. }
  750. host::ScriptableOutputReservation reservation{};
  751. if (!reserve_delivery(instance, reservation)) {
  752. if (instance.programStatus == ProgramStatus::loaded) {
  753. refuse_delivery(instance,
  754. "lifetime_refused",
  755. "host_reservation_unavailable",
  756. host::EffectOutcome::refused);
  757. }
  758. return;
  759. }
  760. const devices::Status status =
  761. lifetimes::set_reserved(instance.view, intent.lifetimeState, reservation);
  762. if (status == devices::Status::queued) {
  763. await_host_commit(instance, now, "lifetime_enqueued");
  764. } else if (!abandon_reserved_delivery(instance, reservation)) {
  765. return;
  766. } else {
  767. refuse_delivery(instance,
  768. "lifetime_refused",
  769. devices::status_name(status),
  770. host::EffectOutcome::refused);
  771. }
  772. return;
  773. }
  774. case lua_vm::IntentKind::stopAuthoredScene:
  775. case lua_vm::IntentKind::signalAuthoredScene:
  776. case lua_vm::IntentKind::activateAuthoredScene: {
  777. if (intent.kind == lua_vm::IntentKind::activateAuthoredScene && intent.active
  778. && !scene_spawn_sources_match(instance.view, intent)) {
  779. refuse_delivery(
  780. instance, "scene_refused", "source_plan_changed", host::EffectOutcome::refused);
  781. return;
  782. }
  783. host::ScriptableOutputReservation reservation{};
  784. if (!reserve_delivery(instance, reservation)) {
  785. if (instance.programStatus == ProgramStatus::loaded) {
  786. refuse_delivery(instance,
  787. "scene_refused",
  788. "host_reservation_unavailable",
  789. host::EffectOutcome::refused);
  790. }
  791. return;
  792. }
  793. const scenes::SceneStatus status =
  794. intent.kind == lua_vm::IntentKind::activateAuthoredScene && intent.active
  795. ? scenes::activate_authored_scene_spawn_reserved(
  796. instance.view, intent.firstRow, intent.secondRow, reservation)
  797. : scenes::activate_authored_scene_reserved(
  798. instance.view,
  799. intent.firstRow,
  800. intent.secondRow,
  801. reservation,
  802. intent.sceneEventKey,
  803. intent.kind == lua_vm::IntentKind::stopAuthoredScene);
  804. if (status == scenes::SceneStatus::queued) {
  805. await_host_commit(instance, now, "scene_enqueued");
  806. } else if (!abandon_reserved_delivery(instance, reservation)) {
  807. return;
  808. } else if (scene_lease_still_publishing(status)) {
  809. report_intent_status(
  810. instance, kIntentStatusSceneLeasePending, scenes::status_name(status));
  811. } else {
  812. refuse_delivery(instance,
  813. "scene_refused",
  814. scenes::status_name(status),
  815. host::EffectOutcome::refused);
  816. }
  817. return;
  818. }
  819. }
  820. }
  821. } // namespace sunrise::server::activity::mission