mission_script_sdk_bridge.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. #include "mission_script_sdk_bridge.h"
  2. #include <algorithm>
  3. #include <array>
  4. #include <cstddef>
  5. #include <cstdint>
  6. #include <cstdio>
  7. #include <cstring>
  8. #include <limits>
  9. #include <span>
  10. #include <string_view>
  11. #include "../../../state/build_data/scriptables/inline_name_evidence.h"
  12. #include "../activity_sdk_device_runtime.h"
  13. #include "../activity_sdk_mission_runtime.h"
  14. #include "../activity_sdk_scene_spawn.h"
  15. #include "mission_script_actor_ability_sdk.h"
  16. #include "mission_script_catalog_sdk_bridge.h"
  17. #include "mission_script_combat_objective_sdk.h"
  18. #include "mission_script_message_catalog.h"
  19. #include "mission_script_slot_index.h"
  20. namespace sunrise::server::activity::mission::sdk_bridge {
  21. namespace {
  22. namespace sdk = state::activity_sdk;
  23. namespace format = state::activity_sdk::format;
  24. namespace scenes = activity_sdk_mission;
  25. // Every occurrence text id starts with this family prefix.
  26. constexpr std::string_view kOccurrencePrefix = "object-occurrence/";
  27. [[nodiscard]] const sdk::BoundView* context_view(const void* context) noexcept {
  28. return static_cast<const sdk::BoundView*>(context);
  29. }
  30. [[nodiscard]] bool valid_view(const sdk::BoundView* view) noexcept {
  31. return view != nullptr && view->catalog != nullptr && sdk::bound_activity(*view) != nullptr
  32. && sdk::bound_scenario(*view) != nullptr;
  33. }
  34. [[nodiscard]] const format::Activity* bound_activity(const sdk::BoundView* view) noexcept {
  35. return view != nullptr && view->catalog != nullptr ? sdk::bound_activity(*view) : nullptr;
  36. }
  37. /** Copies only authenticated pack fields; no mapped pointer is exposed to Lua. */
  38. [[nodiscard]] bool resolve_activity_binding(const void* context,
  39. lua_vm::ActivityBindingDefinition& output) noexcept {
  40. output = {};
  41. const sdk::BoundView* const view = context_view(context);
  42. const format::Activity* const activity = bound_activity(view);
  43. return activity != nullptr && activity_binding_definition(*view->catalog, *activity, output);
  44. }
  45. [[nodiscard]] std::size_t activity_binding_tag_count(const void* context,
  46. lua_vm::ActivityBindingTagKind kind) noexcept {
  47. const sdk::BoundView* const view = context_view(context);
  48. const format::Activity* const activity = bound_activity(view);
  49. return activity != nullptr ? activity_binding_tags(*view->catalog, *activity, kind).size() : 0;
  50. }
  51. /** Resolves the package tag of the activity binding the view names. */
  52. [[nodiscard]] bool resolve_activity_binding_tag(const void* context,
  53. lua_vm::ActivityBindingTagKind kind,
  54. std::uint32_t localRow,
  55. std::uint32_t& output) noexcept {
  56. output = 0;
  57. const sdk::BoundView* const view = context_view(context);
  58. if (view == nullptr || view->catalog == nullptr || localRow == 0) {
  59. return false;
  60. }
  61. const format::Activity* const activity = sdk::bound_activity(*view);
  62. if (activity == nullptr) {
  63. return false;
  64. }
  65. const auto rows = activity_binding_tags(*view->catalog, *activity, kind);
  66. if (localRow > rows.size()) {
  67. return false;
  68. }
  69. output = rows[localRow - 1U].tag;
  70. return output != 0;
  71. }
  72. [[nodiscard]] std::span<const format::ActivityBindingLocator>
  73. binding_locators(const sdk::BoundView& view) noexcept {
  74. const format::Activity* const activity = sdk::bound_activity(view);
  75. return activity != nullptr ? sdk::activity_binding_locators(*view.catalog, *activity)
  76. : std::span<const format::ActivityBindingLocator>{};
  77. }
  78. [[nodiscard]] std::size_t activity_binding_locator_count(const void* context) noexcept {
  79. const sdk::BoundView* const view = context_view(context);
  80. return view != nullptr && view->catalog != nullptr ? binding_locators(*view).size() : 0;
  81. }
  82. /** Copies one 1-based binding locator of the bound activity. @return False when absent. */
  83. [[nodiscard]] bool
  84. resolve_activity_binding_locator(const void* context,
  85. std::uint32_t localRow,
  86. lua_vm::ActivityBindingLocatorDefinition& output) noexcept {
  87. output = {};
  88. const sdk::BoundView* const view = context_view(context);
  89. if (view == nullptr || view->catalog == nullptr || localRow == 0) {
  90. return false;
  91. }
  92. const auto rows = binding_locators(*view);
  93. if (localRow > rows.size()) {
  94. return false;
  95. }
  96. const format::ActivityBindingLocator& row = rows[localRow - 1U];
  97. output = {.tag = row.tag, .offset = row.offset, .localRow = localRow};
  98. return row.tag != 0;
  99. }
  100. /** Rejects incomplete squad rows so Lua never receives a partly usable definition. */
  101. [[nodiscard]] bool squad_definition(const sdk::BoundView& view,
  102. const format::Squad& squad,
  103. std::uint32_t localRow,
  104. lua_vm::SquadDefinition& output) noexcept {
  105. output = {};
  106. const sdk::Catalog& catalog = *view.catalog;
  107. const auto allSquads = catalog.squads();
  108. const auto members = sdk::squad_members(catalog, squad);
  109. if (members.empty() || members.size() > output.defaultCounts.size()
  110. || (squad.flags & format::kSquadRunnableMask) != format::kSquadRunnableMask
  111. || &squad < allSquads.data() || &squad >= allSquads.data() + allSquads.size()) {
  112. return false;
  113. }
  114. for (std::size_t index = 0; index < members.size(); ++index) {
  115. output.defaultCounts[index] = members[index].defaultCount;
  116. }
  117. output.id = catalog.string(squad.id);
  118. const auto slots = catalog.slots();
  119. if (squad.slotIndex >= slots.size()) {
  120. return false;
  121. }
  122. output.name = catalog.string(slots[squad.slotIndex].name);
  123. output.nativeRow = static_cast<std::uint32_t>(&squad - allSquads.data());
  124. output.localRow = localRow;
  125. output.memberCount = members.size();
  126. return !output.id.empty();
  127. }
  128. /** Treats Lua squad rows as one-based positions inside the bound scenario. */
  129. [[nodiscard]] bool resolve_squad_row(const void* context,
  130. std::uint32_t localRow,
  131. lua_vm::SquadDefinition& output) noexcept {
  132. output = {};
  133. const sdk::BoundView* const view = context_view(context);
  134. if (!valid_view(view) || localRow == 0) {
  135. return false;
  136. }
  137. const format::Scenario* const scenario = sdk::bound_scenario(*view);
  138. const auto squads = sdk::scenario_squads(*view->catalog, *scenario);
  139. return localRow <= squads.size()
  140. && squad_definition(*view, squads[localRow - 1], localRow, output);
  141. }
  142. /** Refuses squad names and aliases that do not resolve to one runnable squad. */
  143. [[nodiscard]] bool resolve_squad_id(const void* context,
  144. std::string_view id,
  145. lua_vm::SquadDefinition& output) noexcept {
  146. output = {};
  147. const sdk::BoundView* const view = context_view(context);
  148. if (!valid_view(view) || id.empty()) {
  149. return false;
  150. }
  151. const format::Scenario* const scenario = sdk::bound_scenario(*view);
  152. const auto squads = sdk::scenario_squads(*view->catalog, *scenario);
  153. std::size_t matches = 0;
  154. lua_vm::SquadDefinition selected{};
  155. for (std::size_t index = 0; index < squads.size(); ++index) {
  156. const auto slots = view->catalog->slots();
  157. if (squads[index].slotIndex >= slots.size()) {
  158. return false;
  159. }
  160. const format::Slot& slot = slots[squads[index].slotIndex];
  161. bool names = view->catalog->string(squads[index].id) == id
  162. || view->catalog->string(slot.id) == id
  163. || view->catalog->string(slot.name) == id;
  164. for (const format::Text& alias : sdk::slot_aliases(*view->catalog, slot)) {
  165. names = names || view->catalog->string(alias.value) == id;
  166. }
  167. if (!names) {
  168. continue;
  169. }
  170. lua_vm::SquadDefinition candidate{};
  171. if (!squad_definition(
  172. *view, squads[index], static_cast<std::uint32_t>(index + 1), candidate)) {
  173. continue;
  174. }
  175. selected = candidate;
  176. ++matches;
  177. }
  178. if (matches != 1) {
  179. return false;
  180. }
  181. output = selected;
  182. return true;
  183. }
  184. /** Rejects cross-table rows before exposing stable native indices to Lua. */
  185. [[nodiscard]] bool slot_definition(const sdk::BoundView& view,
  186. const format::Object& object,
  187. const format::Slot& slot,
  188. std::uint32_t localRow,
  189. lua_vm::SlotDefinition& output) noexcept {
  190. output = {};
  191. const sdk::Catalog& catalog = *view.catalog;
  192. const auto allObjects = catalog.objects();
  193. const auto allSlots = catalog.slots();
  194. if (&object < allObjects.data() || &object >= allObjects.data() + allObjects.size()
  195. || &slot < allSlots.data() || &slot >= allSlots.data() + allSlots.size()
  196. || slot.objectIndex != static_cast<std::uint32_t>(&object - allObjects.data())) {
  197. return false;
  198. }
  199. output.id = catalog.string(slot.id);
  200. output.name = catalog.string(slot.name);
  201. output.objectId = catalog.string(object.id);
  202. output.senseSchemaId = catalog.string(slot.senseSchemaId);
  203. output.authSchemaId = catalog.string(slot.authSchemaId);
  204. output.nativeRow = static_cast<std::uint32_t>(&slot - allSlots.data());
  205. output.localRow = localRow;
  206. output.objectTag = object.objectTag;
  207. output.registryKey = object.objectKey;
  208. output.slotIndex = slot.slotIndex;
  209. output.slotType = slot.slotType;
  210. output.componentClass = slot.componentClass;
  211. output.senseSchema = slot.senseSchema;
  212. output.authSchema = slot.authSchema;
  213. output.flags = slot.flags;
  214. return !output.id.empty() && !output.objectId.empty();
  215. }
  216. /** Fills the name hash and the bubble hash from the object's first scenario occurrence. */
  217. void fill_marker_hashes(const sdk::BoundView& view,
  218. const format::Scenario& scenario,
  219. lua_vm::SlotDefinition& output) noexcept {
  220. const sdk::Catalog& catalog = *view.catalog;
  221. const std::string_view name = output.name;
  222. output.nameHash = state::build_data::scriptables::inline_name_evidence::hash(
  223. std::as_bytes(std::span<const char>(name.data(), name.size())));
  224. // A navpoint marker names its bubble; the client routes to the bubble from elsewhere.
  225. const std::uint32_t objectIndex = catalog.slots()[output.nativeRow].objectIndex;
  226. for (const format::Occurrence& occurrence : sdk::scenario_occurrences(catalog, scenario)) {
  227. if (occurrence.scenarioIndex != view.scenarioRow || occurrence.objectIndex != objectIndex) {
  228. continue;
  229. }
  230. for (const format::Bubble& bubble : sdk::scenario_bubbles(catalog, scenario)) {
  231. if (bubble.scenarioIndex == view.scenarioRow
  232. && bubble.bubbleOrdinal == occurrence.bubbleIndex) {
  233. output.bubbleHash = bubble.nameHash;
  234. break;
  235. }
  236. }
  237. break;
  238. }
  239. }
  240. /** Copies the definition of one indexed slot of the bound scenario. */
  241. [[nodiscard]] bool indexed_slot(const sdk::BoundView& view,
  242. bool found,
  243. const slot_index::Found& row,
  244. lua_vm::SlotDefinition& output) noexcept {
  245. output = {};
  246. if (!found) {
  247. return false;
  248. }
  249. const sdk::Catalog& catalog = *view.catalog;
  250. const format::Slot& slot = catalog.slots()[row.nativeRow];
  251. if (!slot_definition(view, catalog.objects()[slot.objectIndex], slot, row.localRow, output)) {
  252. return false;
  253. }
  254. fill_marker_hashes(view, *sdk::bound_scenario(view), output);
  255. return true;
  256. }
  257. /** Treats Lua slot rows as one-based positions in the deduplicated scenario view. */
  258. [[nodiscard]] bool resolve_slot_row(const void* context,
  259. std::uint32_t localRow,
  260. lua_vm::SlotDefinition& output) noexcept {
  261. output = {};
  262. const sdk::BoundView* const view = context_view(context);
  263. slot_index::Found row{};
  264. return valid_view(view)
  265. && indexed_slot(*view, slot_index::by_row(*view, localRow, row), row, output);
  266. }
  267. /** Requires one exact slot ID, name, or alias in the bound scenario. */
  268. [[nodiscard]] bool
  269. resolve_slot_id(const void* context, std::string_view id, lua_vm::SlotDefinition& output) noexcept {
  270. output = {};
  271. const sdk::BoundView* const view = context_view(context);
  272. slot_index::Found row{};
  273. return valid_view(view) && indexed_slot(*view, slot_index::by_id(*view, id, row), row, output);
  274. }
  275. /** Incident sources omit a Sense schema; every authored identity field still has to agree. */
  276. [[nodiscard]] bool resolve_sense_slot(const void* context,
  277. const host::SenseObservationKey& key,
  278. lua_vm::SlotDefinition& output) noexcept {
  279. output = {};
  280. const sdk::BoundView* const view = context_view(context);
  281. if (!valid_view(view)) {
  282. return false;
  283. }
  284. const slot_index::SenseKey indexKey{.registryKey = key.registryKey,
  285. .objectTag = key.objectTag,
  286. .senseSchema = key.senseSchema,
  287. .slotIndex = key.slotIndex,
  288. .slotType = key.slotType};
  289. slot_index::Found row{};
  290. return indexed_slot(*view, slot_index::by_sense(*view, indexKey, row), row, output);
  291. }
  292. /** @return True when a catalog-global slot belongs to the bound scenario. */
  293. [[nodiscard]] bool scenario_has_slot(const sdk::BoundView& view, std::uint32_t slotRow) noexcept {
  294. if (!valid_view(&view) || slotRow >= view.catalog->slots().size()) {
  295. return false;
  296. }
  297. const sdk::Catalog& catalog = *view.catalog;
  298. const auto objects = catalog.objects();
  299. const format::Slot& selected = catalog.slots()[slotRow];
  300. for (const format::Occurrence& occurrence :
  301. sdk::scenario_occurrences(catalog, *sdk::bound_scenario(view))) {
  302. if (occurrence.objectIndex == selected.objectIndex
  303. && occurrence.objectIndex < objects.size()) {
  304. return true;
  305. }
  306. }
  307. return false;
  308. }
  309. /** Copies one exact generated task target into its value-owned VM shape. */
  310. [[nodiscard]] bool task_definition(const sdk::BoundView& view,
  311. const format::TaskTarget& row,
  312. std::uint32_t localRow,
  313. lua_vm::TaskSensorDefinition& output) noexcept {
  314. output = {};
  315. const format::Slot* const objective = sdk::task_linked_objective_slot(*view.catalog, row);
  316. if (objective == nullptr || !scenario_has_slot(view, row.taskSlotIndex)
  317. || !scenario_has_slot(view, row.objectiveSlotIndex)) {
  318. return false;
  319. }
  320. output.id = view.catalog->string(row.id);
  321. output.localRow = localRow;
  322. output.slotRow = row.taskSlotIndex;
  323. output.targetObjectKey = row.targetObjectKey;
  324. output.bitIndex = row.bitIndex;
  325. return !output.id.empty();
  326. }
  327. /** Treats task rows as one-based positions inside the bound scenario. */
  328. [[nodiscard]] bool resolve_task_sensor_row(const void* context,
  329. std::uint32_t localRow,
  330. lua_vm::TaskSensorDefinition& output) noexcept {
  331. output = {};
  332. const sdk::BoundView* const view = context_view(context);
  333. if (!valid_view(view) || localRow == 0) {
  334. return false;
  335. }
  336. std::uint32_t current = 0;
  337. for (const format::TaskTarget& row : view->catalog->task_targets()) {
  338. if (!scenario_has_slot(*view, row.taskSlotIndex)) {
  339. continue;
  340. }
  341. ++current;
  342. if (current == localRow) {
  343. return task_definition(*view, row, current, output);
  344. }
  345. }
  346. return false;
  347. }
  348. /** Resolves one generated task ID without accepting an ambiguous alias. */
  349. [[nodiscard]] bool resolve_task_sensor_id(const void* context,
  350. std::string_view id,
  351. lua_vm::TaskSensorDefinition& output) noexcept {
  352. output = {};
  353. const sdk::BoundView* const view = context_view(context);
  354. if (!valid_view(view) || id.empty()) {
  355. return false;
  356. }
  357. std::uint32_t current = 0;
  358. std::size_t matches = 0;
  359. lua_vm::TaskSensorDefinition selected{};
  360. for (const format::TaskTarget& row : view->catalog->task_targets()) {
  361. if (!scenario_has_slot(*view, row.taskSlotIndex)) {
  362. continue;
  363. }
  364. ++current;
  365. if (view->catalog->string(row.id) == id) {
  366. lua_vm::TaskSensorDefinition candidate{};
  367. if (task_definition(*view, row, current, candidate)) {
  368. selected = candidate;
  369. ++matches;
  370. }
  371. }
  372. }
  373. if (matches != 1) {
  374. return false;
  375. }
  376. output = selected;
  377. return true;
  378. }
  379. /** Copies one generated type-68 element into its crash-safe VM shape. */
  380. [[nodiscard]] bool directive_definition(const sdk::BoundView& view,
  381. const format::DirectiveElement& row,
  382. std::uint32_t localRow,
  383. lua_vm::DirectiveElementDefinition& output) noexcept {
  384. output = {};
  385. if (!scenario_has_slot(view, row.slotIndex) || row.nameHash == 0
  386. || row.nameHash == format::kAbsentIndex || row.elementIndex < 0
  387. || static_cast<std::uint32_t>(row.elementIndex) >= row.elementCount) {
  388. return false;
  389. }
  390. output.id = view.catalog->string(row.id);
  391. output.localRow = localRow;
  392. output.slotRow = row.slotIndex;
  393. output.nameHash = row.nameHash;
  394. output.elementIndex = row.elementIndex;
  395. output.elementCount = row.elementCount;
  396. output.flags = row.flags;
  397. return !output.id.empty();
  398. }
  399. /** Treats directive rows as one-based positions inside the bound scenario. */
  400. [[nodiscard]] bool
  401. resolve_directive_element_row(const void* context,
  402. std::uint32_t localRow,
  403. lua_vm::DirectiveElementDefinition& output) noexcept {
  404. output = {};
  405. const sdk::BoundView* const view = context_view(context);
  406. if (!valid_view(view) || localRow == 0) {
  407. return false;
  408. }
  409. std::uint32_t current = 0;
  410. for (const format::DirectiveElement& row : view->catalog->directive_elements()) {
  411. if (!scenario_has_slot(*view, row.slotIndex)) {
  412. continue;
  413. }
  414. ++current;
  415. if (current == localRow) {
  416. return directive_definition(*view, row, current, output);
  417. }
  418. }
  419. return false;
  420. }
  421. /** Resolves the exact type-68 slot/hash/index triple before the native dereference. */
  422. [[nodiscard]] bool resolve_directive_element(const void* context,
  423. std::uint32_t slotRow,
  424. std::uint32_t nameHash,
  425. std::int32_t elementIndex,
  426. lua_vm::DirectiveElementDefinition& output) noexcept {
  427. output = {};
  428. const sdk::BoundView* const view = context_view(context);
  429. if (!valid_view(view)) {
  430. return false;
  431. }
  432. std::uint32_t current = 0;
  433. std::size_t matches = 0;
  434. lua_vm::DirectiveElementDefinition selected{};
  435. for (const format::DirectiveElement& row : view->catalog->directive_elements()) {
  436. if (!scenario_has_slot(*view, row.slotIndex)) {
  437. continue;
  438. }
  439. ++current;
  440. if (row.slotIndex == slotRow && row.nameHash == nameHash
  441. && row.elementIndex == elementIndex) {
  442. lua_vm::DirectiveElementDefinition candidate{};
  443. if (directive_definition(*view, row, current, candidate)) {
  444. selected = candidate;
  445. ++matches;
  446. }
  447. }
  448. }
  449. if (matches != 1) {
  450. return false;
  451. }
  452. output = selected;
  453. return true;
  454. }
  455. /** Distinct state names the largest guard actor declares fit here with room to spare. */
  456. inline constexpr std::size_t kPerformanceStateCapacity = 256;
  457. /**
  458. * Resolves one state name on the squad a type-42 sensor drives. A zero hash selects the single
  459. * name the target declares. A given hash must be declared by every member that declares names.
  460. */
  461. [[nodiscard]] bool resolve_performance_state(const void* context,
  462. std::uint32_t slotRow,
  463. std::uint32_t nameHash,
  464. lua_vm::PerformanceStateDefinition& output) noexcept {
  465. output = {};
  466. const sdk::BoundView* const view = context_view(context);
  467. if (!valid_view(view) || !scenario_has_slot(*view, slotRow)) {
  468. return false;
  469. }
  470. const sdk::Catalog& catalog = *view->catalog;
  471. const format::Slot& slot = catalog.slots()[slotRow];
  472. if (slot.slotType != format::kPerformanceSlotType
  473. || slot.componentClass != format::kPerformanceComponentClass
  474. || slot.authSchema != format::kPerformanceAuthSchema
  475. || (slot.flags & format::kSlotSchemaJoinExact) == 0) {
  476. return false;
  477. }
  478. std::uint32_t squadSlot = format::kAbsentIndex;
  479. std::size_t edges = 0;
  480. for (const format::AuthoredSceneSquadEdge& edge :
  481. sdk::slot_authored_scene_squad_edges(catalog, slot)) {
  482. if ((edge.flags & format::kAuthoredSceneSquadPerformanceTargetExact) != 0) {
  483. squadSlot = edge.squadSlotIndex;
  484. ++edges;
  485. }
  486. }
  487. if (edges != 1) {
  488. return false;
  489. }
  490. std::array<std::uint32_t, kPerformanceStateCapacity> seen{};
  491. std::size_t distinct = 0;
  492. std::size_t declaringMembers = 0;
  493. for (const format::Squad& squad : sdk::scenario_squads(catalog, *sdk::bound_scenario(*view))) {
  494. if (squad.slotIndex != squadSlot) {
  495. continue;
  496. }
  497. for (const format::SquadMember& member : sdk::squad_members(catalog, squad)) {
  498. const auto names = sdk::actor_class_state_names(catalog, member.actorClassIndex);
  499. if (names.empty()) {
  500. continue;
  501. }
  502. ++declaringMembers;
  503. bool holds = false;
  504. for (const format::ActorStateName& name : names) {
  505. holds = holds || name.nameHash == nameHash;
  506. const auto end = seen.begin() + static_cast<std::ptrdiff_t>(distinct);
  507. if (std::find(seen.begin(), end, name.nameHash) != end) {
  508. continue;
  509. }
  510. if (distinct == seen.size()) {
  511. return false;
  512. }
  513. seen[distinct++] = name.nameHash;
  514. }
  515. if (nameHash != 0 && !holds) {
  516. return false;
  517. }
  518. }
  519. }
  520. if (declaringMembers == 0 || (nameHash == 0 && distinct != 1)) {
  521. return false;
  522. }
  523. output.slotRow = slotRow;
  524. output.nameHash = nameHash != 0 ? nameHash : seen[0];
  525. output.stateCount = static_cast<std::uint32_t>(distinct);
  526. return true;
  527. }
  528. /** Compares every value-owned field captured for one generation-bound Slot handle. */
  529. [[nodiscard]] bool same_slot_definition(const lua_vm::SlotDefinition& left,
  530. const lua_vm::SlotDefinition& right) noexcept {
  531. return left.id == right.id && left.name == right.name && left.objectId == right.objectId
  532. && left.senseSchemaId == right.senseSchemaId && left.authSchemaId == right.authSchemaId
  533. && left.nativeRow == right.nativeRow && left.localRow == right.localRow
  534. && left.objectTag == right.objectTag && left.registryKey == right.registryKey
  535. && left.slotIndex == right.slotIndex && left.slotType == right.slotType
  536. && left.componentClass == right.componentClass && left.senseSchema == right.senseSchema
  537. && left.authSchema == right.authSchema && left.flags == right.flags;
  538. }
  539. [[nodiscard]] bool scene_slot(const sdk::Catalog& catalog, const format::Slot& slot) noexcept {
  540. return slot.slotType == format::kAuthoredSceneSlotType
  541. && slot.componentClass == format::kAuthoredSceneComponentClass
  542. && slot.senseSchema == format::kAuthoredSceneSenseSchema
  543. && slot.authSchema == format::kAuthoredSceneAuthSchema
  544. && (slot.flags & format::kSlotSchemaJoinExact) != 0
  545. && sdk::slot_authored_scene_resources(catalog, slot).size() == 1;
  546. }
  547. /** Refuses scene identities that cannot fit the fixed Lua definition buffer. */
  548. [[nodiscard]] bool scene_id(const sdk::Catalog& catalog,
  549. const format::Occurrence& occurrence,
  550. const format::Slot& slot,
  551. lua_vm::SceneDefinition& output) noexcept {
  552. const std::string_view occurrenceId = catalog.string(occurrence.id);
  553. const std::string_view suffix = occurrenceId.starts_with(kOccurrencePrefix)
  554. ? occurrenceId.substr(kOccurrencePrefix.size())
  555. : occurrenceId;
  556. if (suffix.empty() || suffix.size() > 400
  557. || slot.slotIndex > (std::numeric_limits<std::uint16_t>::max)()
  558. || slot.slotType > (std::numeric_limits<std::uint16_t>::max)()) {
  559. return false;
  560. }
  561. const int written = std::snprintf(output.id.data(),
  562. output.id.size(),
  563. "symbol/%.*s/%04x/%04x",
  564. static_cast<int>(suffix.size()),
  565. suffix.data(),
  566. slot.slotIndex,
  567. slot.slotType);
  568. if (written <= 0 || static_cast<std::size_t>(written) >= output.id.size()) {
  569. output.id = {};
  570. return false;
  571. }
  572. output.idLength = static_cast<std::size_t>(written);
  573. return true;
  574. }
  575. /**
  576. * Resolves one authored scene of the bound activity into its Lua-facing definition.
  577. * Requires one matching exact scene slot across all scenario occurrences.
  578. */
  579. template <typename Select>
  580. [[nodiscard]] bool resolve_scene(const sdk::BoundView& view,
  581. Select&& select,
  582. lua_vm::SceneDefinition& output) noexcept {
  583. output = {};
  584. const sdk::Catalog& catalog = *view.catalog;
  585. const format::Scenario* const scenario = sdk::bound_scenario(view);
  586. const auto allOccurrences = catalog.occurrences();
  587. const auto allSlots = catalog.slots();
  588. const auto occurrences = sdk::scenario_occurrences(catalog, *scenario);
  589. std::uint32_t localRow = 0;
  590. std::size_t matches = 0;
  591. lua_vm::SceneDefinition selected{};
  592. for (const format::Occurrence& occurrence : occurrences) {
  593. if (occurrence.objectIndex >= catalog.objects().size()
  594. || &occurrence < allOccurrences.data()
  595. || &occurrence >= allOccurrences.data() + allOccurrences.size()) {
  596. return false;
  597. }
  598. const format::Object& object = catalog.objects()[occurrence.objectIndex];
  599. for (const format::Slot& slot : sdk::object_slots(catalog, object)) {
  600. if (!scene_slot(catalog, slot)) {
  601. continue;
  602. }
  603. ++localRow;
  604. lua_vm::SceneDefinition candidate{};
  605. candidate.localRow = localRow;
  606. candidate.occurrenceRow =
  607. static_cast<std::uint32_t>(&occurrence - allOccurrences.data());
  608. candidate.slotRow = static_cast<std::uint32_t>(&slot - allSlots.data());
  609. if (!scene_id(catalog, occurrence, slot, candidate)) {
  610. return false;
  611. }
  612. if (!select(candidate)) {
  613. continue;
  614. }
  615. selected = candidate;
  616. ++matches;
  617. }
  618. }
  619. if (matches != 1) {
  620. return false;
  621. }
  622. output = selected;
  623. return true;
  624. }
  625. /** Treats Lua scene rows as one-based positions inside the bound scenario. */
  626. [[nodiscard]] bool resolve_scene_row(const void* context,
  627. std::uint32_t localRow,
  628. lua_vm::SceneDefinition& output) noexcept {
  629. const sdk::BoundView* const view = context_view(context);
  630. return valid_view(view) && localRow != 0
  631. && resolve_scene(
  632. *view,
  633. [localRow](const lua_vm::SceneDefinition& value) noexcept {
  634. return value.localRow == localRow;
  635. },
  636. output);
  637. }
  638. /** Accepts the generated scene symbol or the generated ID/name of its owning slot. */
  639. [[nodiscard]] bool resolve_scene_id(const void* context,
  640. std::string_view id,
  641. lua_vm::SceneDefinition& output) noexcept {
  642. const sdk::BoundView* const view = context_view(context);
  643. return valid_view(view) && !id.empty()
  644. && resolve_scene(
  645. *view,
  646. [view, id](const lua_vm::SceneDefinition& value) noexcept {
  647. if (std::string_view(value.id.data(), value.idLength) == id
  648. || value.slotRow >= view->catalog->slots().size()) {
  649. return std::string_view(value.id.data(), value.idLength) == id;
  650. }
  651. const format::Slot& slot = view->catalog->slots()[value.slotRow];
  652. if (view->catalog->string(slot.id) == id
  653. || view->catalog->string(slot.name) == id) {
  654. return scenes::authored_scene_availability(
  655. *view, value.occurrenceRow, value.slotRow)
  656. == scenes::SceneStatus::ready;
  657. }
  658. for (const format::Text& alias : sdk::slot_aliases(*view->catalog, slot)) {
  659. if (view->catalog->string(alias.value) == id) {
  660. return scenes::authored_scene_availability(
  661. *view, value.occurrenceRow, value.slotRow)
  662. == scenes::SceneStatus::ready;
  663. }
  664. }
  665. return false;
  666. },
  667. output);
  668. }
  669. [[nodiscard]] std::size_t squad_count(const void* context) noexcept {
  670. const sdk::BoundView* const view = context_view(context);
  671. if (!valid_view(view)) {
  672. return 0;
  673. }
  674. return sdk::scenario_squads(*view->catalog, *sdk::bound_scenario(*view)).size();
  675. }
  676. /** Returns zero when any scenario occurrence cannot be resolved safely. */
  677. [[nodiscard]] std::size_t scene_count(const void* context) noexcept {
  678. const sdk::BoundView* const view = context_view(context);
  679. if (!valid_view(view)) {
  680. return 0;
  681. }
  682. std::size_t count = 0;
  683. const sdk::Catalog& catalog = *view->catalog;
  684. for (const format::Occurrence& occurrence :
  685. sdk::scenario_occurrences(catalog, *sdk::bound_scenario(*view))) {
  686. if (occurrence.objectIndex >= catalog.objects().size()) {
  687. return 0;
  688. }
  689. for (const format::Slot& slot :
  690. sdk::object_slots(catalog, catalog.objects()[occurrence.objectIndex])) {
  691. if (scene_slot(catalog, slot)) {
  692. ++count;
  693. }
  694. }
  695. }
  696. return count;
  697. }
  698. /** Counts repeated object definitions once and rejects an invalid occurrence set. */
  699. [[nodiscard]] std::size_t slot_count(const void* context) noexcept {
  700. const sdk::BoundView* const view = context_view(context);
  701. return valid_view(view) ? slot_index::count(*view) : 0;
  702. }
  703. /** @return Sensor count of the bound task, or 0 when nothing is bound. */
  704. [[nodiscard]] std::size_t task_sensor_count(const void* context) noexcept {
  705. const sdk::BoundView* const view = context_view(context);
  706. if (!valid_view(view)) {
  707. return 0;
  708. }
  709. return static_cast<std::size_t>(std::count_if(view->catalog->task_targets().begin(),
  710. view->catalog->task_targets().end(),
  711. [view](const format::TaskTarget& row) {
  712. return scenario_has_slot(*view,
  713. row.taskSlotIndex);
  714. }));
  715. }
  716. /** @return Element count of the bound directive, or 0 when nothing is bound. */
  717. [[nodiscard]] std::size_t directive_element_count(const void* context) noexcept {
  718. const sdk::BoundView* const view = context_view(context);
  719. if (!valid_view(view)) {
  720. return 0;
  721. }
  722. return static_cast<std::size_t>(std::count_if(view->catalog->directive_elements().begin(),
  723. view->catalog->directive_elements().end(),
  724. [view](const format::DirectiveElement& row) {
  725. return scenario_has_slot(*view,
  726. row.slotIndex);
  727. }));
  728. }
  729. [[nodiscard]] char hex_digit(std::uint8_t value) noexcept {
  730. // Lowercase hex digits; every digest and byte field is spelled this way.
  731. constexpr char digits[] = "0123456789abcdef";
  732. return digits[value & 0xFU];
  733. }
  734. } // namespace
  735. /** Publishes only bound activity identities with a complete 32-byte SDK digest. */
  736. bool program_identity(const sdk::BoundView& view,
  737. bool publicTarget,
  738. lua_vm::ProgramIdentity& output) noexcept {
  739. output = {};
  740. const format::Activity* const activity = sdk::bound_activity(view);
  741. if (activity == nullptr || view.catalog == nullptr) {
  742. return false;
  743. }
  744. const auto digest = view.catalog->sdk_build_sha256();
  745. const std::string_view activityId = view.catalog->string(activity->id);
  746. if (digest.size() != 32 || activityId.empty() || activityId.size() >= output.activityId.size()
  747. || view.activityRow == (std::numeric_limits<std::uint32_t>::max)()) {
  748. return false;
  749. }
  750. // The generated SDK build identity is spelled as this prefix and 64 hex digits.
  751. constexpr std::string_view prefix = "sha256:";
  752. std::copy(prefix.begin(), prefix.end(), output.sdkBuildId.begin());
  753. std::size_t cursor = prefix.size();
  754. for (const std::byte byte : digest) {
  755. const auto value = static_cast<std::uint8_t>(byte);
  756. output.sdkBuildId[cursor++] = hex_digit(static_cast<std::uint8_t>(value >> 4U));
  757. output.sdkBuildId[cursor++] = hex_digit(value);
  758. }
  759. std::copy(activityId.begin(), activityId.end(), output.activityId.begin());
  760. output.activityRow = view.activityRow + 1;
  761. output.definitionHash = activity->definitionHash;
  762. output.publicTarget = publicTarget;
  763. return true;
  764. }
  765. /** The caller must keep the immutable view alive while Lua uses the returned callbacks. */
  766. lua_vm::DefinitionApi definition_api(const sdk::BoundView& view) noexcept {
  767. lua_vm::DefinitionApi output{
  768. .context = &view,
  769. .resolveCombatObjectiveGroup = &combat_objective::resolve,
  770. .resolveActorAbility = &actor_ability::resolve,
  771. .resolveActorAbilityTarget = &actor_ability::target,
  772. .resolveActorProgramSource =
  773. [](const void* context, std::uint32_t slot, std::uint32_t& source) noexcept {
  774. const auto* view = context_view(context);
  775. return view != nullptr
  776. && activity_sdk_devices::actor_program_source_squad(*view, slot, source)
  777. == activity_sdk_devices::Status::ready;
  778. },
  779. .resolveSceneSpawnSources =
  780. [](const void* context,
  781. std::uint32_t occurrence,
  782. std::uint32_t slot,
  783. std::span<std::uint32_t> sources,
  784. std::size_t& count) noexcept {
  785. const auto* view = context_view(context);
  786. return view != nullptr
  787. && scenes::scene_spawn_sources(*view, occurrence, slot, sources, count)
  788. == scenes::SceneStatus::ready;
  789. },
  790. .resolveSquadRow = &resolve_squad_row,
  791. .resolveSquadId = &resolve_squad_id,
  792. .resolveSceneRow = &resolve_scene_row,
  793. .resolveSceneId = &resolve_scene_id,
  794. .resolveSlotRow = &resolve_slot_row,
  795. .resolveSlotId = &resolve_slot_id,
  796. .resolveSenseSlot = &resolve_sense_slot,
  797. .resolveTaskSensorRow = &resolve_task_sensor_row,
  798. .resolveTaskSensorId = &resolve_task_sensor_id,
  799. .resolveDirectiveElementRow = &resolve_directive_element_row,
  800. .resolveDirectiveElement = &resolve_directive_element,
  801. .resolvePerformanceState = &resolve_performance_state,
  802. .resolveActivityBinding = &resolve_activity_binding,
  803. .activityBindingTagCount = &activity_binding_tag_count,
  804. .resolveActivityBindingTag = &resolve_activity_binding_tag,
  805. .activityBindingLocatorCount = &activity_binding_locator_count,
  806. .resolveActivityBindingLocator = &resolve_activity_binding_locator,
  807. .squadCount = &squad_count,
  808. .sceneCount = &scene_count,
  809. .slotCount = &slot_count,
  810. .taskSensorCount = &task_sensor_count,
  811. .directiveElementCount = &directive_element_count,
  812. };
  813. message_catalog::attach(output);
  814. output.catalog = catalog_definition_api(view);
  815. return output;
  816. }
  817. } // namespace sunrise::server::activity::mission::sdk_bridge