mission_script_lua_device_api.cpp 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. #include <array>
  2. #include <bit>
  3. #include <cstddef>
  4. #include <cstdint>
  5. #include <limits>
  6. #include <span>
  7. #include <string_view>
  8. #include "../../../middleware/bap/activity_message/damage_monitor_auth.h"
  9. #include "../../../middleware/bap/activity_message/darkness_zone_auth.h"
  10. #include "../../../middleware/bap/activity_message/ghost_link_auth.h"
  11. #include "../../../middleware/bap/activity_message/interactable_object_auth.h"
  12. #include "../../../middleware/encoding/bit_writer.h"
  13. #include "mission_script_lua_internal.h"
  14. namespace sunrise::server::activity::mission::lua_vm::detail {
  15. namespace format = state::activity_sdk::format;
  16. namespace scriptable_auth = middleware::bap::activity_message::scriptable_auth;
  17. namespace auth_fields = middleware::bap::activity_message::auth_fields;
  18. namespace {
  19. /** Templates need a positive counter; the native reducer chooses the transported value. */
  20. constexpr std::int32_t kUncommittedCounter = 1;
  21. /** @return True when one live Slot row is an exact type-23 device. */
  22. [[nodiscard]] bool exact_device_slot(const SlotDefinition& definition) noexcept {
  23. return definition.slotType == format::kDeviceSlotType
  24. && definition.componentClass == format::kDeviceComponentClass
  25. && definition.senseSchema == format::kDeviceSenseSchema
  26. && definition.authSchema == format::kDeviceAuthSchema
  27. && (definition.flags & format::kSlotSchemaJoinExact) != 0;
  28. }
  29. /** @return True when one live Slot row is an exact type-4 authored object. */
  30. [[nodiscard]] bool exact_object_slot(const SlotDefinition& definition) noexcept {
  31. return definition.slotType == format::kObjectSlotType
  32. && definition.componentClass == format::kObjectComponentClass
  33. && definition.senseSchema == format::kObjectSenseSchema
  34. && definition.authSchema == format::kObjectAuthSchema
  35. && (definition.flags & format::kSlotSchemaJoinExact) != 0;
  36. }
  37. /** @return True when one live Slot row is an exact type-65 Ghost-link sensor. */
  38. [[nodiscard]] bool exact_ghost_link_slot(const SlotDefinition& definition) noexcept {
  39. namespace ghost = middleware::bap::activity_message::ghost_link;
  40. return definition.slotType == ghost::kSlotType
  41. && definition.componentClass == ghost::kComponentClass
  42. && definition.senseSchema == ghost::kSenseSchema
  43. && definition.authSchema == ghost::kAuthSchema
  44. && (definition.flags & format::kSlotSchemaJoinExact) != 0;
  45. }
  46. /** @return True when one live Slot row is an exact type-31 configured trigger. */
  47. [[nodiscard]] bool exact_trigger_slot(const SlotDefinition& definition) noexcept {
  48. namespace auth = middleware::bap::activity_message::scriptable_auth;
  49. return definition.slotType == auth::kType31SlotType
  50. && definition.authSchema == auth::kType31Schema
  51. && (definition.flags & format::kSlotSchemaJoinExact) != 0;
  52. }
  53. /** @return True when one live Slot row is an exact type-30 occupancy condition. */
  54. [[nodiscard]] bool exact_occupancy_slot(const SlotDefinition& definition) noexcept {
  55. return definition.slotType == format::kOccupancySlotType
  56. && definition.componentClass == format::kOccupancyComponentClass
  57. && definition.senseSchema == format::kOccupancySenseSchema
  58. && definition.authSchema == format::kOccupancyAuthSchema
  59. && (definition.flags & format::kSlotSchemaJoinExact) != 0;
  60. }
  61. /**
  62. * Reads the optional `with` list: more object slots that answer on this one's revision.
  63. * @return False with the Lua error already raised.
  64. */
  65. [[nodiscard]] bool parse_object_burst(lua_State* state, Intent& intent) {
  66. lua_getfield(state, 2, "with");
  67. if (lua_isnoneornil(state, -1)) {
  68. lua_pop(state, 1);
  69. return true;
  70. }
  71. if (!lua_istable(state, -1)) {
  72. lua_pop(state, 1);
  73. static_cast<void>(luaL_argerror(state, 2, "object burst list is not a table"));
  74. return false;
  75. }
  76. const int list = lua_gettop(state);
  77. const lua_Integer count = static_cast<lua_Integer>(lua_rawlen(state, list));
  78. if (count < 0
  79. || static_cast<std::size_t>(count)
  80. > ::sunrise::state::activity::mission::kIntentBurstCapacity) {
  81. lua_pop(state, 1);
  82. static_cast<void>(luaL_argerror(state, 2, "object burst list is too long"));
  83. return false;
  84. }
  85. for (lua_Integer entry = 1; entry <= count; ++entry) {
  86. lua_rawgeti(state, list, entry);
  87. SlotDefinition member{};
  88. const bool resolved =
  89. resolve_slot(state, lua_gettop(state), member) && exact_object_slot(member);
  90. lua_pop(state, 1);
  91. if (!resolved) {
  92. lua_pop(state, 1);
  93. static_cast<void>(
  94. luaL_argerror(state, 2, "object burst names a slot that is not an exact type-4"));
  95. return false;
  96. }
  97. intent.burstRows[static_cast<std::size_t>(entry - 1)] = member.nativeRow;
  98. }
  99. lua_pop(state, 1);
  100. intent.burstRowCount = static_cast<std::uint8_t>(count);
  101. return true;
  102. }
  103. /** The occupancy Auth body is a fixed 87 bits: a 55-bit client reference then one int32. */
  104. constexpr std::size_t kOccupancyAuthBitCount = 87;
  105. constexpr std::size_t kOccupancyAuthByteCount = 11;
  106. /** Volumes one filter may test. Every predicate still shares the type-34 capacity. */
  107. constexpr std::size_t kMaximumFilterVolumes = 5;
  108. /** Type-34 predicate modes: 0 tests the flag or reference as given, 1 tests inside a volume. */
  109. constexpr std::int8_t kFilterModeDirect = 0;
  110. constexpr std::int8_t kFilterModeInside = 1;
  111. /** Appends one predicate. @return False when the type-34 body is already full. */
  112. [[nodiscard]] bool append_predicate(scriptable_auth::Type34Body& body,
  113. const scriptable_auth::Type34Predicate& predicate) noexcept {
  114. if (body.count >= scriptable_auth::kType34PredicateCapacity) {
  115. return false;
  116. }
  117. body.predicates[body.count++] = predicate;
  118. return true;
  119. }
  120. } // namespace
  121. /**
  122. * Sets the object filter and caller value carried by one type-30 condition. Without a filter the
  123. * reference is absent, and the client measures its default player set.
  124. */
  125. [[nodiscard]] int slot_set_occupancy_condition(lua_State* state) {
  126. const auto* const handle =
  127. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  128. // Named arguments this call accepts. Any other key is refused.
  129. static constexpr std::array<std::string_view, 2> kDeclared{"filter", "value"};
  130. refuse_unknown_arguments(state, kDeclared);
  131. SlotHandle reference{};
  132. const bool filtered = optional_argument(state, "filter", kSlotMetatable, reference);
  133. const lua_Integer value = checked_integer_argument(state, "value");
  134. SlotDefinition slot{};
  135. SlotDefinition playerSet{};
  136. if (!current_slot(state, *handle, slot)
  137. || (filtered && !current_slot(state, reference, playerSet))) {
  138. return luaL_error(state, "activity slot is stale or invalid");
  139. }
  140. if (!exact_occupancy_slot(slot)) {
  141. return luaL_error(state, "activity slot is not an exact type-30 occupancy condition");
  142. }
  143. // The Auth field is a full-range int32 and lua_Integer is wider, so the lane is still checked.
  144. if (value < (std::numeric_limits<std::int32_t>::min)()
  145. || value > (std::numeric_limits<std::int32_t>::max)()) {
  146. return luaL_error(state, "value must be a 32-bit signed integer");
  147. }
  148. std::array<std::byte, kOccupancyAuthByteCount> body{};
  149. middleware::encoding::bits::Writer writer(body);
  150. const std::uint32_t encodedValue =
  151. std::bit_cast<std::uint32_t>(static_cast<std::int32_t>(value)) + 0x80000000U;
  152. const bool referenceWritten =
  153. filtered ? writer.write(playerSet.registryKey, 32)
  154. && writer.write(static_cast<std::uint32_t>(playerSet.slotType) + 1U, 7)
  155. && writer.write(static_cast<std::uint32_t>(playerSet.slotIndex) + 32768U, 16)
  156. : auth_fields::write_absent_client_ref(writer);
  157. if (!referenceWritten || !writer.write(encodedValue, 32)
  158. || writer.bit_count() != kOccupancyAuthBitCount) {
  159. return luaL_error(state, "occupancy condition native encoder failed");
  160. }
  161. return queue_slot_auth(state, slot, format::kOccupancyAuthSchema, kOccupancyAuthBitCount, body);
  162. }
  163. /** Enables the native darkness restriction; roster assembly supplies its matching bubble. */
  164. [[nodiscard]] int slot_set_darkness_zone(lua_State* state) {
  165. namespace darkness = middleware::bap::activity_message::darkness_zone;
  166. const auto* const handle =
  167. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  168. // Only these named arguments belong to this API.
  169. static constexpr std::array<std::string_view, 2> kDeclared{"enabled", "wipe_seconds"};
  170. refuse_unknown_arguments(state, kDeclared);
  171. SlotDefinition slot{};
  172. if (!current_slot(state, *handle, slot) || slot.slotType != darkness::kSlotType
  173. || slot.componentClass != darkness::kComponentClass || slot.authSchema != darkness::kSchema
  174. || (slot.flags & format::kSlotSchemaJoinExact) == 0) {
  175. return luaL_error(state, "darkness zone requires the exact hard-wipe globals sensor");
  176. }
  177. const lua_Integer wipe = optional_integer_argument(state, "wipe_seconds", darkness::kNoWipe);
  178. std::array<std::byte, darkness::kBytes> body{};
  179. if (wipe < darkness::kNoWipe || wipe > darkness::kMaximumWipeSeconds
  180. || !darkness::encode(
  181. optional_boolean_argument(state, "enabled", false), body, static_cast<int>(wipe))) {
  182. return luaL_error(state, "darkness zone encoder failed");
  183. }
  184. return queue_slot_auth(state, slot, darkness::kSchema, darkness::kBits, body);
  185. }
  186. /**
  187. * Native typed object filters: players, one or several objects, a squad, and volume intersection.
  188. * Every predicate shares the type-34 capacity; a filter that would exceed it is refused.
  189. */
  190. [[nodiscard]] int slot_set_object_filter(lua_State* state) {
  191. namespace auth = scriptable_auth;
  192. const auto* const handle =
  193. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  194. // Only these named arguments belong to this API.
  195. static constexpr std::array<std::string_view, 6> kDeclared{
  196. "players", "target", "targets", "squad", "inside", "inside_any"};
  197. refuse_unknown_arguments(state, kDeclared);
  198. SlotDefinition slot{};
  199. if (!current_slot(state, *handle, slot) || slot.slotType != auth::kType34SlotType
  200. || slot.authSchema != auth::kType34Schema) {
  201. return luaL_error(state, "object filter requires an authored type-34 sensor");
  202. }
  203. constexpr const char* kCapacityError = "object filter exceeds the type-34 predicate capacity";
  204. auth::Type34Body body{};
  205. lua_getfield(state, 2, "inside_any");
  206. const bool volumes = !lua_isnil(state, -1);
  207. if (volumes) {
  208. luaL_checktype(state, -1, LUA_TTABLE);
  209. const std::size_t count = lua_rawlen(state, -1);
  210. if (count == 0 || count > kMaximumFilterVolumes) {
  211. return luaL_error(state, "inside_any volume count is outside the filter capacity");
  212. }
  213. for (std::size_t index = 1; index <= count; ++index) {
  214. lua_rawgeti(state, -1, static_cast<lua_Integer>(index));
  215. const auto* const volumeHandle =
  216. static_cast<const SlotHandle*>(luaL_checkudata(state, -1, kSlotMetatable));
  217. SlotDefinition volume{};
  218. if (!current_slot(state, *volumeHandle, volume)
  219. || volume.slotType != auth::kType60SlotType) {
  220. return luaL_error(state, "inside_any requires authored type-60 volumes");
  221. }
  222. if (!append_predicate(
  223. body,
  224. auth::Type34ModeFlagSlotRef{kFilterModeDirect,
  225. true,
  226. {volume.registryKey,
  227. static_cast<std::int8_t>(auth::kType60SlotType),
  228. static_cast<std::int16_t>(volume.slotIndex)}})) {
  229. return luaL_error(state, kCapacityError);
  230. }
  231. lua_pop(state, 1);
  232. }
  233. }
  234. lua_pop(state, 1);
  235. if (optional_boolean_argument(state, "players", false)
  236. && !append_predicate(body, auth::Type34ModeOnlyB{static_cast<std::int8_t>(volumes)})) {
  237. return luaL_error(state, kCapacityError);
  238. }
  239. auth::Type2LaneClientRef target{};
  240. if (!optional_slot_reference(state, "target", auth::kType4SlotType, target)) {
  241. return luaL_error(state, "filter target must be an authored type-4 object");
  242. }
  243. if (target.slotIndex >= 0
  244. && !append_predicate(body, auth::Type34ModeSlotRefC{kFilterModeDirect, target})) {
  245. return luaL_error(state, kCapacityError);
  246. }
  247. // Several authored objects at once (a beam hop-on names every crystal it tethers).
  248. lua_getfield(state, 2, "targets");
  249. if (!lua_isnil(state, -1)) {
  250. luaL_checktype(state, -1, LUA_TTABLE);
  251. const std::size_t count = lua_rawlen(state, -1);
  252. if (count == 0) {
  253. return luaL_error(state, "targets must name at least one object");
  254. }
  255. for (std::size_t index = 1; index <= count; ++index) {
  256. lua_rawgeti(state, -1, static_cast<lua_Integer>(index));
  257. const auto* const targetHandle =
  258. static_cast<const SlotHandle*>(luaL_checkudata(state, -1, kSlotMetatable));
  259. SlotDefinition object{};
  260. if (!current_slot(state, *targetHandle, object)
  261. || object.slotType != auth::kType4SlotType
  262. || object.slotIndex > auth_fields::kMaximumClientRefIndex) {
  263. return luaL_error(state, "targets require authored type-4 objects");
  264. }
  265. if (!append_predicate(
  266. body,
  267. auth::Type34ModeSlotRefC{kFilterModeDirect,
  268. {object.registryKey,
  269. static_cast<std::int8_t>(auth::kType4SlotType),
  270. static_cast<std::int16_t>(object.slotIndex)}})) {
  271. return luaL_error(state, kCapacityError);
  272. }
  273. lua_pop(state, 1);
  274. }
  275. }
  276. lua_pop(state, 1);
  277. // A squad reference selects the boss a shield or tether hop-on hosts on.
  278. auth::Type2LaneClientRef squad{};
  279. if (!optional_slot_reference(state, "squad", format::kSquadSlotType, squad)) {
  280. return luaL_error(state, "filter squad must be an authored type-1 squad");
  281. }
  282. if (squad.slotIndex >= 0
  283. && !append_predicate(body, auth::Type34ModeSlotRefC{kFilterModeDirect, squad})) {
  284. return luaL_error(state, kCapacityError);
  285. }
  286. auth::Type2LaneClientRef inside{};
  287. if (!optional_slot_reference(state, "inside", auth::kType60SlotType, inside)) {
  288. return luaL_error(state, "filter inside must be an authored type-60 volume");
  289. }
  290. if (inside.slotIndex >= 0
  291. && !append_predicate(body, auth::Type34ModeFlagSlotRef{kFilterModeInside, false, inside})) {
  292. return luaL_error(state, kCapacityError);
  293. }
  294. std::array<std::byte, auth::kType34MaximumByteCount> bytes{};
  295. std::size_t written = 0;
  296. std::size_t bits = 0;
  297. if (!auth::encode_type34(body, bytes, written, bits)) {
  298. return luaL_error(state, "object filter encoder failed");
  299. }
  300. return queue_slot_auth(state, slot, auth::kType34Schema, bits, std::span(bytes).first(written));
  301. }
  302. /** Binds an authored damage monitor to one exact object; a new revision re-binds it. */
  303. [[nodiscard]] int slot_watch_damage(lua_State* state) {
  304. namespace damage = middleware::bap::activity_message::damage_monitor;
  305. const auto* const handle =
  306. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  307. // Only these named arguments belong to this API.
  308. static constexpr std::array<std::string_view, 1> kDeclared{"target"};
  309. refuse_unknown_arguments(state, kDeclared);
  310. SlotDefinition slot{};
  311. if (!current_slot(state, *handle, slot) || slot.slotType != damage::kSlotType
  312. || slot.authSchema != damage::kAuthSchema) {
  313. return luaL_error(state, "damage watch requires an authored type-20 monitor");
  314. }
  315. lua_getfield(state, 2, "target");
  316. const auto* const targetHandle =
  317. static_cast<const SlotHandle*>(luaL_checkudata(state, -1, kSlotMetatable));
  318. SlotDefinition target{};
  319. const bool exact = current_slot(state, *targetHandle, target) && exact_object_slot(target);
  320. lua_pop(state, 1);
  321. if (!exact) {
  322. return luaL_error(state, "damage target must be an authored type-4 object");
  323. }
  324. std::array<std::byte, damage::kBytes> body{};
  325. std::size_t written = 0;
  326. if (!damage::encode(target.registryKey,
  327. static_cast<std::uint16_t>(target.slotIndex),
  328. kUncommittedCounter,
  329. body,
  330. written)) {
  331. return luaL_error(state, "damage monitor encoder failed");
  332. }
  333. return queue_slot_auth(
  334. state, slot, damage::kAuthSchema, damage::kBits, body, IntentKind::watchDamage);
  335. }
  336. /** Spawns authored entry zero and subscribes to accepted native player use. */
  337. [[nodiscard]] int slot_set_interactable_object(lua_State* state) {
  338. namespace object = middleware::bap::activity_message::interactable_object;
  339. const auto* const handle =
  340. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  341. // Only these named arguments belong to this API.
  342. static constexpr std::array<std::string_view, 3> kDeclared{"track_owner", "active", "used"};
  343. refuse_unknown_arguments(state, kDeclared);
  344. SlotDefinition slot{};
  345. if (!current_slot(state, *handle, slot) || !exact_object_slot(slot)) {
  346. return luaL_error(state, "interaction requires an exact authored object");
  347. }
  348. const bool trackOwner = optional_boolean_argument(state, "track_owner", false);
  349. std::array<std::byte, object::kOwnerBytes> body{};
  350. std::size_t written = 0;
  351. if (!object::encode(kUncommittedCounter,
  352. body,
  353. written,
  354. trackOwner,
  355. optional_boolean_argument(state, "active", true),
  356. optional_boolean_argument(state, "used", false))) {
  357. return luaL_error(state, "interactable object encoder failed");
  358. }
  359. return queue_slot_auth(state,
  360. slot,
  361. object::kSchema,
  362. trackOwner ? object::kOwnerBits : object::kBits,
  363. std::span(body).first(written),
  364. IntentKind::setInteractableObject);
  365. }
  366. /** Arms or disarms the authored Ghost-link scan. The Host stamps the generation this body needs. */
  367. [[nodiscard]] int slot_set_ghost_link(lua_State* state) {
  368. namespace ghost = middleware::bap::activity_message::ghost_link;
  369. const auto* const handle =
  370. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  371. // Only this named argument belongs to this API.
  372. static constexpr std::array<std::string_view, 1> kDeclared{"active"};
  373. refuse_unknown_arguments(state, kDeclared);
  374. SlotDefinition slot{};
  375. if (!current_slot(state, *handle, slot) || !exact_ghost_link_slot(slot)) {
  376. return luaL_error(state, "activity slot is not an exact type-65 Ghost-link sensor");
  377. }
  378. std::array<std::byte, ghost::kByteCount> body{};
  379. std::size_t written = 0;
  380. if (!ghost::encode(
  381. kUncommittedCounter, optional_boolean_argument(state, "active", true), body, written)) {
  382. return luaL_error(state, "Ghost-link encoder failed");
  383. }
  384. return queue_slot_auth(state,
  385. slot,
  386. ghost::kAuthSchema,
  387. ghost::kBitCount,
  388. std::span(body).first(written),
  389. IntentKind::setGhostLink);
  390. }
  391. /**
  392. * Reads the level the client last reported for one Ghost link.
  393. * @return The generation, progress and active fields, or nil before all three were reported.
  394. */
  395. [[nodiscard]] int slot_ghost_link(lua_State* state) {
  396. const auto* const handle =
  397. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  398. // The read has no parameters, so an unsafe call cannot be spelled.
  399. static constexpr std::array<std::string_view, 0> kDeclared{};
  400. refuse_unknown_arguments(state, kDeclared);
  401. SlotDefinition slot{};
  402. if (!current_slot(state, *handle, slot) || !exact_ghost_link_slot(slot)) {
  403. return luaL_error(state, "activity slot is not an exact type-65 Ghost-link sensor");
  404. }
  405. const Impl* const impl = impl_from_state(state);
  406. for (std::size_t index = 0; index < impl->ghostLevelCount; ++index) {
  407. const GhostLinkRow& row = impl->ghostLevels[index];
  408. if (row.registryKey != slot.registryKey || row.objectTag != slot.objectTag
  409. || static_cast<std::uint32_t>(row.slotIndex) != slot.slotIndex
  410. || row.level.seen != kGhostSeenAll) {
  411. continue;
  412. }
  413. lua_createtable(state, 0, 3);
  414. lua_pushinteger(state, row.level.generation);
  415. lua_setfield(state, -2, "generation");
  416. lua_pushnumber(state, row.level.progress);
  417. lua_setfield(state, -2, "progress");
  418. lua_pushboolean(state, row.level.active);
  419. lua_setfield(state, -2, "active");
  420. return 1;
  421. }
  422. lua_pushnil(state);
  423. return 1;
  424. }
  425. /** Instantiates or removes the package-owned entry one type-4 slot selects. */
  426. [[nodiscard]] int slot_set_object_active(lua_State* state) {
  427. const auto* const handle =
  428. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  429. SlotDefinition definition{};
  430. if (!current_slot(state, *handle, definition) || !exact_object_slot(definition)) {
  431. return luaL_error(state, "activity slot is not an exact type-4 authored object");
  432. }
  433. // Named arguments this call accepts. Any other key is refused.
  434. static constexpr std::array<std::string_view, 2> kDeclared{"active", "with"};
  435. refuse_unknown_arguments(state, kDeclared);
  436. CallFrame& frame = active_frame(state);
  437. Intent intent{};
  438. intent.kind = IntentKind::setObjectActive;
  439. intent.firstRow = definition.nativeRow;
  440. intent.entryIndex = 0;
  441. intent.active = optional_boolean_argument(state, "active", true);
  442. if (!parse_object_burst(state, intent)) {
  443. return 0;
  444. }
  445. return queue_intent(state, frame, intent);
  446. }
  447. /** Sets one verified type-23 channel through the same guarded route. */
  448. [[nodiscard]] int slot_set_channel(lua_State* state) {
  449. const auto* const handle =
  450. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  451. SlotDefinition definition{};
  452. if (!current_slot(state, *handle, definition)) {
  453. return luaL_error(state, "activity slot is stale or invalid");
  454. }
  455. if (!exact_device_slot(definition)) {
  456. return luaL_error(state, "activity slot is not an exact type-23 device");
  457. }
  458. // Both parameters carry their own bound, so neither the lane nor the range is tested here.
  459. static constexpr std::array<std::string_view, 3> kDeclared{"channel", "value", "snap"};
  460. refuse_unknown_arguments(state, kDeclared);
  461. const DeviceChannelHandle channel =
  462. checked_argument<DeviceChannelHandle>(state, "channel", kDeviceChannelMetatable);
  463. const UnitScalarHandle value =
  464. checked_argument<UnitScalarHandle>(state, "value", kUnitScalarMetatable);
  465. const bool snap = optional_boolean_argument(state, "snap", false);
  466. CallFrame& frame = active_frame(state);
  467. Intent intent{};
  468. intent.kind = IntentKind::setDeviceChannel;
  469. intent.firstRow = definition.nativeRow;
  470. intent.deviceValue = value.value;
  471. intent.deviceChannel = channel.channel;
  472. intent.deviceSnap = snap;
  473. return queue_intent(state, frame, intent);
  474. }
  475. /** Applies one named device transition through the same guarded channel route. */
  476. [[nodiscard]] int slot_transition(lua_State* state) {
  477. const auto* const handle =
  478. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  479. SlotDefinition definition{};
  480. if (!current_slot(state, *handle, definition)) {
  481. return luaL_error(state, "activity slot is stale or invalid");
  482. }
  483. if (!exact_device_slot(definition)) {
  484. return luaL_error(state, "activity slot is not an exact type-23 device");
  485. }
  486. // The handle is a row of the closed vocabulary, so no word is matched here.
  487. static constexpr std::array<std::string_view, 2> kDeclared{"transition", "snap"};
  488. refuse_unknown_arguments(state, kDeclared);
  489. const DeviceTransitionHandle requested =
  490. checked_argument<DeviceTransitionHandle>(state, "transition", kDeviceTransitionMetatable);
  491. const bool snap = optional_boolean_argument(state, "snap", false);
  492. const DeviceTransition& transition = kDeviceTransitions[requested.row];
  493. CallFrame& frame = active_frame(state);
  494. Intent intent{};
  495. intent.kind = IntentKind::setDeviceChannel;
  496. intent.firstRow = definition.nativeRow;
  497. intent.deviceValue = transition.value;
  498. intent.deviceChannel = static_cast<std::uint8_t>(transition.channel);
  499. intent.deviceSnap = snap;
  500. return queue_intent(state, frame, intent);
  501. }
  502. /**
  503. * Fires one type-31 configured trigger. One authored typed reference must be live and eligible.
  504. * The pulse carries no caller value. `enabled` is fixed true and the auxiliary stays zero, and
  505. * the Host mints the generation from the target's own guard so a replay cannot reorder.
  506. */
  507. [[nodiscard]] int slot_fire_trigger(lua_State* state) {
  508. const auto* const handle =
  509. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  510. SlotDefinition definition{};
  511. if (!current_slot(state, *handle, definition)) {
  512. return luaL_error(state, "activity slot is stale or invalid");
  513. }
  514. if (!exact_trigger_slot(definition)) {
  515. return luaL_error(state, "activity slot is not an exact type-31 trigger");
  516. }
  517. // The pulse has no parameters, so an unsafe call cannot be spelled.
  518. static constexpr std::array<std::string_view, 0> kDeclared{};
  519. refuse_unknown_arguments(state, kDeclared);
  520. CallFrame& frame = active_frame(state);
  521. Intent intent{};
  522. intent.kind = IntentKind::fireTrigger;
  523. intent.firstRow = definition.nativeRow;
  524. intent.active = true;
  525. return queue_intent(state, frame, intent);
  526. }
  527. /** Disarms one type-31 trigger, so an arm after its first fire stops reporting. */
  528. [[nodiscard]] int slot_disarm_trigger(lua_State* state) {
  529. const auto* const handle =
  530. static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  531. SlotDefinition definition{};
  532. if (!current_slot(state, *handle, definition) || !exact_trigger_slot(definition)) {
  533. return luaL_error(state, "activity slot is not an exact type-31 trigger");
  534. }
  535. // The disarm has no parameters, so any named argument is refused.
  536. static constexpr std::array<std::string_view, 0> kDeclared{};
  537. refuse_unknown_arguments(state, kDeclared);
  538. Intent intent{};
  539. intent.kind = IntentKind::fireTrigger;
  540. intent.firstRow = definition.nativeRow;
  541. intent.active = false;
  542. return queue_intent(state, active_frame(state), intent);
  543. }
  544. /**
  545. * A newer desired command hides every earlier applied result on the same device channel.
  546. * @param state Lua call holding the device slot and named channel argument.
  547. * @return True only after a report satisfied the latest desired request.
  548. */
  549. int slot_applied(lua_State* state) {
  550. const auto* handle = static_cast<const SlotHandle*>(luaL_checkudata(state, 1, kSlotMetatable));
  551. // Only these named arguments belong to this API.
  552. static constexpr std::array<std::string_view, 1> kDeclared{"channel"};
  553. refuse_unknown_arguments(state, kDeclared);
  554. const auto channel =
  555. checked_argument<DeviceChannelHandle>(state, "channel", kDeviceChannelMetatable);
  556. SlotDefinition slot{};
  557. if (!current_slot(state, *handle, slot)
  558. || slot.slotType != ::sunrise::state::activity_sdk::format::kDeviceSlotType) {
  559. return luaL_error(state, "device completion requires an authored device slot");
  560. }
  561. const Impl* impl = impl_from_state(state);
  562. const auto pending = [&](const auto& intent) noexcept {
  563. return intent.kind == IntentKind::setDeviceChannel && intent.firstRow == slot.nativeRow
  564. && intent.deviceChannel == channel.channel
  565. && intent.attemptGeneration == impl->attempt.generation;
  566. };
  567. for (const auto& intent : active_frame(state).candidate.intents) {
  568. if (pending(intent)) {
  569. lua_pushboolean(state, false);
  570. return 1;
  571. }
  572. }
  573. for (std::size_t index = impl->outboxRead; index < impl->outbox.size(); ++index) {
  574. if (pending(impl->outbox[index])) {
  575. lua_pushboolean(state, false);
  576. return 1;
  577. }
  578. }
  579. bool applied = false;
  580. for (const auto& request : impl->deviceRequests) {
  581. if (request.slotRow == slot.nativeRow && request.channel == channel.channel
  582. && request.attemptGeneration == impl->attempt.generation
  583. && impl->deviceRequestGeneration != 0
  584. && request.sourceGeneration == impl->deviceRequestGeneration) {
  585. applied = request.applied;
  586. break;
  587. }
  588. }
  589. lua_pushboolean(state, applied);
  590. return 1;
  591. }
  592. } // namespace sunrise::server::activity::mission::lua_vm::detail