hash_name_matcher.cpp 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. #include "hash_name_matcher.h"
  2. #include <algorithm>
  3. #include "../../../state/build_data/runtime.h"
  4. namespace sunrise::client::content::hash_names {
  5. namespace {
  6. namespace layouts = state::build_data::scenarios;
  7. namespace spawn_state = state::build_data::spawn_sets;
  8. /** FNV-1 32-bit offset basis. The engine hashes names with FNV-1, not FNV-1a. */
  9. constexpr std::uint32_t kHashBasis = 0x811C9DC5U;
  10. /** FNV-1 32-bit prime. */
  11. constexpr std::uint32_t kHashPrime = 0x01000193U;
  12. /** @return The FNV-1 hash of text, which is what a bubble carries. */
  13. [[nodiscard]] std::uint32_t hash_of(std::string_view text) noexcept {
  14. std::uint32_t value = kHashBasis;
  15. for (const char character : text) {
  16. value = (value * kHashPrime) ^ static_cast<std::uint8_t>(character);
  17. }
  18. return value;
  19. }
  20. /**
  21. * Records one candidate against its target row.
  22. * @param state One reader's targets and rows.
  23. * @param text Candidate id that hashes to a target.
  24. * @param slot Target row index.
  25. */
  26. void keep(MatchState& state, std::string_view text, std::size_t slot) noexcept {
  27. names_state::Name& row = state.resolved[slot];
  28. // The shortest candidate is the bare name; a longer one is a suffix of some object's name.
  29. if (row.nameLength != 0 && row.nameLength <= text.size()) {
  30. return;
  31. }
  32. row = {};
  33. row.hash = state.targets[slot];
  34. std::copy_n(text.begin(), text.size(), row.name.begin());
  35. row.nameLength = static_cast<std::uint8_t>(text.size());
  36. }
  37. /**
  38. * Finds one hash in the ascending target set.
  39. * @param state One reader's targets and rows.
  40. * @param slot Receives the target row index.
  41. * @return True when the hash is a target.
  42. */
  43. [[nodiscard]] bool
  44. target_slot(const MatchState& state, std::uint32_t hash, std::size_t& slot) noexcept {
  45. const auto first = state.targets.begin();
  46. const auto last = state.targets.end();
  47. const auto found = std::lower_bound(first, last, hash);
  48. if (found == last || *found != hash) {
  49. return false;
  50. }
  51. slot = static_cast<std::size_t>(found - first);
  52. return true;
  53. }
  54. } // namespace
  55. /** Clears one extraction pass without freeing process-owned outer storage. */
  56. void reset(Storage& storage) noexcept {
  57. storage.targets.fill(0);
  58. storage.targetCount = 0;
  59. storage.resolved.fill(names_state::Name{});
  60. storage.resolvedCount = 0;
  61. storage.tagCount = 0;
  62. storage.cursor = 0;
  63. storage.collected = false;
  64. storage.invalid = false;
  65. }
  66. /** Records every hash the published domains declare: bubble names and spawn-set names. */
  67. bool collect_targets(Storage& storage) noexcept {
  68. storage.targets.fill(0);
  69. storage.targetCount = 0;
  70. // One whole domain of fixed rows, so the snapshot is static rather than a local.
  71. static std::array<layouts::Definition, layouts::kDefinitionCapacity> scratch{};
  72. std::size_t rows = 0;
  73. if (!state::build_data::snapshot_scenario_layouts(scratch, rows)) {
  74. return false;
  75. }
  76. for (std::size_t row = 0; row < rows; ++row) {
  77. const layouts::Definition& layout = scratch[row];
  78. const std::size_t declared =
  79. (std::min)(static_cast<std::size_t>(layout.bubbleCount), layout.bubbleHashes.size());
  80. for (std::size_t bubble = 0; bubble < declared; ++bubble) {
  81. const std::uint32_t hash = layout.bubbleHashes[bubble];
  82. if (hash == 0 || storage.targetCount >= storage.targets.size()) {
  83. continue;
  84. }
  85. storage.targets[storage.targetCount++] = hash;
  86. }
  87. }
  88. // The spawn sets name themselves the same way, and this pass already reads every blob that
  89. // could carry one, so their hashes ride along at no extra read.
  90. static std::array<spawn_state::NameHash, spawn_state::kNameHashCapacity> spawns{};
  91. std::size_t spawnCount = 0;
  92. if (state::build_data::snapshot_spawn_name_hashes(spawns, spawnCount)) {
  93. for (std::size_t row = 0; row < spawnCount; ++row) {
  94. if (spawns[row].value != 0 && storage.targetCount < storage.targets.size()) {
  95. storage.targets[storage.targetCount++] = spawns[row].value;
  96. }
  97. }
  98. }
  99. auto end = storage.targets.begin() + static_cast<std::ptrdiff_t>(storage.targetCount);
  100. std::sort(storage.targets.begin(), end);
  101. storage.targetCount = static_cast<std::size_t>(std::unique(storage.targets.begin(), end)
  102. - storage.targets.begin());
  103. return storage.targetCount != 0;
  104. }
  105. /** Offers one id to the pass, keeping the shortest name that gives a target hash. */
  106. void offer(MatchState& state, std::string_view text) noexcept {
  107. if (text.empty() || text.size() > kRunCapacity) {
  108. return;
  109. }
  110. // Token boundaries, so every underscore-bounded prefix and suffix can be tried.
  111. std::array<std::size_t, kTokenCapacity> starts{};
  112. std::size_t tokens = 0;
  113. starts[tokens++] = 0;
  114. for (std::size_t index = 0; index < text.size() && tokens < starts.size(); ++index) {
  115. if (text[index] == '_') {
  116. starts[tokens++] = index + 1;
  117. }
  118. }
  119. for (std::size_t first = 0; first < tokens; ++first) {
  120. for (std::size_t last = tokens; last > first; --last) {
  121. const std::size_t begin = starts[first];
  122. // A suffix run ends at the token separator before the next start, or at the end.
  123. const std::size_t end = last == tokens ? text.size() : starts[last] - 1;
  124. if (end <= begin) {
  125. continue;
  126. }
  127. const std::string_view candidate = text.substr(begin, end - begin);
  128. std::size_t slot = 0;
  129. if (target_slot(state, hash_of(candidate), slot)) {
  130. keep(state, candidate, slot);
  131. }
  132. }
  133. }
  134. }
  135. /** Offers every id inside one blob. */
  136. void offer_blob(MatchState& state, std::span<const std::byte> blob) noexcept {
  137. std::size_t start = 0;
  138. std::size_t length = 0;
  139. for (std::size_t index = 0; index <= blob.size(); ++index) {
  140. const char character = index < blob.size() ? static_cast<char>(blob[index]) : '\0';
  141. if (index < blob.size() && names_state::name_character(character)) {
  142. if (length == 0) {
  143. start = index;
  144. }
  145. ++length;
  146. continue;
  147. }
  148. if (length != 0 && length <= kRunCapacity) {
  149. offer(state,
  150. std::string_view(reinterpret_cast<const char*>(blob.data()) + start, length));
  151. }
  152. length = 0;
  153. }
  154. }
  155. /** @param storage Pass storage. @return Its targets and its own rows. */
  156. MatchState match_state(Storage& storage) noexcept {
  157. return MatchState{{storage.targets.data(), storage.targetCount},
  158. {storage.resolved.data(), storage.targetCount}};
  159. }
  160. /** Folds one reader's rows into the pass, keeping the shortest name for every target. */
  161. void merge(Storage& storage, std::span<const names_state::Name> rows) noexcept {
  162. const std::size_t count = (std::min)(rows.size(), storage.targetCount);
  163. for (std::size_t slot = 0; slot < count; ++slot) {
  164. const names_state::Name& candidate = rows[slot];
  165. names_state::Name& row = storage.resolved[slot];
  166. if (candidate.nameLength == 0
  167. || (row.nameLength != 0 && row.nameLength <= candidate.nameLength)) {
  168. continue;
  169. }
  170. row = candidate;
  171. }
  172. }
  173. /** Adds one swept tag to an unfinished pass. */
  174. bool add_tag(Storage& storage, std::uint32_t tag) noexcept {
  175. if (storage.collected || storage.invalid || tag == 0) {
  176. return false;
  177. }
  178. if (storage.tagCount >= storage.tags.size()) {
  179. storage.invalid = true;
  180. return false;
  181. }
  182. storage.tags[storage.tagCount++] = tag;
  183. return true;
  184. }
  185. /** Packs the resolved rows into ascending hash order for publishing. */
  186. void finish(Storage& storage) noexcept {
  187. std::size_t kept = 0;
  188. for (std::size_t row = 0; row < storage.targetCount; ++row) {
  189. if (storage.resolved[row].nameLength != 0) {
  190. storage.resolved[kept++] = storage.resolved[row];
  191. }
  192. }
  193. // The targets are already ascending and unique, so the kept rows keep that order.
  194. for (std::size_t row = kept; row < storage.resolved.size(); ++row) {
  195. storage.resolved[row] = {};
  196. }
  197. storage.resolvedCount = kept;
  198. }
  199. } // namespace sunrise::client::content::hash_names