actor_command_runtime_codec.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. #include "actor_command_runtime_codec.h"
  2. #include <array>
  3. #include <limits>
  4. namespace sunrise::middleware::gameplay::external {
  5. namespace {
  6. namespace format = state::activity_sdk::format;
  7. namespace wire = actor_wire;
  8. /** A command selector is seven bits. */
  9. constexpr std::uint64_t kMaximumCommandSelector = 0x7FU;
  10. /** The walker view stores the type code and codec parameter 2 as one byte each. */
  11. constexpr std::uint32_t kMaximumByteField = 0xFFU;
  12. /** Activity-family type codes run 0 through 46, which is all the fallback can check. */
  13. constexpr std::uint8_t kActivityTypeCodeCount = 47;
  14. /** Structural values every type-36 body carries before its payload. */
  15. constexpr std::size_t kCommandHeaderValueCount = 7;
  16. /** The command mode is three bits. */
  17. constexpr std::uint8_t kMaximumCommandMode = 7;
  18. /** The default and target-reference fields are one bit each. */
  19. constexpr std::uint8_t kMaximumCommandBit = 1;
  20. /** Activity types 29 and 43 share the primary-reference and tag/index wire body. */
  21. constexpr std::uint8_t kReferenceValueType = 29;
  22. constexpr std::uint8_t kReferenceValuePairType = 43;
  23. /** Activity aliases must not change the raw reference forms used by other families. */
  24. [[nodiscard]] std::uint8_t sdk_canonical_type(const void*, std::uint8_t type) noexcept {
  25. return type == kReferenceValueType ? kReferenceValuePairType : type;
  26. }
  27. /** Carries one selected message while delegating ordinary reflection. */
  28. struct ResolverContext final {
  29. const ActorCommandCatalog* catalog{};
  30. const format::ActorMessageSchema* message{};
  31. wire::RuntimeSchemaResolver reflection{};
  32. };
  33. /** Rejects catalog ranges that wrap or escape their owning span. */
  34. [[nodiscard]] bool valid_range(format::Range range, std::size_t size) noexcept {
  35. return range.first <= size && range.count <= size - range.first;
  36. }
  37. /** Requires every borrowed SDK span used by the runtime codec. */
  38. [[nodiscard]] bool valid_catalog(const ActorCommandCatalog& catalog) noexcept {
  39. return !catalog.messages.empty() && !catalog.commands.empty() && !catalog.schemas.empty()
  40. && !catalog.fields.empty();
  41. }
  42. /** Resolves a unique schema handle into the codec's compact view. */
  43. [[nodiscard]] bool
  44. sdk_find_schema(const void* raw, std::uint32_t handle, wire::runtime::SchemaView& output) noexcept {
  45. const auto* const catalog = static_cast<const ActorCommandCatalog*>(raw);
  46. if (catalog == nullptr) {
  47. return false;
  48. }
  49. for (std::size_t index = 0; index < catalog->schemas.size(); ++index) {
  50. const format::RuntimeSchema& row = catalog->schemas[index];
  51. if (row.handle != handle) {
  52. continue;
  53. }
  54. /** Only these two flags are understood; any other flag refuses the row. */
  55. constexpr std::uint32_t kAllowedFlags =
  56. format::kRuntimeSchemaExact | format::kRuntimeSchemaArrayRegion;
  57. const bool arrayRegion = (row.flags & format::kRuntimeSchemaArrayRegion) != 0;
  58. if ((row.flags & format::kRuntimeSchemaExact) == 0 || (row.flags & ~kAllowedFlags) != 0
  59. || arrayRegion != (row.arrayElementCount != 0)
  60. || row.fields.first > catalog->fields.size()
  61. || row.fields.count > catalog->fields.size() - row.fields.first) {
  62. return false;
  63. }
  64. output = {static_cast<std::uint32_t>(index),
  65. row.handle,
  66. row.arrayElementCount,
  67. row.fields.first,
  68. row.fields.count,
  69. row.decodedSize};
  70. return true;
  71. }
  72. return false;
  73. }
  74. /** Reads one schema row without exposing SDK storage to the walker. */
  75. [[nodiscard]] bool sdk_read_schema(const void* raw,
  76. std::uint32_t rowIndex,
  77. wire::runtime::SchemaView& output) noexcept {
  78. const auto* const catalog = static_cast<const ActorCommandCatalog*>(raw);
  79. return catalog != nullptr && rowIndex < catalog->schemas.size()
  80. && sdk_find_schema(raw, catalog->schemas[rowIndex].handle, output)
  81. && output.row == rowIndex;
  82. }
  83. /** Converts SDK field parameters into the generic walker contract. */
  84. [[nodiscard]] bool
  85. sdk_read_field(const void* raw, std::uint32_t rowIndex, wire::runtime::FieldView& output) noexcept {
  86. const auto* const catalog = static_cast<const ActorCommandCatalog*>(raw);
  87. if (catalog == nullptr || rowIndex >= catalog->fields.size()) {
  88. return false;
  89. }
  90. const format::RuntimeField& row = catalog->fields[rowIndex];
  91. if (row.schemaIndex >= catalog->schemas.size() || (row.flags & format::kRuntimeFieldExact) == 0
  92. || row.typeCode > kMaximumByteField) {
  93. return false;
  94. }
  95. std::uint32_t nestedRow = wire::runtime::kAbsentRuntimeRow;
  96. if (row.nestedHandle != format::kAbsentIndex) {
  97. wire::runtime::SchemaView nested{};
  98. if (!sdk_find_schema(raw, row.nestedHandle, nested)) {
  99. return false;
  100. }
  101. nestedRow = nested.row;
  102. }
  103. std::int32_t bias = 0;
  104. if (row.bias != format::kAbsentSignedValue) {
  105. if (row.bias < (std::numeric_limits<std::int32_t>::min)()
  106. || row.bias > (std::numeric_limits<std::int32_t>::max)()) {
  107. return false;
  108. }
  109. bias = static_cast<std::int32_t>(row.bias);
  110. }
  111. const bool dynamicArray = (row.flags & format::kRuntimeFieldDynamicArray) != 0;
  112. const bool quantizedFloat =
  113. row.typeCode == static_cast<std::uint32_t>(format::RuntimeFieldType::quantizedFloat);
  114. const std::int32_t bits = quantizedFloat ? static_cast<std::int32_t>(row.codecParameters[1])
  115. : dynamicArray ? static_cast<std::int32_t>(row.codecParameters[1])
  116. : row.bits == format::kAbsentIndex
  117. ? 0
  118. : static_cast<std::int32_t>(row.bits);
  119. output = {rowIndex,
  120. nestedRow,
  121. row.structOffset,
  122. quantizedFloat ? static_cast<std::int32_t>(row.codecParameters[0])
  123. : dynamicArray ? 1
  124. : bias,
  125. bits,
  126. sdk_canonical_type(raw, static_cast<std::uint8_t>(row.typeCode)),
  127. static_cast<std::uint8_t>((row.flags & format::kRuntimeFieldPresenceBit) != 0),
  128. static_cast<std::uint8_t>(
  129. row.codecParameters[2] <= kMaximumByteField ? row.codecParameters[2] : 0)};
  130. return true;
  131. }
  132. /** Resolves one unique command payload from the complete command catalog. */
  133. [[nodiscard]] bool
  134. sdk_resolve_payload(const void* raw, std::uint8_t selector, std::uint32_t& output) noexcept {
  135. const auto* const catalog = static_cast<const ActorCommandCatalog*>(raw);
  136. const format::ActorCommandDefinition* match = nullptr;
  137. if (catalog == nullptr) {
  138. return false;
  139. }
  140. for (const format::ActorCommandDefinition& command : catalog->commands) {
  141. if (command.selector != selector) {
  142. continue;
  143. }
  144. if (match != nullptr || command.payloadHandle == 0
  145. || command.flags != format::kActorCommandDefinitionExact) {
  146. return false;
  147. }
  148. match = &command;
  149. }
  150. if (match == nullptr) {
  151. return false;
  152. }
  153. output = match->payloadHandle;
  154. return true;
  155. }
  156. /** Variant selectors must exist in the activity reflection family. */
  157. [[nodiscard]] bool sdk_validate_type(const void* raw, std::uint8_t typeCode) noexcept {
  158. const auto* const catalog = static_cast<const ActorCommandCatalog*>(raw);
  159. if (catalog == nullptr || catalog->owner == nullptr) {
  160. return typeCode < kActivityTypeCodeCount;
  161. }
  162. return state::activity_sdk::runtime_type_by_code(
  163. *catalog->owner, format::RuntimeCodecFamily::activity, typeCode)
  164. != nullptr;
  165. }
  166. /** Accepts only exact published no-op types whose wire width is zero. */
  167. [[nodiscard]] bool sdk_zero_bit_type(const void* raw, std::uint8_t typeCode) noexcept {
  168. const auto* const catalog = static_cast<const ActorCommandCatalog*>(raw);
  169. if (catalog == nullptr || catalog->owner == nullptr) {
  170. return false;
  171. }
  172. const format::RuntimeTypeDefinition* const row = state::activity_sdk::runtime_type_by_code(
  173. *catalog->owner, format::RuntimeCodecFamily::activity, typeCode);
  174. return row != nullptr && (row->flags & format::kRuntimeTypeDefinitionExact) != 0
  175. && (row->flags & format::kRuntimeTypeUnsupported) != 0 && row->fixedBits == 0
  176. && row->minimumBits == 0 && row->maximumBits == 0 && row->reserved == 0;
  177. }
  178. /** Accepts only exact type-36 message rows with valid command ranges. */
  179. [[nodiscard]] bool valid_message(const ActorCommandCatalog& catalog,
  180. std::uint32_t index,
  181. const format::ActorMessageSchema*& output) noexcept {
  182. if (!valid_catalog(catalog) || index >= catalog.messages.size()) {
  183. return false;
  184. }
  185. const format::ActorMessageSchema& message = catalog.messages[index];
  186. if (message.definitionHandle == 0 || message.durableKey == 0 || message.ownerClass == 0
  187. || message.commands.count == 0 || !valid_range(message.commands, catalog.commands.size())
  188. || message.flags != format::kActorMessageSchemaExact || message.reserved != 0) {
  189. return false;
  190. }
  191. output = &message;
  192. return true;
  193. }
  194. /** Accepts only the command row owned by the selected message. */
  195. [[nodiscard]] bool valid_command(const ActorCommandCatalog& catalog,
  196. const format::ActorMessageSchema& message,
  197. std::uint32_t index,
  198. const format::ActorCommandDefinition*& output) noexcept {
  199. if (index < message.commands.first
  200. || index >= message.commands.first + message.commands.count) {
  201. return false;
  202. }
  203. const format::ActorCommandDefinition& command = catalog.commands[index];
  204. if (command.selector > kMaximumCommandSelector || command.payloadHandle == 0
  205. || command.flags != format::kActorCommandDefinitionExact) {
  206. return false;
  207. }
  208. output = &command;
  209. return true;
  210. }
  211. /** Delegates schema lookup through the selected-message resolver. */
  212. [[nodiscard]] bool
  213. find_schema(const void* raw, std::uint32_t handle, wire::runtime::SchemaView& output) noexcept {
  214. const auto* const context = static_cast<const ResolverContext*>(raw);
  215. return context != nullptr && context->reflection.findSchema != nullptr
  216. && context->reflection.findSchema(context->reflection.context, handle, output);
  217. }
  218. /** Delegates schema-row reads through the selected-message resolver. */
  219. [[nodiscard]] bool
  220. read_schema(const void* raw, std::uint32_t row, wire::runtime::SchemaView& output) noexcept {
  221. const auto* const context = static_cast<const ResolverContext*>(raw);
  222. return context != nullptr && context->reflection.readSchema != nullptr
  223. && context->reflection.readSchema(context->reflection.context, row, output);
  224. }
  225. /** Delegates field-row reads through the selected-message resolver. */
  226. [[nodiscard]] bool
  227. read_field(const void* raw, std::uint32_t row, wire::runtime::FieldView& output) noexcept {
  228. const auto* const context = static_cast<const ResolverContext*>(raw);
  229. return context != nullptr && context->reflection.readField != nullptr
  230. && context->reflection.readField(context->reflection.context, row, output);
  231. }
  232. /** Restricts command selection to the current message's command range. */
  233. [[nodiscard]] bool
  234. resolve_payload(const void* raw, std::uint8_t selector, std::uint32_t& output) noexcept {
  235. const auto* const context = static_cast<const ResolverContext*>(raw);
  236. if (context == nullptr || context->catalog == nullptr || context->message == nullptr) {
  237. return false;
  238. }
  239. const format::Range range = context->message->commands;
  240. const format::ActorCommandDefinition* match = nullptr;
  241. for (std::uint32_t offset = 0; offset < range.count; ++offset) {
  242. const format::ActorCommandDefinition& command =
  243. context->catalog->commands[range.first + offset];
  244. if (command.selector != selector) {
  245. continue;
  246. }
  247. if (match != nullptr || command.payloadHandle == 0
  248. || command.flags != format::kActorCommandDefinitionExact) {
  249. return false;
  250. }
  251. match = &command;
  252. }
  253. if (match == nullptr) {
  254. return false;
  255. }
  256. output = match->payloadHandle;
  257. return true;
  258. }
  259. /** Delegates union-selector validation through the borrowed reflection resolver. */
  260. [[nodiscard]] bool validate_type(const void* raw, std::uint8_t typeCode) noexcept {
  261. const auto* const context = static_cast<const ResolverContext*>(raw);
  262. return context != nullptr && context->reflection.validateType != nullptr
  263. && context->reflection.validateType(context->reflection.context, typeCode);
  264. }
  265. /** Delegates the exact zero-bit no-op classification through borrowed reflection. */
  266. [[nodiscard]] bool zero_bit_type(const void* raw, std::uint8_t typeCode) noexcept {
  267. const auto* const context = static_cast<const ResolverContext*>(raw);
  268. return context != nullptr && context->reflection.isZeroBitType != nullptr
  269. && context->reflection.isZeroBitType(context->reflection.context, typeCode);
  270. }
  271. /** Delegates family-specific aliases through the borrowed reflection context. */
  272. [[nodiscard]] std::uint8_t canonical_type(const void* raw, std::uint8_t typeCode) noexcept {
  273. const auto& context = *static_cast<const ResolverContext*>(raw);
  274. return context.reflection.canonicalType != nullptr
  275. ? context.reflection.canonicalType(context.reflection.context, typeCode)
  276. : typeCode;
  277. }
  278. /**
  279. * Overrides command selection while preserving ordinary reflection.
  280. * @param context Borrowed message and reflection state, which must outlive the adapter.
  281. * @return An adapter retaining the source codec's type aliases.
  282. */
  283. [[nodiscard]] wire::RuntimeSchemaResolver resolver(ResolverContext& context) noexcept {
  284. wire::RuntimeSchemaResolver output{&context,
  285. find_schema,
  286. read_schema,
  287. read_field,
  288. resolve_payload,
  289. validate_type,
  290. zero_bit_type};
  291. output.canonicalType = canonical_type;
  292. return output;
  293. }
  294. /** Finds the message schema's single dynamic actor-command field. */
  295. [[nodiscard]] bool root_field(const format::ActorMessageSchema& message,
  296. const wire::RuntimeSchemaResolver& reflection,
  297. wire::runtime::SchemaView& schema,
  298. wire::runtime::FieldView& field) noexcept {
  299. return reflection.findSchema != nullptr && reflection.readField != nullptr
  300. && reflection.findSchema(reflection.context, message.definitionHandle, schema)
  301. && schema.handle == message.definitionHandle && schema.arrayLength == 0
  302. && schema.fieldCount == 1
  303. && reflection.readField(reflection.context, schema.firstField, field)
  304. && field.row == schema.firstField && field.typeCode == message.bodyType;
  305. }
  306. /** Retains every published identity needed to replay the command. */
  307. [[nodiscard]] ActorCommandIdentity
  308. identity(std::uint32_t messageIndex,
  309. std::uint32_t commandIndex,
  310. const format::ActorMessageSchema& message,
  311. const format::ActorCommandDefinition& command) noexcept {
  312. return {messageIndex,
  313. commandIndex,
  314. message.definitionHandle,
  315. message.durableKey,
  316. message.ownerClass,
  317. message.handlerSlot,
  318. command.selector,
  319. command.payloadHandle};
  320. }
  321. } // namespace
  322. /**
  323. * Builds the generic activity-family reflection adapter.
  324. * @param catalog Borrowed SDK rows, which must outlive the adapter.
  325. * @return An adapter using the activity family's field and union aliases.
  326. */
  327. wire::RuntimeSchemaResolver
  328. actor_runtime_schema_resolver(const ActorCommandCatalog& catalog) noexcept {
  329. wire::RuntimeSchemaResolver output{&catalog,
  330. sdk_find_schema,
  331. sdk_read_schema,
  332. sdk_read_field,
  333. sdk_resolve_payload,
  334. sdk_validate_type,
  335. sdk_zero_bit_type};
  336. output.canonicalType = sdk_canonical_type;
  337. return output;
  338. }
  339. /** Encodes one type-36 command and its selector-owned payload. */
  340. bool encode_actor_command_body(const ActorCommandCatalog& catalog,
  341. std::uint32_t messageIndex,
  342. std::uint32_t commandIndex,
  343. const ActorCommandHeader& header,
  344. std::span<const wire::RuntimeDraftValue> payloadValues,
  345. const wire::RuntimeSchemaResolver& reflection,
  346. std::span<std::byte> output,
  347. std::size_t& written,
  348. std::size_t& writtenBits,
  349. wire::CodecStatus& status,
  350. ActorCommandIdentity& outputIdentity) noexcept {
  351. written = 0;
  352. writtenBits = 0;
  353. status = wire::CodecStatus::needsRuntimeSchema;
  354. outputIdentity = {};
  355. const format::ActorMessageSchema* message = nullptr;
  356. const format::ActorCommandDefinition* command = nullptr;
  357. wire::runtime::SchemaView schema{};
  358. wire::runtime::FieldView field{};
  359. if (!valid_message(catalog, messageIndex, message)
  360. || !valid_command(catalog, *message, commandIndex, command)
  361. || !root_field(*message, reflection, schema, field)
  362. || payloadValues.size() > wire::kRuntimeValueCapacity - kCommandHeaderValueCount
  363. || header.defaultValue > kMaximumCommandBit || header.mode > kMaximumCommandMode
  364. || header.targetReference > kMaximumCommandBit) {
  365. status = wire::CodecStatus::unsupportedField;
  366. return false;
  367. }
  368. std::array<wire::RuntimeDraftValue, wire::kRuntimeValueCapacity> values{};
  369. /** Header roles in wire order; the values below are staged in the same order. */
  370. constexpr std::array<wire::ValueRole, kCommandHeaderValueCount> roles{
  371. wire::ValueRole::commandDefault,
  372. wire::ValueRole::commandMode,
  373. wire::ValueRole::commandSelector,
  374. wire::ValueRole::commandTargetReference,
  375. wire::ValueRole::commandTarget,
  376. wire::ValueRole::commandAuxiliary16,
  377. wire::ValueRole::commandAuxiliary8,
  378. };
  379. const std::array<std::uint64_t, kCommandHeaderValueCount> headerValues{
  380. header.defaultValue,
  381. header.mode,
  382. command->selector,
  383. header.targetReference,
  384. header.target,
  385. header.auxiliary16,
  386. header.auxiliary8,
  387. };
  388. for (std::size_t index = 0; index < roles.size(); ++index) {
  389. wire::RuntimeDraftValue& value = values[index];
  390. value.schemaHandle = schema.handle;
  391. value.schemaRow = schema.row;
  392. value.fieldRow = field.row;
  393. value.role = roles[index];
  394. value.kind = wire::ValueKind::unsignedInteger;
  395. value.unsignedValue = headerValues[index];
  396. value.present = true;
  397. }
  398. for (std::size_t index = 0; index < payloadValues.size(); ++index) {
  399. const wire::RuntimeDraftValue& value = payloadValues[index];
  400. if (value.schemaHandle != command->payloadHandle) {
  401. status = wire::CodecStatus::unsupportedField;
  402. return false;
  403. }
  404. values[roles.size() + index] = value;
  405. }
  406. ResolverContext context{&catalog, message, reflection};
  407. const wire::RuntimeSchemaResolver selected = resolver(context);
  408. if (!wire::encode_full_schema(message->definitionHandle,
  409. std::span(values).first(roles.size() + payloadValues.size()),
  410. selected,
  411. output,
  412. written,
  413. writtenBits,
  414. status)) {
  415. return false;
  416. }
  417. outputIdentity = identity(messageIndex, commandIndex, *message, *command);
  418. return true;
  419. }
  420. /** Decodes one type-36 command into structural and payload values. */
  421. bool decode_actor_command_body(const ActorCommandCatalog& catalog,
  422. std::uint32_t messageIndex,
  423. std::span<const std::byte> payload,
  424. std::size_t bitCount,
  425. const wire::RuntimeSchemaResolver& reflection,
  426. std::span<wire::RuntimeDecodedValue> values,
  427. wire::RuntimeDecodeResult& result,
  428. ActorCommandIdentity& outputIdentity) noexcept {
  429. result = {};
  430. outputIdentity = {};
  431. const format::ActorMessageSchema* message = nullptr;
  432. wire::runtime::SchemaView schema{};
  433. wire::runtime::FieldView field{};
  434. if (!valid_message(catalog, messageIndex, message)
  435. || !root_field(*message, reflection, schema, field)) {
  436. result.status = wire::CodecStatus::unsupportedField;
  437. return false;
  438. }
  439. ResolverContext context{&catalog, message, reflection};
  440. const wire::RuntimeSchemaResolver selected = resolver(context);
  441. if (!wire::decode_full_schema(
  442. message->definitionHandle, payload, bitCount, selected, values, result)) {
  443. return false;
  444. }
  445. const wire::RuntimeDecodedValue* selector = nullptr;
  446. for (std::size_t index = 0; index < result.valueCount; ++index) {
  447. const wire::RuntimeDecodedValue& value = values[index];
  448. if (value.schemaHandle == schema.handle && value.schemaRow == schema.row
  449. && value.fieldRow == field.row && value.role == wire::ValueRole::commandSelector) {
  450. if (selector != nullptr || value.unsignedValue > kMaximumCommandSelector) {
  451. result.status = wire::CodecStatus::malformed;
  452. return false;
  453. }
  454. selector = &value;
  455. }
  456. }
  457. if (selector == nullptr) {
  458. result.status = wire::CodecStatus::malformed;
  459. return false;
  460. }
  461. const format::Range range = message->commands;
  462. const format::ActorCommandDefinition* command = nullptr;
  463. std::uint32_t commandIndex = format::kAbsentIndex;
  464. for (std::uint32_t offset = 0; offset < range.count; ++offset) {
  465. const std::uint32_t index = range.first + offset;
  466. const format::ActorCommandDefinition& candidate = catalog.commands[index];
  467. if (candidate.selector != selector->unsignedValue) {
  468. continue;
  469. }
  470. if (command != nullptr || !valid_command(catalog, *message, index, command)) {
  471. result.status = wire::CodecStatus::malformed;
  472. return false;
  473. }
  474. command = &candidate;
  475. commandIndex = index;
  476. }
  477. if (command == nullptr) {
  478. result.status = wire::CodecStatus::needsRuntimeSchema;
  479. return false;
  480. }
  481. outputIdentity = identity(messageIndex, commandIndex, *message, *command);
  482. return true;
  483. }
  484. } // namespace sunrise::middleware::gameplay::external