hoi4-agent-tools 0.0.0-bootstrap.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (416) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/LICENSE +201 -0
  3. package/README.md +140 -2
  4. package/SECURITY.md +17 -0
  5. package/dist/bin/http.d.ts +3 -0
  6. package/dist/bin/http.d.ts.map +1 -0
  7. package/dist/bin/http.js +27 -0
  8. package/dist/bin/http.js.map +1 -0
  9. package/dist/bin/setup.d.ts +3 -0
  10. package/dist/bin/setup.d.ts.map +1 -0
  11. package/dist/bin/setup.js +188 -0
  12. package/dist/bin/setup.js.map +1 -0
  13. package/dist/bin/stdio.d.ts +3 -0
  14. package/dist/bin/stdio.d.ts.map +1 -0
  15. package/dist/bin/stdio.js +36 -0
  16. package/dist/bin/stdio.js.map +1 -0
  17. package/dist/hoi4_agent_tools/core/artifacts.d.ts +117 -0
  18. package/dist/hoi4_agent_tools/core/artifacts.d.ts.map +1 -0
  19. package/dist/hoi4_agent_tools/core/artifacts.js +1047 -0
  20. package/dist/hoi4_agent_tools/core/artifacts.js.map +1 -0
  21. package/dist/hoi4_agent_tools/core/canonical.d.ts +13 -0
  22. package/dist/hoi4_agent_tools/core/canonical.d.ts.map +1 -0
  23. package/dist/hoi4_agent_tools/core/canonical.js +41 -0
  24. package/dist/hoi4_agent_tools/core/canonical.js.map +1 -0
  25. package/dist/hoi4_agent_tools/core/configuration.d.ts +137 -0
  26. package/dist/hoi4_agent_tools/core/configuration.d.ts.map +1 -0
  27. package/dist/hoi4_agent_tools/core/configuration.js +383 -0
  28. package/dist/hoi4_agent_tools/core/configuration.js.map +1 -0
  29. package/dist/hoi4_agent_tools/core/diagnostic-collector.d.ts +18 -0
  30. package/dist/hoi4_agent_tools/core/diagnostic-collector.d.ts.map +1 -0
  31. package/dist/hoi4_agent_tools/core/diagnostic-collector.js +45 -0
  32. package/dist/hoi4_agent_tools/core/diagnostic-collector.js.map +1 -0
  33. package/dist/hoi4_agent_tools/core/diagnostics.d.ts +26 -0
  34. package/dist/hoi4_agent_tools/core/diagnostics.d.ts.map +1 -0
  35. package/dist/hoi4_agent_tools/core/diagnostics.js +25 -0
  36. package/dist/hoi4_agent_tools/core/diagnostics.js.map +1 -0
  37. package/dist/hoi4_agent_tools/core/diff.d.ts +22 -0
  38. package/dist/hoi4_agent_tools/core/diff.d.ts.map +1 -0
  39. package/dist/hoi4_agent_tools/core/diff.js +93 -0
  40. package/dist/hoi4_agent_tools/core/diff.js.map +1 -0
  41. package/dist/hoi4_agent_tools/core/engine.d.ts +56 -0
  42. package/dist/hoi4_agent_tools/core/engine.d.ts.map +1 -0
  43. package/dist/hoi4_agent_tools/core/engine.js +319 -0
  44. package/dist/hoi4_agent_tools/core/engine.js.map +1 -0
  45. package/dist/hoi4_agent_tools/core/image-diff.d.ts +14 -0
  46. package/dist/hoi4_agent_tools/core/image-diff.d.ts.map +1 -0
  47. package/dist/hoi4_agent_tools/core/image-diff.js +121 -0
  48. package/dist/hoi4_agent_tools/core/image-diff.js.map +1 -0
  49. package/dist/hoi4_agent_tools/core/index.d.ts +64 -0
  50. package/dist/hoi4_agent_tools/core/index.d.ts.map +1 -0
  51. package/dist/hoi4_agent_tools/core/index.js +963 -0
  52. package/dist/hoi4_agent_tools/core/index.js.map +1 -0
  53. package/dist/hoi4_agent_tools/core/index.public.d.ts +16 -0
  54. package/dist/hoi4_agent_tools/core/index.public.d.ts.map +1 -0
  55. package/dist/hoi4_agent_tools/core/index.public.js +16 -0
  56. package/dist/hoi4_agent_tools/core/index.public.js.map +1 -0
  57. package/dist/hoi4_agent_tools/core/render-budget.d.ts +51 -0
  58. package/dist/hoi4_agent_tools/core/render-budget.d.ts.map +1 -0
  59. package/dist/hoi4_agent_tools/core/render-budget.js +109 -0
  60. package/dist/hoi4_agent_tools/core/render-budget.js.map +1 -0
  61. package/dist/hoi4_agent_tools/core/result.d.ts +45 -0
  62. package/dist/hoi4_agent_tools/core/result.d.ts.map +1 -0
  63. package/dist/hoi4_agent_tools/core/result.js +26 -0
  64. package/dist/hoi4_agent_tools/core/result.js.map +1 -0
  65. package/dist/hoi4_agent_tools/core/scanner.d.ts +28 -0
  66. package/dist/hoi4_agent_tools/core/scanner.d.ts.map +1 -0
  67. package/dist/hoi4_agent_tools/core/scanner.js +136 -0
  68. package/dist/hoi4_agent_tools/core/scanner.js.map +1 -0
  69. package/dist/hoi4_agent_tools/core/server-state.d.ts +31 -0
  70. package/dist/hoi4_agent_tools/core/server-state.d.ts.map +1 -0
  71. package/dist/hoi4_agent_tools/core/server-state.js +413 -0
  72. package/dist/hoi4_agent_tools/core/server-state.js.map +1 -0
  73. package/dist/hoi4_agent_tools/core/source/encoding.d.ts +10 -0
  74. package/dist/hoi4_agent_tools/core/source/encoding.d.ts.map +1 -0
  75. package/dist/hoi4_agent_tools/core/source/encoding.js +54 -0
  76. package/dist/hoi4_agent_tools/core/source/encoding.js.map +1 -0
  77. package/dist/hoi4_agent_tools/core/source/index.d.ts +7 -0
  78. package/dist/hoi4_agent_tools/core/source/index.d.ts.map +1 -0
  79. package/dist/hoi4_agent_tools/core/source/index.js +7 -0
  80. package/dist/hoi4_agent_tools/core/source/index.js.map +1 -0
  81. package/dist/hoi4_agent_tools/core/source/lexer.d.ts +30 -0
  82. package/dist/hoi4_agent_tools/core/source/lexer.d.ts.map +1 -0
  83. package/dist/hoi4_agent_tools/core/source/lexer.js +266 -0
  84. package/dist/hoi4_agent_tools/core/source/lexer.js.map +1 -0
  85. package/dist/hoi4_agent_tools/core/source/limits.d.ts +21 -0
  86. package/dist/hoi4_agent_tools/core/source/limits.d.ts.map +1 -0
  87. package/dist/hoi4_agent_tools/core/source/limits.js +49 -0
  88. package/dist/hoi4_agent_tools/core/source/limits.js.map +1 -0
  89. package/dist/hoi4_agent_tools/core/source/localisation.d.ts +20 -0
  90. package/dist/hoi4_agent_tools/core/source/localisation.d.ts.map +1 -0
  91. package/dist/hoi4_agent_tools/core/source/localisation.js +163 -0
  92. package/dist/hoi4_agent_tools/core/source/localisation.js.map +1 -0
  93. package/dist/hoi4_agent_tools/core/source/parser.d.ts +44 -0
  94. package/dist/hoi4_agent_tools/core/source/parser.d.ts.map +1 -0
  95. package/dist/hoi4_agent_tools/core/source/parser.js +334 -0
  96. package/dist/hoi4_agent_tools/core/source/parser.js.map +1 -0
  97. package/dist/hoi4_agent_tools/core/source/rewrite.d.ts +10 -0
  98. package/dist/hoi4_agent_tools/core/source/rewrite.d.ts.map +1 -0
  99. package/dist/hoi4_agent_tools/core/source/rewrite.js +28 -0
  100. package/dist/hoi4_agent_tools/core/source/rewrite.js.map +1 -0
  101. package/dist/hoi4_agent_tools/core/svg-text.d.ts +34 -0
  102. package/dist/hoi4_agent_tools/core/svg-text.d.ts.map +1 -0
  103. package/dist/hoi4_agent_tools/core/svg-text.js +127 -0
  104. package/dist/hoi4_agent_tools/core/svg-text.js.map +1 -0
  105. package/dist/hoi4_agent_tools/core/transaction-limits.d.ts +9 -0
  106. package/dist/hoi4_agent_tools/core/transaction-limits.d.ts.map +1 -0
  107. package/dist/hoi4_agent_tools/core/transaction-limits.js +9 -0
  108. package/dist/hoi4_agent_tools/core/transaction-limits.js.map +1 -0
  109. package/dist/hoi4_agent_tools/core/transactions.d.ts +162 -0
  110. package/dist/hoi4_agent_tools/core/transactions.d.ts.map +1 -0
  111. package/dist/hoi4_agent_tools/core/transactions.js +1181 -0
  112. package/dist/hoi4_agent_tools/core/transactions.js.map +1 -0
  113. package/dist/hoi4_agent_tools/core/workspace.d.ts +68 -0
  114. package/dist/hoi4_agent_tools/core/workspace.d.ts.map +1 -0
  115. package/dist/hoi4_agent_tools/core/workspace.js +794 -0
  116. package/dist/hoi4_agent_tools/core/workspace.js.map +1 -0
  117. package/dist/hoi4_agent_tools/focus/compiler.d.ts +9 -0
  118. package/dist/hoi4_agent_tools/focus/compiler.d.ts.map +1 -0
  119. package/dist/hoi4_agent_tools/focus/compiler.js +273 -0
  120. package/dist/hoi4_agent_tools/focus/compiler.js.map +1 -0
  121. package/dist/hoi4_agent_tools/focus/continuous-render.d.ts +32 -0
  122. package/dist/hoi4_agent_tools/focus/continuous-render.d.ts.map +1 -0
  123. package/dist/hoi4_agent_tools/focus/continuous-render.js +179 -0
  124. package/dist/hoi4_agent_tools/focus/continuous-render.js.map +1 -0
  125. package/dist/hoi4_agent_tools/focus/drift.d.ts +22 -0
  126. package/dist/hoi4_agent_tools/focus/drift.d.ts.map +1 -0
  127. package/dist/hoi4_agent_tools/focus/drift.js +124 -0
  128. package/dist/hoi4_agent_tools/focus/drift.js.map +1 -0
  129. package/dist/hoi4_agent_tools/focus/geometry.d.ts +17 -0
  130. package/dist/hoi4_agent_tools/focus/geometry.d.ts.map +1 -0
  131. package/dist/hoi4_agent_tools/focus/geometry.js +35 -0
  132. package/dist/hoi4_agent_tools/focus/geometry.js.map +1 -0
  133. package/dist/hoi4_agent_tools/focus/importer.d.ts +9 -0
  134. package/dist/hoi4_agent_tools/focus/importer.d.ts.map +1 -0
  135. package/dist/hoi4_agent_tools/focus/importer.js +732 -0
  136. package/dist/hoi4_agent_tools/focus/importer.js.map +1 -0
  137. package/dist/hoi4_agent_tools/focus/index.d.ts +14 -0
  138. package/dist/hoi4_agent_tools/focus/index.d.ts.map +1 -0
  139. package/dist/hoi4_agent_tools/focus/index.js +14 -0
  140. package/dist/hoi4_agent_tools/focus/index.js.map +1 -0
  141. package/dist/hoi4_agent_tools/focus/layout.d.ts +5 -0
  142. package/dist/hoi4_agent_tools/focus/layout.d.ts.map +1 -0
  143. package/dist/hoi4_agent_tools/focus/layout.js +653 -0
  144. package/dist/hoi4_agent_tools/focus/layout.js.map +1 -0
  145. package/dist/hoi4_agent_tools/focus/limits.d.ts +9 -0
  146. package/dist/hoi4_agent_tools/focus/limits.d.ts.map +1 -0
  147. package/dist/hoi4_agent_tools/focus/limits.js +9 -0
  148. package/dist/hoi4_agent_tools/focus/limits.js.map +1 -0
  149. package/dist/hoi4_agent_tools/focus/lint.d.ts +14 -0
  150. package/dist/hoi4_agent_tools/focus/lint.d.ts.map +1 -0
  151. package/dist/hoi4_agent_tools/focus/lint.js +839 -0
  152. package/dist/hoi4_agent_tools/focus/lint.js.map +1 -0
  153. package/dist/hoi4_agent_tools/focus/model.d.ts +303 -0
  154. package/dist/hoi4_agent_tools/focus/model.d.ts.map +1 -0
  155. package/dist/hoi4_agent_tools/focus/model.js +20 -0
  156. package/dist/hoi4_agent_tools/focus/model.js.map +1 -0
  157. package/dist/hoi4_agent_tools/focus/native-effects.d.ts +9 -0
  158. package/dist/hoi4_agent_tools/focus/native-effects.d.ts.map +1 -0
  159. package/dist/hoi4_agent_tools/focus/native-effects.js +563 -0
  160. package/dist/hoi4_agent_tools/focus/native-effects.js.map +1 -0
  161. package/dist/hoi4_agent_tools/focus/planning.d.ts +15 -0
  162. package/dist/hoi4_agent_tools/focus/planning.d.ts.map +1 -0
  163. package/dist/hoi4_agent_tools/focus/planning.js +241 -0
  164. package/dist/hoi4_agent_tools/focus/planning.js.map +1 -0
  165. package/dist/hoi4_agent_tools/focus/presentation.d.ts +24 -0
  166. package/dist/hoi4_agent_tools/focus/presentation.d.ts.map +1 -0
  167. package/dist/hoi4_agent_tools/focus/presentation.js +204 -0
  168. package/dist/hoi4_agent_tools/focus/presentation.js.map +1 -0
  169. package/dist/hoi4_agent_tools/focus/render.d.ts +35 -0
  170. package/dist/hoi4_agent_tools/focus/render.d.ts.map +1 -0
  171. package/dist/hoi4_agent_tools/focus/render.js +491 -0
  172. package/dist/hoi4_agent_tools/focus/render.js.map +1 -0
  173. package/dist/hoi4_agent_tools/focus/service.d.ts +82 -0
  174. package/dist/hoi4_agent_tools/focus/service.d.ts.map +1 -0
  175. package/dist/hoi4_agent_tools/focus/service.js +492 -0
  176. package/dist/hoi4_agent_tools/focus/service.js.map +1 -0
  177. package/dist/hoi4_agent_tools/focus/source-update.d.ts +6 -0
  178. package/dist/hoi4_agent_tools/focus/source-update.d.ts.map +1 -0
  179. package/dist/hoi4_agent_tools/focus/source-update.js +328 -0
  180. package/dist/hoi4_agent_tools/focus/source-update.js.map +1 -0
  181. package/dist/hoi4_agent_tools/gui/animation-manifest.d.ts +37 -0
  182. package/dist/hoi4_agent_tools/gui/animation-manifest.d.ts.map +1 -0
  183. package/dist/hoi4_agent_tools/gui/animation-manifest.js +66 -0
  184. package/dist/hoi4_agent_tools/gui/animation-manifest.js.map +1 -0
  185. package/dist/hoi4_agent_tools/gui/assets.d.ts +71 -0
  186. package/dist/hoi4_agent_tools/gui/assets.d.ts.map +1 -0
  187. package/dist/hoi4_agent_tools/gui/assets.js +629 -0
  188. package/dist/hoi4_agent_tools/gui/assets.js.map +1 -0
  189. package/dist/hoi4_agent_tools/gui/dds.d.ts +17 -0
  190. package/dist/hoi4_agent_tools/gui/dds.d.ts.map +1 -0
  191. package/dist/hoi4_agent_tools/gui/dds.js +245 -0
  192. package/dist/hoi4_agent_tools/gui/dds.js.map +1 -0
  193. package/dist/hoi4_agent_tools/gui/helpers.d.ts +101 -0
  194. package/dist/hoi4_agent_tools/gui/helpers.d.ts.map +1 -0
  195. package/dist/hoi4_agent_tools/gui/helpers.js +1072 -0
  196. package/dist/hoi4_agent_tools/gui/helpers.js.map +1 -0
  197. package/dist/hoi4_agent_tools/gui/index.d.ts +15 -0
  198. package/dist/hoi4_agent_tools/gui/index.d.ts.map +1 -0
  199. package/dist/hoi4_agent_tools/gui/index.js +15 -0
  200. package/dist/hoi4_agent_tools/gui/index.js.map +1 -0
  201. package/dist/hoi4_agent_tools/gui/layout.d.ts +6 -0
  202. package/dist/hoi4_agent_tools/gui/layout.d.ts.map +1 -0
  203. package/dist/hoi4_agent_tools/gui/layout.js +753 -0
  204. package/dist/hoi4_agent_tools/gui/layout.js.map +1 -0
  205. package/dist/hoi4_agent_tools/gui/limits.d.ts +25 -0
  206. package/dist/hoi4_agent_tools/gui/limits.d.ts.map +1 -0
  207. package/dist/hoi4_agent_tools/gui/limits.js +27 -0
  208. package/dist/hoi4_agent_tools/gui/limits.js.map +1 -0
  209. package/dist/hoi4_agent_tools/gui/renderer.d.ts +18 -0
  210. package/dist/hoi4_agent_tools/gui/renderer.d.ts.map +1 -0
  211. package/dist/hoi4_agent_tools/gui/renderer.js +388 -0
  212. package/dist/hoi4_agent_tools/gui/renderer.js.map +1 -0
  213. package/dist/hoi4_agent_tools/gui/scenario.d.ts +66 -0
  214. package/dist/hoi4_agent_tools/gui/scenario.d.ts.map +1 -0
  215. package/dist/hoi4_agent_tools/gui/scenario.js +191 -0
  216. package/dist/hoi4_agent_tools/gui/scenario.js.map +1 -0
  217. package/dist/hoi4_agent_tools/gui/source-graph.d.ts +7 -0
  218. package/dist/hoi4_agent_tools/gui/source-graph.d.ts.map +1 -0
  219. package/dist/hoi4_agent_tools/gui/source-graph.js +1074 -0
  220. package/dist/hoi4_agent_tools/gui/source-graph.js.map +1 -0
  221. package/dist/hoi4_agent_tools/gui/source-patch.d.ts +13 -0
  222. package/dist/hoi4_agent_tools/gui/source-patch.d.ts.map +1 -0
  223. package/dist/hoi4_agent_tools/gui/source-patch.js +90 -0
  224. package/dist/hoi4_agent_tools/gui/source-patch.js.map +1 -0
  225. package/dist/hoi4_agent_tools/gui/studio.d.ts +86 -0
  226. package/dist/hoi4_agent_tools/gui/studio.d.ts.map +1 -0
  227. package/dist/hoi4_agent_tools/gui/studio.js +830 -0
  228. package/dist/hoi4_agent_tools/gui/studio.js.map +1 -0
  229. package/dist/hoi4_agent_tools/gui/tga.d.ts +16 -0
  230. package/dist/hoi4_agent_tools/gui/tga.d.ts.map +1 -0
  231. package/dist/hoi4_agent_tools/gui/tga.js +157 -0
  232. package/dist/hoi4_agent_tools/gui/tga.js.map +1 -0
  233. package/dist/hoi4_agent_tools/gui/types.d.ts +322 -0
  234. package/dist/hoi4_agent_tools/gui/types.d.ts.map +1 -0
  235. package/dist/hoi4_agent_tools/gui/types.js +19 -0
  236. package/dist/hoi4_agent_tools/gui/types.js.map +1 -0
  237. package/dist/hoi4_agent_tools/gui/validators.d.ts +8 -0
  238. package/dist/hoi4_agent_tools/gui/validators.d.ts.map +1 -0
  239. package/dist/hoi4_agent_tools/gui/validators.js +951 -0
  240. package/dist/hoi4_agent_tools/gui/validators.js.map +1 -0
  241. package/dist/hoi4_agent_tools/map/bmp.d.ts +62 -0
  242. package/dist/hoi4_agent_tools/map/bmp.d.ts.map +1 -0
  243. package/dist/hoi4_agent_tools/map/bmp.js +363 -0
  244. package/dist/hoi4_agent_tools/map/bmp.js.map +1 -0
  245. package/dist/hoi4_agent_tools/map/diagnostic-limit.d.ts +5 -0
  246. package/dist/hoi4_agent_tools/map/diagnostic-limit.d.ts.map +1 -0
  247. package/dist/hoi4_agent_tools/map/diagnostic-limit.js +21 -0
  248. package/dist/hoi4_agent_tools/map/diagnostic-limit.js.map +1 -0
  249. package/dist/hoi4_agent_tools/map/index.d.ts +8 -0
  250. package/dist/hoi4_agent_tools/map/index.d.ts.map +1 -0
  251. package/dist/hoi4_agent_tools/map/index.js +8 -0
  252. package/dist/hoi4_agent_tools/map/index.js.map +1 -0
  253. package/dist/hoi4_agent_tools/map/limits.d.ts +13 -0
  254. package/dist/hoi4_agent_tools/map/limits.d.ts.map +1 -0
  255. package/dist/hoi4_agent_tools/map/limits.js +13 -0
  256. package/dist/hoi4_agent_tools/map/limits.js.map +1 -0
  257. package/dist/hoi4_agent_tools/map/model.d.ts +269 -0
  258. package/dist/hoi4_agent_tools/map/model.d.ts.map +1 -0
  259. package/dist/hoi4_agent_tools/map/model.js +1114 -0
  260. package/dist/hoi4_agent_tools/map/model.js.map +1 -0
  261. package/dist/hoi4_agent_tools/map/operations.d.ts +401 -0
  262. package/dist/hoi4_agent_tools/map/operations.d.ts.map +1 -0
  263. package/dist/hoi4_agent_tools/map/operations.js +2717 -0
  264. package/dist/hoi4_agent_tools/map/operations.js.map +1 -0
  265. package/dist/hoi4_agent_tools/map/render.d.ts +90 -0
  266. package/dist/hoi4_agent_tools/map/render.d.ts.map +1 -0
  267. package/dist/hoi4_agent_tools/map/render.js +759 -0
  268. package/dist/hoi4_agent_tools/map/render.js.map +1 -0
  269. package/dist/hoi4_agent_tools/map/service.d.ts +81 -0
  270. package/dist/hoi4_agent_tools/map/service.d.ts.map +1 -0
  271. package/dist/hoi4_agent_tools/map/service.js +695 -0
  272. package/dist/hoi4_agent_tools/map/service.js.map +1 -0
  273. package/dist/hoi4_agent_tools/map/validation.d.ts +23 -0
  274. package/dist/hoi4_agent_tools/map/validation.d.ts.map +1 -0
  275. package/dist/hoi4_agent_tools/map/validation.js +1177 -0
  276. package/dist/hoi4_agent_tools/map/validation.js.map +1 -0
  277. package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts +3 -0
  278. package/dist/hoi4_agent_tools/mcp/prompts/register.d.ts.map +1 -0
  279. package/dist/hoi4_agent_tools/mcp/prompts/register.js +50 -0
  280. package/dist/hoi4_agent_tools/mcp/prompts/register.js.map +1 -0
  281. package/dist/hoi4_agent_tools/mcp/registry/metadata.d.ts +9 -0
  282. package/dist/hoi4_agent_tools/mcp/registry/metadata.d.ts.map +1 -0
  283. package/dist/hoi4_agent_tools/mcp/registry/metadata.js +10 -0
  284. package/dist/hoi4_agent_tools/mcp/registry/metadata.js.map +1 -0
  285. package/dist/hoi4_agent_tools/mcp/resources/register.d.ts +5 -0
  286. package/dist/hoi4_agent_tools/mcp/resources/register.d.ts.map +1 -0
  287. package/dist/hoi4_agent_tools/mcp/resources/register.js +278 -0
  288. package/dist/hoi4_agent_tools/mcp/resources/register.js.map +1 -0
  289. package/dist/hoi4_agent_tools/mcp/security/auth.d.ts +24 -0
  290. package/dist/hoi4_agent_tools/mcp/security/auth.d.ts.map +1 -0
  291. package/dist/hoi4_agent_tools/mcp/security/auth.js +234 -0
  292. package/dist/hoi4_agent_tools/mcp/security/auth.js.map +1 -0
  293. package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts +35 -0
  294. package/dist/hoi4_agent_tools/mcp/server/base-tools.d.ts.map +1 -0
  295. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +835 -0
  296. package/dist/hoi4_agent_tools/mcp/server/base-tools.js.map +1 -0
  297. package/dist/hoi4_agent_tools/mcp/server/create.d.ts +5 -0
  298. package/dist/hoi4_agent_tools/mcp/server/create.d.ts.map +1 -0
  299. package/dist/hoi4_agent_tools/mcp/server/create.js +26 -0
  300. package/dist/hoi4_agent_tools/mcp/server/create.js.map +1 -0
  301. package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts +133 -0
  302. package/dist/hoi4_agent_tools/mcp/server/output-schemas.d.ts.map +1 -0
  303. package/dist/hoi4_agent_tools/mcp/server/output-schemas.js +127 -0
  304. package/dist/hoi4_agent_tools/mcp/server/output-schemas.js.map +1 -0
  305. package/dist/hoi4_agent_tools/mcp/server/progress.d.ts +8 -0
  306. package/dist/hoi4_agent_tools/mcp/server/progress.d.ts.map +1 -0
  307. package/dist/hoi4_agent_tools/mcp/server/progress.js +21 -0
  308. package/dist/hoi4_agent_tools/mcp/server/progress.js.map +1 -0
  309. package/dist/hoi4_agent_tools/mcp/server/result.d.ts +232 -0
  310. package/dist/hoi4_agent_tools/mcp/server/result.d.ts.map +1 -0
  311. package/dist/hoi4_agent_tools/mcp/server/result.js +353 -0
  312. package/dist/hoi4_agent_tools/mcp/server/result.js.map +1 -0
  313. package/dist/hoi4_agent_tools/mcp/tools/focus.d.ts +5 -0
  314. package/dist/hoi4_agent_tools/mcp/tools/focus.d.ts.map +1 -0
  315. package/dist/hoi4_agent_tools/mcp/tools/focus.js +1059 -0
  316. package/dist/hoi4_agent_tools/mcp/tools/focus.js.map +1 -0
  317. package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts +111 -0
  318. package/dist/hoi4_agent_tools/mcp/tools/gui.d.ts.map +1 -0
  319. package/dist/hoi4_agent_tools/mcp/tools/gui.js +598 -0
  320. package/dist/hoi4_agent_tools/mcp/tools/gui.js.map +1 -0
  321. package/dist/hoi4_agent_tools/mcp/tools/map.d.ts +5 -0
  322. package/dist/hoi4_agent_tools/mcp/tools/map.d.ts.map +1 -0
  323. package/dist/hoi4_agent_tools/mcp/tools/map.js +685 -0
  324. package/dist/hoi4_agent_tools/mcp/tools/map.js.map +1 -0
  325. package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.d.ts +58 -0
  326. package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.d.ts.map +1 -0
  327. package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.js +181 -0
  328. package/dist/hoi4_agent_tools/mcp/transports/bounded-stdio.js.map +1 -0
  329. package/dist/hoi4_agent_tools/mcp/transports/event-store.d.ts +26 -0
  330. package/dist/hoi4_agent_tools/mcp/transports/event-store.d.ts.map +1 -0
  331. package/dist/hoi4_agent_tools/mcp/transports/event-store.js +86 -0
  332. package/dist/hoi4_agent_tools/mcp/transports/event-store.js.map +1 -0
  333. package/dist/hoi4_agent_tools/mcp/transports/http.d.ts +28 -0
  334. package/dist/hoi4_agent_tools/mcp/transports/http.d.ts.map +1 -0
  335. package/dist/hoi4_agent_tools/mcp/transports/http.js +744 -0
  336. package/dist/hoi4_agent_tools/mcp/transports/http.js.map +1 -0
  337. package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.d.ts +15 -0
  338. package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.d.ts.map +1 -0
  339. package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js +51 -0
  340. package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js.map +1 -0
  341. package/dist/hoi4_agent_tools/runtime.d.ts +4 -0
  342. package/dist/hoi4_agent_tools/runtime.d.ts.map +1 -0
  343. package/dist/hoi4_agent_tools/runtime.js +27 -0
  344. package/dist/hoi4_agent_tools/runtime.js.map +1 -0
  345. package/dist/hoi4_agent_tools/schemas/common.d.ts +74 -0
  346. package/dist/hoi4_agent_tools/schemas/common.d.ts.map +1 -0
  347. package/dist/hoi4_agent_tools/schemas/common.js +51 -0
  348. package/dist/hoi4_agent_tools/schemas/common.js.map +1 -0
  349. package/dist/hoi4_agent_tools/schemas/focus.d.ts +784 -0
  350. package/dist/hoi4_agent_tools/schemas/focus.d.ts.map +1 -0
  351. package/dist/hoi4_agent_tools/schemas/focus.js +330 -0
  352. package/dist/hoi4_agent_tools/schemas/focus.js.map +1 -0
  353. package/dist/hoi4_agent_tools/schemas/index.d.ts +5 -0
  354. package/dist/hoi4_agent_tools/schemas/index.d.ts.map +1 -0
  355. package/dist/hoi4_agent_tools/schemas/index.js +5 -0
  356. package/dist/hoi4_agent_tools/schemas/index.js.map +1 -0
  357. package/dist/hoi4_agent_tools/schemas/map.d.ts +654 -0
  358. package/dist/hoi4_agent_tools/schemas/map.d.ts.map +1 -0
  359. package/dist/hoi4_agent_tools/schemas/map.js +570 -0
  360. package/dist/hoi4_agent_tools/schemas/map.js.map +1 -0
  361. package/dist/hoi4_agent_tools/schemas/transaction.d.ts +259 -0
  362. package/dist/hoi4_agent_tools/schemas/transaction.d.ts.map +1 -0
  363. package/dist/hoi4_agent_tools/schemas/transaction.js +104 -0
  364. package/dist/hoi4_agent_tools/schemas/transaction.js.map +1 -0
  365. package/dist/hoi4_agent_tools/version.d.ts +6 -0
  366. package/dist/hoi4_agent_tools/version.d.ts.map +1 -0
  367. package/dist/hoi4_agent_tools/version.js +7 -0
  368. package/dist/hoi4_agent_tools/version.js.map +1 -0
  369. package/dist/index.d.ts +3 -0
  370. package/dist/index.d.ts.map +1 -0
  371. package/dist/index.js +3 -0
  372. package/dist/index.js.map +1 -0
  373. package/docs/README.md +22 -0
  374. package/docs/adr/0001-runtime-and-language.md +16 -0
  375. package/docs/adr/0002-lossless-source-model.md +18 -0
  376. package/docs/adr/0003-image-and-font-codecs.md +18 -0
  377. package/docs/adr/0004-focus-layout.md +16 -0
  378. package/docs/adr/0005-offline-rendering.md +18 -0
  379. package/docs/adr/0006-mcp-sdk-and-protocol.md +16 -0
  380. package/docs/adr/0007-storage-and-transactions.md +24 -0
  381. package/docs/adr/0008-test-strategy.md +18 -0
  382. package/docs/adr/0009-packaging-and-registry.md +16 -0
  383. package/docs/adr/0010-unsupported-and-refusal-policy.md +18 -0
  384. package/docs/adr/0011-http-security.md +16 -0
  385. package/docs/adr/README.md +13 -0
  386. package/docs/architecture.md +60 -0
  387. package/docs/artifacts.md +133 -0
  388. package/docs/compatibility.md +33 -0
  389. package/docs/completion-report.md +69 -0
  390. package/docs/configuration.md +285 -0
  391. package/docs/focus-workflow.md +68 -0
  392. package/docs/gui-workflow.md +121 -0
  393. package/docs/limitations.md +30 -0
  394. package/docs/map-workflow.md +89 -0
  395. package/docs/performance.md +117 -0
  396. package/docs/release.md +200 -0
  397. package/docs/research/mcp-2026-07-10.md +46 -0
  398. package/docs/research/source-ledger.md +68 -0
  399. package/docs/security.md +139 -0
  400. package/docs/self-hosting.md +168 -0
  401. package/docs/testing.md +43 -0
  402. package/docs/third-party-notices.md +11 -0
  403. package/docs/transactions.md +162 -0
  404. package/docs/troubleshooting.md +46 -0
  405. package/package.json +98 -5
  406. package/schemas/configuration.schema.json +794 -0
  407. package/schemas/continuous-focus-palette.schema.json +325 -0
  408. package/schemas/focus-plan.schema.json +902 -0
  409. package/schemas/focus-planning-sidecar.schema.json +364 -0
  410. package/schemas/gui-animation-source.schema.json +176 -0
  411. package/schemas/gui-helper.schema.json +301 -0
  412. package/schemas/gui-scenario.schema.json +298 -0
  413. package/schemas/map-operation.schema.json +2284 -0
  414. package/schemas/operation-result.schema.json +344 -0
  415. package/schemas/transaction-manifest.schema.json +685 -0
  416. package/server.json +32 -0
@@ -0,0 +1,2717 @@
1
+ import path from 'node:path';
2
+ import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
3
+ import { compareCodeUnits, deterministicId, sha256Bytes } from '../core/canonical.js';
4
+ import { ServiceError } from '../core/result.js';
5
+ import { applyReplacements, assignments, encodeSource, parseLocalisation, } from '../core/source/index.js';
6
+ import { rgbKey } from './bmp.js';
7
+ import { MAP_MASK_CELL_LIMIT, MAP_POLYGON_WORK_LIMIT, MAP_SELECTED_OFFSET_BYTES, MAP_SELECTED_PIXEL_LIMIT, } from './limits.js';
8
+ import { derivedStateCapital, encodeTextDocument, MapWorkspaceIndex, parseTextDocument, } from './model.js';
9
+ function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
10
+ if (iteration % stride !== 0)
11
+ return;
12
+ signal?.throwIfAborted();
13
+ yield;
14
+ }
15
+ function completeSynchronously(steps) {
16
+ for (;;) {
17
+ const step = steps.next();
18
+ if (step.done)
19
+ return step.value;
20
+ }
21
+ }
22
+ async function completeCooperatively(steps, signal) {
23
+ let yieldDeadline = performance.now() + 8;
24
+ for (;;) {
25
+ const step = steps.next();
26
+ if (step.done)
27
+ return step.value;
28
+ signal?.throwIfAborted();
29
+ if (performance.now() < yieldDeadline)
30
+ continue;
31
+ await yieldToEventLoop();
32
+ signal?.throwIfAborted();
33
+ yieldDeadline = performance.now() + 8;
34
+ }
35
+ }
36
+ function normalizeRelative(value) {
37
+ return value.replaceAll('\\', '/').replace(/^\.\//u, '').toLowerCase();
38
+ }
39
+ function numberText(value) {
40
+ if (!Number.isFinite(value))
41
+ throw new ServiceError('MAP_NUMBER_INVALID', 'Map values must be finite', { value });
42
+ if (Number.isInteger(value))
43
+ return String(value);
44
+ return value.toFixed(6).replace(/0+$/u, '').replace(/\.$/u, '');
45
+ }
46
+ function required(value, code, message) {
47
+ if (value === undefined)
48
+ throw new ServiceError(code, message);
49
+ return value;
50
+ }
51
+ function quote(value) {
52
+ return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
53
+ }
54
+ function uniqueSorted(values) {
55
+ return [...new Set(values)].sort((left, right) => left - right);
56
+ }
57
+ function uniqueStrings(values) {
58
+ return [...new Set(values)].sort((left, right) => compareCodeUnits(left, right));
59
+ }
60
+ function lineIndent(text, offset) {
61
+ const lineStart = Math.max(text.lastIndexOf('\n', Math.max(0, offset - 1)), text.lastIndexOf('\r', Math.max(0, offset - 1))) + 1;
62
+ return /^[\t ]*/u.exec(text.slice(lineStart, offset))?.[0] ?? '';
63
+ }
64
+ function assignmentFor(block, key) {
65
+ return assignments(block, key)[0];
66
+ }
67
+ function renderScalarMap(values, indent, newline) {
68
+ const rows = [...values]
69
+ .sort(([left], [right]) => compareCodeUnits(left, right))
70
+ .map(([key, value]) => `${indent}\t${key} = ${numberText(value)}`);
71
+ return rows.length === 0 ? `{ }` : `{${newline}${rows.join(newline)}${newline}${indent}}`;
72
+ }
73
+ function renderNumberList(values, indent, newline) {
74
+ const sorted = uniqueSorted(values);
75
+ return sorted.length === 0
76
+ ? `{ }`
77
+ : `{${newline}${indent}\t${sorted.join(' ')}${newline}${indent}}`;
78
+ }
79
+ function renderBuildings(data, indent, newline) {
80
+ const rows = [...data.stateBuildings]
81
+ .sort(([left], [right]) => compareCodeUnits(left, right))
82
+ .map(([key, value]) => `${indent}\t${key} = ${numberText(value)}`);
83
+ for (const [provinceId, buildings] of [...data.provinceBuildings].sort(([left], [right]) => left - right)) {
84
+ const inner = [...buildings]
85
+ .sort(([left], [right]) => compareCodeUnits(left, right))
86
+ .map(([key, value]) => `${indent}\t\t${key} = ${numberText(value)}`)
87
+ .join(newline);
88
+ rows.push(`${indent}\t${provinceId} = {${newline}${inner}${newline}${indent}\t}`);
89
+ }
90
+ return rows.length === 0 ? `{ }` : `{${newline}${rows.join(newline)}${newline}${indent}}`;
91
+ }
92
+ function blockInsertion(block, documentText, content, newline) {
93
+ if (block.close === undefined)
94
+ throw new ServiceError('MAP_SOURCE_BLOCK_UNCLOSED', 'Cannot insert into an unclosed source block');
95
+ const indent = lineIndent(documentText, block.close.start);
96
+ return {
97
+ start: block.close.start,
98
+ end: block.close.start,
99
+ text: `\t${content}${newline}${indent}`,
100
+ description: 'Insert map source assignment',
101
+ };
102
+ }
103
+ function cloneState(state) {
104
+ return {
105
+ id: state.id,
106
+ name: state.name,
107
+ manpower: state.manpower,
108
+ category: state.category,
109
+ provinces: [...state.provinces],
110
+ resources: new Map(state.resources),
111
+ owner: state.owner,
112
+ controller: state.controller,
113
+ cores: [...state.cores],
114
+ claims: [...state.claims],
115
+ victoryPoints: state.victoryPoints.map(({ provinceId, value }) => ({ provinceId, value })),
116
+ provinceBuildings: new Map([...state.provinceBuildings].map(([provinceId, values]) => [provinceId, new Map(values)])),
117
+ stateBuildings: new Map(state.stateBuildings),
118
+ };
119
+ }
120
+ function compileHistory(data, indent, newline) {
121
+ const rows = [];
122
+ if (data.owner !== undefined)
123
+ rows.push(`${indent}\towner = ${data.owner}`);
124
+ if (data.controller !== undefined)
125
+ rows.push(`${indent}\tcontroller = ${data.controller}`);
126
+ rows.push(...uniqueStrings(data.cores).map((tag) => `${indent}\tadd_core_of = ${tag}`));
127
+ rows.push(...uniqueStrings(data.claims).map((tag) => `${indent}\tadd_claim_by = ${tag}`));
128
+ rows.push(...data.victoryPoints
129
+ .slice()
130
+ .sort((left, right) => left.provinceId - right.provinceId || left.value - right.value)
131
+ .map(({ provinceId, value }) => `${indent}\tvictory_points = { ${provinceId} ${numberText(value)} }`));
132
+ if (data.stateBuildings.size > 0 || data.provinceBuildings.size > 0) {
133
+ rows.push(`${indent}\tbuildings = ${renderBuildings(data, `${indent}\t`, newline)}`);
134
+ }
135
+ return rows.length === 0 ? `{ }` : `{${newline}${rows.join(newline)}${newline}${indent}}`;
136
+ }
137
+ function compileState(data, newline = '\n') {
138
+ const resources = data.resources.size === 0
139
+ ? ''
140
+ : `\tresources = ${renderScalarMap(data.resources, '\t', newline)}${newline}`;
141
+ const history = compileHistory(data, '\t', newline);
142
+ const text = [
143
+ 'state = {',
144
+ `\tid = ${data.id}`,
145
+ `\tname = ${quote(data.name)}`,
146
+ `\tmanpower = ${numberText(data.manpower)}`,
147
+ `\tstate_category = ${data.category}`,
148
+ resources.trimEnd(),
149
+ `\tprovinces = ${renderNumberList(data.provinces, '\t', newline)}`,
150
+ `\thistory = ${history}`,
151
+ '}',
152
+ '',
153
+ ]
154
+ .filter((line) => line !== '')
155
+ .join(newline);
156
+ return Buffer.from(`${text}${newline}`, 'utf8');
157
+ }
158
+ function removeAssignments(block, key, replacements) {
159
+ for (const assignment of assignments(block, key)) {
160
+ replacements.push({
161
+ start: assignment.start,
162
+ end: assignment.end,
163
+ text: '',
164
+ description: `Remove ${key}`,
165
+ });
166
+ }
167
+ }
168
+ function patchState(state, data, fields) {
169
+ const replacements = [];
170
+ const stateInsertions = [];
171
+ const newline = state.document.newline;
172
+ const stateIndent = lineIndent(state.document.text, state.assignment.start);
173
+ if (fields.manpower === true) {
174
+ const assignment = assignmentFor(state.block, 'manpower');
175
+ if (assignment?.value.type === 'scalar') {
176
+ replacements.push({
177
+ start: assignment.value.start,
178
+ end: assignment.value.end,
179
+ text: numberText(data.manpower),
180
+ description: 'Update state manpower',
181
+ });
182
+ }
183
+ else {
184
+ stateInsertions.push(`manpower = ${numberText(data.manpower)}`);
185
+ }
186
+ }
187
+ if (fields.category === true) {
188
+ const assignment = assignmentFor(state.block, 'state_category');
189
+ if (assignment?.value.type === 'scalar') {
190
+ replacements.push({
191
+ start: assignment.value.start,
192
+ end: assignment.value.end,
193
+ text: data.category,
194
+ description: 'Update state category',
195
+ });
196
+ }
197
+ else {
198
+ stateInsertions.push(`state_category = ${data.category}`);
199
+ }
200
+ }
201
+ if (fields.resources === true) {
202
+ const assignment = assignmentFor(state.block, 'resources');
203
+ const rendered = `resources = ${renderScalarMap(data.resources, `${stateIndent}\t`, newline)}`;
204
+ if (assignment !== undefined) {
205
+ replacements.push({
206
+ start: assignment.start,
207
+ end: assignment.end,
208
+ text: rendered,
209
+ description: 'Update state resources',
210
+ });
211
+ }
212
+ else if (data.resources.size > 0) {
213
+ stateInsertions.push(rendered);
214
+ }
215
+ }
216
+ if (fields.provinces === true) {
217
+ const assignment = assignmentFor(state.block, 'provinces');
218
+ const rendered = `provinces = ${renderNumberList(data.provinces, `${stateIndent}\t`, newline)}`;
219
+ if (assignment !== undefined) {
220
+ replacements.push({
221
+ start: assignment.start,
222
+ end: assignment.end,
223
+ text: rendered,
224
+ description: 'Update state provinces',
225
+ });
226
+ }
227
+ else {
228
+ stateInsertions.push(rendered);
229
+ }
230
+ }
231
+ const historyChanged = fields.owner === true ||
232
+ fields.controller === true ||
233
+ fields.cores === true ||
234
+ fields.claims === true ||
235
+ fields.victoryPoints === true ||
236
+ fields.buildings === true;
237
+ if (historyChanged) {
238
+ const history = state.historyBlock;
239
+ if (history === undefined) {
240
+ stateInsertions.push(`history = ${compileHistory(data, `${stateIndent}\t`, newline)}`);
241
+ }
242
+ else {
243
+ const historyIndent = lineIndent(state.document.text, history.open?.start ?? history.start);
244
+ const historyInsertions = [];
245
+ const patchOptionalScalar = (key, value) => {
246
+ const matching = assignments(history, key);
247
+ if (value === undefined)
248
+ removeAssignments(history, key, replacements);
249
+ else if (matching[0]?.value.type === 'scalar') {
250
+ replacements.push({
251
+ start: matching[0].value.start,
252
+ end: matching[0].value.end,
253
+ text: value,
254
+ description: `Update state ${key}`,
255
+ });
256
+ for (const duplicate of matching.slice(1)) {
257
+ replacements.push({
258
+ start: duplicate.start,
259
+ end: duplicate.end,
260
+ text: '',
261
+ description: `Remove duplicate ${key}`,
262
+ });
263
+ }
264
+ }
265
+ else {
266
+ removeAssignments(history, key, replacements);
267
+ historyInsertions.push(`${key} = ${value}`);
268
+ }
269
+ };
270
+ if (fields.owner === true)
271
+ patchOptionalScalar('owner', data.owner);
272
+ if (fields.controller === true)
273
+ patchOptionalScalar('controller', data.controller);
274
+ if (fields.cores === true) {
275
+ removeAssignments(history, 'add_core_of', replacements);
276
+ historyInsertions.push(...uniqueStrings(data.cores).map((tag) => `add_core_of = ${tag}`));
277
+ }
278
+ if (fields.claims === true) {
279
+ removeAssignments(history, 'add_claim_by', replacements);
280
+ historyInsertions.push(...uniqueStrings(data.claims).map((tag) => `add_claim_by = ${tag}`));
281
+ }
282
+ if (fields.victoryPoints === true) {
283
+ removeAssignments(history, 'victory_points', replacements);
284
+ historyInsertions.push(...data.victoryPoints
285
+ .slice()
286
+ .sort((left, right) => left.provinceId - right.provinceId || left.value - right.value)
287
+ .map(({ provinceId, value }) => `victory_points = { ${provinceId} ${numberText(value)} }`));
288
+ }
289
+ if (fields.buildings === true) {
290
+ const assignment = assignmentFor(history, 'buildings');
291
+ const rendered = `buildings = ${renderBuildings(data, `${historyIndent}\t`, newline)}`;
292
+ if (assignment !== undefined) {
293
+ replacements.push({
294
+ start: assignment.start,
295
+ end: assignment.end,
296
+ text: rendered,
297
+ description: 'Update state buildings',
298
+ });
299
+ }
300
+ else if (data.stateBuildings.size > 0 || data.provinceBuildings.size > 0) {
301
+ historyInsertions.push(rendered);
302
+ }
303
+ }
304
+ if (historyInsertions.length > 0) {
305
+ replacements.push(blockInsertion(history, state.document.text, historyInsertions.join(`${newline}${historyIndent}\t`), newline));
306
+ }
307
+ }
308
+ }
309
+ if (stateInsertions.length > 0) {
310
+ replacements.push(blockInsertion(state.block, state.document.text, stateInsertions.join(`${newline}${stateIndent}\t`), newline));
311
+ }
312
+ return applyReplacements(state.document, replacements);
313
+ }
314
+ function patchRegion(region, provinces) {
315
+ const assignment = assignmentFor(region.block, 'provinces');
316
+ const indent = lineIndent(region.document.text, region.assignment.start);
317
+ const rendered = `provinces = ${renderNumberList(provinces, `${indent}\t`, region.document.newline)}`;
318
+ const replacement = assignment === undefined
319
+ ? blockInsertion(region.block, region.document.text, rendered, region.document.newline)
320
+ : {
321
+ start: assignment.start,
322
+ end: assignment.end,
323
+ text: rendered,
324
+ description: 'Update strategic region provinces',
325
+ };
326
+ return applyReplacements(region.document, [replacement]);
327
+ }
328
+ function definitionLine(definition) {
329
+ return [
330
+ definition.id,
331
+ definition.color.r,
332
+ definition.color.g,
333
+ definition.color.b,
334
+ definition.type,
335
+ definition.coastal ? 'true' : 'false',
336
+ definition.terrain,
337
+ definition.continent,
338
+ ].join(';');
339
+ }
340
+ function replaceDelimitedFields(line, replacements) {
341
+ if (replacements.size === 0)
342
+ return line;
343
+ const maximumIndex = [...replacements.keys()].reduce((maximum, index) => Math.max(maximum, index), -1);
344
+ const parts = [];
345
+ let start = 0;
346
+ for (let index = 0; index <= maximumIndex; index += 1) {
347
+ const delimiter = line.indexOf(';', start);
348
+ const end = delimiter < 0 ? line.length : delimiter;
349
+ if (start > line.length || (delimiter < 0 && index < maximumIndex))
350
+ throw new ServiceError('MAP_DELIMITED_FIELD_MISSING', `Delimited source row has no field ${index}`);
351
+ const field = line.slice(start, end);
352
+ const value = replacements.get(index);
353
+ const leading = /^\s*/u.exec(field)?.[0] ?? '';
354
+ const trailing = /\s*$/u.exec(field)?.[0] ?? '';
355
+ parts.push(value === undefined ? field : `${leading}${value}${trailing}`);
356
+ if (delimiter < 0) {
357
+ start = line.length;
358
+ break;
359
+ }
360
+ parts.push(';');
361
+ start = delimiter + 1;
362
+ }
363
+ parts.push(line.slice(start));
364
+ return parts.join('');
365
+ }
366
+ function patchDefinitionLine(source, updated) {
367
+ const line = source.document.lines[source.line - 1];
368
+ if (line === undefined)
369
+ throw new ServiceError('MAP_DEFINITION_LINE_MISSING', `Definition line ${source.line} is missing`);
370
+ const replacements = new Map();
371
+ if (updated.id !== source.id)
372
+ replacements.set(0, String(updated.id));
373
+ if (updated.color.r !== source.color.r)
374
+ replacements.set(1, String(updated.color.r));
375
+ if (updated.color.g !== source.color.g)
376
+ replacements.set(2, String(updated.color.g));
377
+ if (updated.color.b !== source.color.b)
378
+ replacements.set(3, String(updated.color.b));
379
+ if (updated.type !== source.type)
380
+ replacements.set(4, updated.type);
381
+ if (updated.coastal !== source.coastal)
382
+ replacements.set(5, updated.coastal ? 'true' : 'false');
383
+ if (updated.terrain !== source.terrain)
384
+ replacements.set(6, updated.terrain);
385
+ if (updated.continent !== source.continent)
386
+ replacements.set(7, String(updated.continent));
387
+ return replaceDelimitedFields(line.text, replacements);
388
+ }
389
+ function applyTextRanges(document, replacements) {
390
+ const ordered = [...replacements].sort((left, right) => left.start - right.start || left.end - right.end);
391
+ let cursor = 0;
392
+ const pieces = [];
393
+ for (const replacement of ordered) {
394
+ if (replacement.start < cursor ||
395
+ replacement.end < replacement.start ||
396
+ replacement.end > document.text.length) {
397
+ throw new ServiceError('MAP_TEXT_REPLACEMENT_OVERLAP', 'Map text replacements overlap or leave the file bounds');
398
+ }
399
+ pieces.push(document.text.slice(cursor, replacement.start), replacement.text);
400
+ cursor = replacement.end;
401
+ }
402
+ pieces.push(document.text.slice(cursor));
403
+ return encodeTextDocument(document, pieces.join(''));
404
+ }
405
+ function patchDefinitions(index, updates, removals, additions) {
406
+ const file = index.definitionFile;
407
+ if (file === undefined)
408
+ throw new ServiceError('MAP_DEFINITION_FILE_MISSING', 'Cannot edit provinces without definition.csv');
409
+ const document = parseTextDocument(file);
410
+ const replacements = [];
411
+ for (const definition of index.definitions) {
412
+ const line = document.lines[definition.line - 1];
413
+ if (line === undefined)
414
+ throw new ServiceError('MAP_DEFINITION_LINE_MISSING', `Definition line ${definition.line} is missing`);
415
+ if (removals.has(definition.id)) {
416
+ replacements.push({
417
+ start: line.start,
418
+ end: line.fullEnd,
419
+ text: '',
420
+ description: `Remove province ${definition.id}`,
421
+ });
422
+ }
423
+ else {
424
+ const update = updates.get(definition.id);
425
+ if (update !== undefined) {
426
+ replacements.push({
427
+ start: line.start,
428
+ end: line.end,
429
+ text: patchDefinitionLine(definition, update),
430
+ description: `Update province ${definition.id}`,
431
+ });
432
+ }
433
+ }
434
+ }
435
+ if (additions.length > 0) {
436
+ const separator = document.text.length === 0 || /(?:\r\n|\n|\r)$/u.test(document.text) ? '' : document.newline;
437
+ replacements.push({
438
+ start: document.text.length,
439
+ end: document.text.length,
440
+ text: `${separator}${additions.map(definitionLine).join(document.newline)}${document.newline}`,
441
+ description: 'Append province definitions',
442
+ });
443
+ }
444
+ return applyTextRanges(document, replacements);
445
+ }
446
+ function rootEvidence(records) {
447
+ const groups = new Map();
448
+ for (const record of records) {
449
+ const key = `${record.file.rootKind}:${record.file.loadOrder}`;
450
+ const existing = groups.get(key);
451
+ if (existing === undefined) {
452
+ groups.set(key, {
453
+ rootKind: record.file.rootKind,
454
+ loadOrder: record.file.loadOrder,
455
+ sourcePaths: new Set([record.file.displayPath]),
456
+ samplePath: record.file.displayPath,
457
+ maximumId: record.id,
458
+ });
459
+ continue;
460
+ }
461
+ existing.sourcePaths.add(record.file.displayPath);
462
+ if (compareCodeUnits(record.file.displayPath, existing.samplePath) < 0) {
463
+ existing.samplePath = record.file.displayPath;
464
+ }
465
+ existing.maximumId = Math.max(existing.maximumId, record.id);
466
+ }
467
+ return [...groups.values()]
468
+ .map(({ sourcePaths, ...group }) => ({ ...group, sourceCount: sourcePaths.size }))
469
+ .sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.rootKind, right.rootKind));
470
+ }
471
+ function contiguous(ids, start) {
472
+ if (ids.length === 0)
473
+ return true;
474
+ const sorted = [...new Set(ids)].sort((left, right) => left - right);
475
+ let expected = start;
476
+ for (const id of sorted) {
477
+ if (!Number.isSafeInteger(id) || id !== expected)
478
+ return false;
479
+ expected += 1;
480
+ }
481
+ return true;
482
+ }
483
+ function maximumObservedId(values, fallback) {
484
+ let maximum = fallback;
485
+ for (const value of values) {
486
+ if (!Number.isSafeInteger(value)) {
487
+ throw new ServiceError('MAP_ID_INVALID', 'Scanned map ID is not a safe integer');
488
+ }
489
+ maximum = Math.max(maximum, value);
490
+ }
491
+ return maximum;
492
+ }
493
+ function* allocateStateId(index, requested, signal) {
494
+ const highest = maximumObservedId(index.statesAcrossRoots.map(({ id }) => id), 0);
495
+ const used = new Set(index.statesAcrossRoots.map(({ id }) => id));
496
+ if (requested !== undefined) {
497
+ if (!Number.isSafeInteger(requested) || requested <= 0 || requested > 2_147_483_647)
498
+ throw new ServiceError('MAP_STATE_ID_INVALID', 'Explicit state ID must be a positive integer');
499
+ if (used.has(requested))
500
+ throw new ServiceError(index.statesById.has(requested)
501
+ ? 'MAP_STATE_ID_COLLISION'
502
+ : 'MAP_DEPENDENCY_STATE_ID_CONFLICT', `Explicit state ID ${requested} is already present in a scanned root`, {
503
+ stateId: requested,
504
+ sources: index.statesAcrossRoots
505
+ .filter(({ id }) => id === requested)
506
+ .map(({ file }) => file.displayPath)
507
+ .sort(),
508
+ });
509
+ return {
510
+ id: requested,
511
+ evidence: {
512
+ kind: 'state-id',
513
+ allocated: requested,
514
+ strategy: 'explicit-request-after-full-scan',
515
+ highestObserved: highest,
516
+ probes: used.size,
517
+ roots: rootEvidence(index.statesAcrossRoots.map(({ id: value, file }) => ({ id: value, file }))),
518
+ },
519
+ };
520
+ }
521
+ if (highest >= 2_147_483_647) {
522
+ throw new ServiceError('MAP_STATE_ID_EXHAUSTED', 'No safe state ID remains available');
523
+ }
524
+ let id = 1;
525
+ let probes = 1;
526
+ while (used.has(id)) {
527
+ yield* cancellationCheckpoint(signal, probes, 256);
528
+ id += 1;
529
+ probes += 1;
530
+ }
531
+ return {
532
+ id,
533
+ evidence: {
534
+ kind: 'state-id',
535
+ allocated: id,
536
+ strategy: 'lowest-positive-unused-active-id-across-roots',
537
+ highestObserved: highest,
538
+ contiguousBefore: contiguous([...used], 1),
539
+ probes,
540
+ roots: rootEvidence(index.statesAcrossRoots.map(({ id: value, file }) => ({ id: value, file }))),
541
+ },
542
+ };
543
+ }
544
+ function* allocateProvince(index, requested, signal) {
545
+ const highest = maximumObservedId(index.definitions.map(({ id }) => id), -1);
546
+ const contiguousBefore = contiguous(index.definitions.map(({ id: value }) => value), 0);
547
+ if (!contiguousBefore) {
548
+ throw new ServiceError('MAP_PROVINCE_ID_GAP', 'Province IDs must be contiguous before a new ID can be allocated');
549
+ }
550
+ let id = highest + 1;
551
+ let idStrategy = 'maximum-active-id-plus-one-across-roots';
552
+ if (requested !== undefined) {
553
+ if (!Number.isSafeInteger(requested) || requested < 0 || requested > 2_147_483_647)
554
+ throw new ServiceError('MAP_PROVINCE_ID_INVALID', 'Explicit province ID must be a nonnegative integer');
555
+ if (index.definitionsAcrossRoots.some(({ id: existingId }) => existingId === requested))
556
+ throw new ServiceError(index.definitionsById.has(requested)
557
+ ? 'MAP_PROVINCE_ID_COLLISION'
558
+ : 'MAP_DEPENDENCY_PROVINCE_ID_CONFLICT', `Explicit province ID ${requested} is already present in a scanned root`, {
559
+ provinceId: requested,
560
+ sources: index.definitionsAcrossRoots
561
+ .filter(({ id: existingId }) => existingId === requested)
562
+ .map(({ document }) => document.file.displayPath)
563
+ .sort(),
564
+ });
565
+ if (requested !== highest + 1)
566
+ throw new ServiceError('MAP_PROVINCE_ID_NOT_CONTIGUOUS', `Explicit province ID must be ${highest + 1} to preserve contiguous province definitions`);
567
+ id = requested;
568
+ idStrategy = 'explicit-request-after-full-scan';
569
+ }
570
+ if (!Number.isSafeInteger(id) || id > 2_147_483_647) {
571
+ throw new ServiceError('MAP_PROVINCE_ID_EXHAUSTED', 'No safe province ID remains available');
572
+ }
573
+ const crossRootId = index.definitionsAcrossRoots.find(({ id: existingId, document }) => existingId === id && document.file.displayPath !== index.definitionFile?.displayPath);
574
+ if (crossRootId !== undefined)
575
+ throw new ServiceError('MAP_DEPENDENCY_PROVINCE_ID_CONFLICT', `Next contiguous province ID ${id} collides with a lower-load-order definition`, { provinceId: id, source: crossRootId.document.file.displayPath });
576
+ const occupied = new Set(index.definitionsAcrossRoots.map(({ color }) => rgbKey(color)));
577
+ for (const unknown of index.raster?.unknownColors ?? [])
578
+ occupied.add(rgbKey(unknown.color));
579
+ const seed = Number.parseInt(sha256Bytes(`province-color:${id}`).slice(0, 6), 16);
580
+ let probes = 0;
581
+ while (probes < 0xff_ff_ff) {
582
+ yield* cancellationCheckpoint(signal, probes, 4096);
583
+ const candidate = ((seed + probes) % 0xff_ff_ff) + 1;
584
+ const color = {
585
+ r: (candidate >>> 16) & 0xff,
586
+ g: (candidate >>> 8) & 0xff,
587
+ b: candidate & 0xff,
588
+ };
589
+ if (!occupied.has(rgbKey(color))) {
590
+ return {
591
+ id,
592
+ color,
593
+ evidence: [
594
+ {
595
+ kind: 'province-id',
596
+ allocated: id,
597
+ strategy: idStrategy,
598
+ highestObserved: highest,
599
+ contiguousBefore,
600
+ roots: rootEvidence(index.definitionsAcrossRoots.map(({ id: value, document }) => ({
601
+ id: value,
602
+ file: document.file,
603
+ }))),
604
+ },
605
+ {
606
+ kind: 'province-color',
607
+ allocated: rgbKey(color),
608
+ strategy: 'sha256-seeded-linear-probe-excluding-definition-and-bitmap-colors',
609
+ probes: probes + 1,
610
+ occupiedCount: occupied.size,
611
+ roots: rootEvidence(index.definitionsAcrossRoots.map(({ id: value, document }) => ({
612
+ id: value,
613
+ file: document.file,
614
+ }))),
615
+ },
616
+ ],
617
+ };
618
+ }
619
+ probes += 1;
620
+ }
621
+ throw new ServiceError('MAP_PROVINCE_COLOR_EXHAUSTED', 'No unused 24-bit province color remains');
622
+ }
623
+ function* allocateMapIdentifierSteps(index, request, signal) {
624
+ yield* cancellationCheckpoint(signal);
625
+ if (request.kind === 'state') {
626
+ const allocation = yield* allocateStateId(index, request.requestedId, signal);
627
+ return { stateId: allocation.id, evidence: [allocation.evidence] };
628
+ }
629
+ const allocation = yield* allocateProvince(index, request.requestedId, signal);
630
+ if (request.requestedColor === undefined) {
631
+ return {
632
+ provinceId: allocation.id,
633
+ color: allocation.color,
634
+ evidence: allocation.evidence,
635
+ };
636
+ }
637
+ const color = request.requestedColor;
638
+ if (![color.r, color.g, color.b].every((channel) => Number.isInteger(channel) && channel >= 0 && channel <= 255) ||
639
+ (color.r === 0 && color.g === 0 && color.b === 0)) {
640
+ throw new ServiceError('MAP_PROVINCE_COLOR_INVALID', 'Explicit province color must be a non-black 24-bit RGB value');
641
+ }
642
+ const key = rgbKey(color);
643
+ const crossRootDefinition = index.definitionsAcrossRoots.find(({ color: existing }) => rgbKey(existing) === key);
644
+ if (crossRootDefinition !== undefined ||
645
+ index.raster?.unknownColors.some(({ color: unknown }) => rgbKey(unknown) === key)) {
646
+ throw new ServiceError(crossRootDefinition !== undefined && !index.definitionsByColor.has(key)
647
+ ? 'MAP_DEPENDENCY_PROVINCE_COLOR_CONFLICT'
648
+ : 'MAP_PROVINCE_COLOR_DUPLICATE', `Explicit province color ${key} is already used in a scanned source`, {
649
+ ...(crossRootDefinition === undefined
650
+ ? {}
651
+ : { source: crossRootDefinition.document.file.displayPath }),
652
+ });
653
+ }
654
+ return {
655
+ provinceId: allocation.id,
656
+ color,
657
+ evidence: allocation.evidence.map((entry) => entry.kind === 'province-color'
658
+ ? { ...entry, allocated: key, strategy: 'explicit-request-after-full-scan' }
659
+ : entry),
660
+ };
661
+ }
662
+ /** Preview deterministic identifiers only after the complete active map index has been scanned. */
663
+ export function allocateMapIdentifiers(index, request, signal) {
664
+ return completeSynchronously(allocateMapIdentifierSteps(index, request, signal));
665
+ }
666
+ export async function allocateMapIdentifiersAsync(index, request, signal) {
667
+ return completeCooperatively(allocateMapIdentifierSteps(index, request, signal), signal);
668
+ }
669
+ function syntheticFile(relativePath, bytes, loadOrder) {
670
+ return {
671
+ absolutePath: `virtual://${relativePath}`,
672
+ displayPath: `mod:${relativePath}`,
673
+ relativePath,
674
+ rootKind: 'mod',
675
+ loadOrder,
676
+ size: bytes.length,
677
+ modifiedMs: 0,
678
+ sha256: sha256Bytes(bytes),
679
+ bytes,
680
+ };
681
+ }
682
+ export function indexWithProposedChanges(base, changes) {
683
+ const maximumLoadOrder = base.sourceFiles.reduce((maximum, { loadOrder }) => Math.max(maximum, loadOrder), 0) + 1;
684
+ const byPath = new Map(changes.map((change) => [normalizeRelative(change.relativePath), change]));
685
+ const files = base.sourceFiles.filter((file) => !(file.rootKind === 'mod' && byPath.has(normalizeRelative(file.relativePath))));
686
+ for (const change of changes) {
687
+ if (change.content !== null)
688
+ files.push(syntheticFile(change.relativePath, Buffer.from(change.content), maximumLoadOrder));
689
+ }
690
+ return MapWorkspaceIndex.build(files, base.sourceRoots, base.sharedIndex.rebuild(files));
691
+ }
692
+ function addChange(changes, relativePath, content, operationId, mediaType) {
693
+ const key = normalizeRelative(relativePath);
694
+ const current = changes.get(key);
695
+ if (current === undefined) {
696
+ changes.set(key, {
697
+ relativePath: relativePath.replaceAll('\\', '/'),
698
+ content: content === null ? null : Buffer.from(content),
699
+ operationIds: new Set([operationId]),
700
+ mediaType,
701
+ });
702
+ }
703
+ else {
704
+ current.content = content === null ? null : Buffer.from(content);
705
+ current.operationIds.add(operationId);
706
+ current.mediaType = mediaType;
707
+ }
708
+ }
709
+ function proposedChanges(changes) {
710
+ return [...changes.values()]
711
+ .sort((left, right) => compareCodeUnits(left.relativePath, right.relativePath))
712
+ .map((change) => ({
713
+ relativePath: change.relativePath,
714
+ content: change.content,
715
+ operationIds: [...change.operationIds].sort((left, right) => compareCodeUnits(left, right)),
716
+ mediaType: change.mediaType,
717
+ }));
718
+ }
719
+ function currentIndex(base, changes) {
720
+ return indexWithProposedChanges(base, proposedChanges(changes));
721
+ }
722
+ function samePolicy(actual, expected) {
723
+ if (actual === null || typeof actual !== 'object')
724
+ return false;
725
+ const object = actual;
726
+ return Object.entries(expected).every(([key, value]) => object[key] === value);
727
+ }
728
+ function requireMovePolicy(operation) {
729
+ const distribution = operation.distribution;
730
+ if (!samePolicy(distribution, {
731
+ stateValues: 'retain-in-current-states',
732
+ ownership: 'retain-in-current-states',
733
+ provinceBuildings: 'follow-province',
734
+ victoryPoints: 'follow-province',
735
+ ports: 'follow-province',
736
+ supplyNodes: 'follow-province',
737
+ railways: 'follow-province',
738
+ positions: 'follow-province',
739
+ }) ||
740
+ (distribution?.strategicRegion !== 'require-same' &&
741
+ distribution?.strategicRegion !== 'move-to-target-region')) {
742
+ throw new ServiceError('MAP_STATE_DISTRIBUTION_REQUIRED', 'Moving state provinces requires explicit state-value, ownership, building, victory-point, port, network, position, and strategic-region policies');
743
+ }
744
+ }
745
+ function requireSplitPolicy(operation) {
746
+ const policy = operation
747
+ .distribution;
748
+ if (policy?.manpower === undefined ||
749
+ policy.resources === undefined ||
750
+ policy.stateBuildings === undefined ||
751
+ policy.owner === undefined ||
752
+ policy.controller === undefined ||
753
+ policy.cores === undefined ||
754
+ policy.claims === undefined ||
755
+ policy.victoryPoints !== 'follow-province' ||
756
+ policy.provinceBuildings !== 'follow-province' ||
757
+ policy.ports !== 'follow-province' ||
758
+ policy.supplyNodes !== 'follow-province' ||
759
+ policy.railways !== 'follow-province' ||
760
+ policy.positions !== 'follow-province') {
761
+ throw new ServiceError('MAP_STATE_DISTRIBUTION_REQUIRED', 'State creation requires a complete explicit value, ownership, port, network, position, and province-data distribution policy');
762
+ }
763
+ return policy;
764
+ }
765
+ function requireSafeLocalisationDocument(document, relativePath) {
766
+ if (document.encoding !== 'utf8-bom') {
767
+ throw new ServiceError('MAP_STATE_LOCALISATION_ENCODING_INVALID', 'State localisation edits require an existing UTF-8 BOM source file', { file: relativePath, encoding: document.encoding });
768
+ }
769
+ const errors = document.diagnostics.filter(({ severity }) => severity === 'error' || severity === 'blocker');
770
+ if (errors.length > 0) {
771
+ throw new ServiceError('MAP_STATE_LOCALISATION_SOURCE_INVALID', 'State localisation cannot target a malformed source file', { file: relativePath, diagnostics: errors.map(({ code }) => code) });
772
+ }
773
+ }
774
+ function safeLocalisationRelativePath(index, requested) {
775
+ const value = requested.replaceAll('\\', '/');
776
+ const segments = value.split('/');
777
+ if (value === '' ||
778
+ value.includes('\0') ||
779
+ value.startsWith('/') ||
780
+ /^[A-Za-z]:/u.test(value) ||
781
+ segments.some((segment) => segment === '' || segment === '.' || segment === '..') ||
782
+ path.posix.normalize(value) !== value ||
783
+ !/\.yml$/iu.test(value)) {
784
+ throw new ServiceError('MAP_STATE_LOCALISATION_TARGET_INVALID', 'State localisation target must be a safe relative .yml path', { file: requested });
785
+ }
786
+ const normalized = normalizeRelative(value);
787
+ if (!index.sourceRoots.localisation.some((root) => normalized === root || normalized.startsWith(`${root}/`))) {
788
+ throw new ServiceError('MAP_STATE_LOCALISATION_TARGET_INVALID', 'State localisation target must be inside a configured localisation root', { file: requested, roots: index.sourceRoots.localisation });
789
+ }
790
+ return value;
791
+ }
792
+ function escapedLocalisationValue(value) {
793
+ return value
794
+ .replaceAll('\\', '\\\\')
795
+ .replaceAll('"', '\\"')
796
+ .replaceAll('\r\n', '\\n')
797
+ .replaceAll('\r', '\\n')
798
+ .replaceAll('\n', '\\n');
799
+ }
800
+ function localisationEntryLine(document, entry, value) {
801
+ const line = document.text.slice(entry.start, entry.end);
802
+ const opening = line.indexOf('"');
803
+ if (opening < 0)
804
+ throw new ServiceError('MAP_STATE_LOCALISATION_SOURCE_INVALID', 'State localisation source entry has no quoted value', { file: document.path, line: entry.line });
805
+ let closing = -1;
806
+ let escaped = false;
807
+ for (let index = opening + 1; index < line.length; index += 1) {
808
+ const character = line[index];
809
+ if (escaped) {
810
+ escaped = false;
811
+ continue;
812
+ }
813
+ if (character === '\\') {
814
+ escaped = true;
815
+ continue;
816
+ }
817
+ if (character === '"') {
818
+ closing = index;
819
+ break;
820
+ }
821
+ }
822
+ if (closing < 0)
823
+ throw new ServiceError('MAP_STATE_LOCALISATION_SOURCE_INVALID', 'State localisation source entry has an unclosed quoted value', { file: document.path, line: entry.line });
824
+ return `${line.slice(0, opening + 1)}${escapedLocalisationValue(value)}${line.slice(closing)}`;
825
+ }
826
+ function appendLocalisationEntry(document, key, value) {
827
+ let englishHeaders = 0;
828
+ const matcher = /^\s*l_english:\s*(?:#.*)?$/gmu;
829
+ while (matcher.exec(document.text) !== null && englishHeaders < 2)
830
+ englishHeaders += 1;
831
+ if (englishHeaders !== 1) {
832
+ throw new ServiceError('MAP_STATE_LOCALISATION_SOURCE_INVALID', 'State localisation target must contain exactly one l_english declaration', { file: document.path, englishHeaders });
833
+ }
834
+ const separator = document.text.length === 0 || /(?:\r\n|\n|\r)$/u.test(document.text) ? '' : document.newline;
835
+ return encodeSource(`${document.text}${separator}${key}: "${escapedLocalisationValue(value)}"${document.newline}`, 'utf8-bom');
836
+ }
837
+ function applyStateLocalisation(index, changes, operation, key) {
838
+ const policy = operation.localisation;
839
+ if (policy === null || typeof policy !== 'object' || Array.isArray(policy)) {
840
+ throw new ServiceError('MAP_STATE_LOCALISATION_POLICY_REQUIRED', 'State creation requires an explicit existing or upsert localisation policy');
841
+ }
842
+ const object = policy;
843
+ const keys = Object.keys(object).sort();
844
+ const existingPolicy = object.method === 'existing' &&
845
+ object.language === 'l_english' &&
846
+ keys.join(',') === 'language,method';
847
+ const upsertPolicy = object.method === 'upsert' &&
848
+ object.language === 'l_english' &&
849
+ typeof object.value === 'string' &&
850
+ object.value.length > 0 &&
851
+ (object.file === undefined || (typeof object.file === 'string' && object.file.length > 0)) &&
852
+ (keys.join(',') === 'language,method,value' || keys.join(',') === 'file,language,method,value');
853
+ if (!existingPolicy && !upsertPolicy) {
854
+ throw new ServiceError('MAP_STATE_LOCALISATION_POLICY_REQUIRED', 'State creation localisation policy is incomplete or unsupported', { requiredLanguage: 'l_english', methods: ['existing', 'upsert'] });
855
+ }
856
+ const entries = index.localisationByKey.get(`l_english:${key}`) ?? [];
857
+ if (entries.length > 1) {
858
+ throw new ServiceError('MAP_STATE_LOCALISATION_AMBIGUOUS', `English localisation key ${key} has multiple active definitions`, {
859
+ key,
860
+ sources: entries.map(({ file, entry }) => ({ file: file.displayPath, line: entry.line })),
861
+ });
862
+ }
863
+ const current = entries[0];
864
+ if (existingPolicy) {
865
+ if (current === undefined) {
866
+ throw new ServiceError('MAP_STATE_LOCALISATION_MISSING', `English localisation key ${key} does not exist`, { key });
867
+ }
868
+ requireSafeLocalisationDocument(current.document, current.file.relativePath);
869
+ return;
870
+ }
871
+ const value = object.value;
872
+ const requestedFile = object.file;
873
+ if (current !== undefined) {
874
+ requireSafeLocalisationDocument(current.document, current.file.relativePath);
875
+ if (requestedFile !== undefined &&
876
+ normalizeRelative(safeLocalisationRelativePath(index, requestedFile)) !==
877
+ normalizeRelative(current.file.relativePath)) {
878
+ throw new ServiceError('MAP_STATE_LOCALISATION_TARGET_MISMATCH', `English localisation key ${key} exists in a different target file`, { requestedFile, existingFile: current.file.relativePath });
879
+ }
880
+ const replacement = localisationEntryLine(current.document, current.entry, value);
881
+ const text = `${current.document.text.slice(0, current.entry.start)}${replacement}${current.document.text.slice(current.entry.end)}`;
882
+ addChange(changes, current.file.relativePath, encodeSource(text, 'utf8-bom'), operation.id, 'text/yaml');
883
+ return;
884
+ }
885
+ if (requestedFile === undefined) {
886
+ throw new ServiceError('MAP_STATE_LOCALISATION_TARGET_REQUIRED', 'Creating an English localisation key requires an explicit target file', { key });
887
+ }
888
+ const relativePath = safeLocalisationRelativePath(index, requestedFile);
889
+ const target = index.activeFiles.byRelativePath.get(normalizeRelative(relativePath));
890
+ if (target === undefined) {
891
+ addChange(changes, relativePath, encodeSource(`l_english:\n${key}: "${escapedLocalisationValue(value)}"\n`, 'utf8-bom'), operation.id, 'text/yaml');
892
+ return;
893
+ }
894
+ const document = parseLocalisation(target.bytes, target.displayPath);
895
+ requireSafeLocalisationDocument(document, target.relativePath);
896
+ addChange(changes, target.relativePath, appendLocalisationEntry(document, key, value), operation.id, 'text/yaml');
897
+ }
898
+ function splitScalar(total, ratio, policy) {
899
+ if (policy.method === 'retain-in-source')
900
+ return [total, 0];
901
+ if (policy.method === 'proportional-by-land-pixels') {
902
+ const destination = Math.round(total * ratio);
903
+ return [total - destination, destination];
904
+ }
905
+ if (Math.abs(policy.source + policy.destination - total) > 0.000_001) {
906
+ throw new ServiceError('MAP_DISTRIBUTION_TOTAL_MISMATCH', 'Exact scalar distribution does not preserve the source total', {
907
+ total,
908
+ source: policy.source,
909
+ destination: policy.destination,
910
+ });
911
+ }
912
+ return [policy.source, policy.destination];
913
+ }
914
+ function splitMap(total, ratio, policy) {
915
+ if (policy.method === 'retain-in-source')
916
+ return [new Map(total), new Map()];
917
+ if (policy.method === 'proportional-by-land-pixels') {
918
+ const source = new Map();
919
+ const destination = new Map();
920
+ for (const [key, value] of total) {
921
+ const moved = Math.round(value * ratio * 1_000_000) / 1_000_000;
922
+ source.set(key, value - moved);
923
+ if (moved !== 0)
924
+ destination.set(key, moved);
925
+ }
926
+ return [source, destination];
927
+ }
928
+ const source = new Map(Object.entries(policy.source));
929
+ const destination = new Map(Object.entries(policy.destination));
930
+ const keys = new Set([...total.keys(), ...source.keys(), ...destination.keys()]);
931
+ for (const key of keys) {
932
+ if (Math.abs((source.get(key) ?? 0) + (destination.get(key) ?? 0) - (total.get(key) ?? 0)) >
933
+ 0.000_001) {
934
+ throw new ServiceError('MAP_DISTRIBUTION_TOTAL_MISMATCH', `Exact map distribution does not preserve ${key}`);
935
+ }
936
+ }
937
+ return [source, destination];
938
+ }
939
+ function splitTag(value, policy) {
940
+ if (policy.method === 'copy-source')
941
+ return [value, value];
942
+ return [policy.source ?? undefined, policy.destination ?? undefined];
943
+ }
944
+ function splitTags(value, policy) {
945
+ return policy.method === 'copy-source'
946
+ ? [[...value], [...value]]
947
+ : [uniqueStrings(policy.source), uniqueStrings(policy.destination)];
948
+ }
949
+ function provincePixelTotal(index, provinceIds) {
950
+ return provinceIds.reduce((sum, id) => sum + (index.raster?.geometry.get(id)?.pixelCount ?? 0), 0);
951
+ }
952
+ function mergeBounds(left, right) {
953
+ if (left === undefined)
954
+ return right;
955
+ if (right === undefined)
956
+ return left;
957
+ return {
958
+ minX: Math.min(left.minX, right.minX),
959
+ minY: Math.min(left.minY, right.minY),
960
+ maxX: Math.max(left.maxX, right.maxX),
961
+ maxY: Math.max(left.maxY, right.maxY),
962
+ count: left.count + right.count,
963
+ };
964
+ }
965
+ function regionMembershipId(index, provinceId) {
966
+ const regions = index.regionForProvince(provinceId);
967
+ if (regions.length !== 1)
968
+ throw new ServiceError('MAP_REGION_MEMBERSHIP_AMBIGUOUS', `Province ${provinceId} must have exactly one strategic region`);
969
+ return required(regions[0], 'MAP_REGION_MEMBERSHIP_AMBIGUOUS', `Province ${provinceId} has no indexed strategic region`).id;
970
+ }
971
+ function stateMembershipId(index, provinceId) {
972
+ const states = index.stateForProvince(provinceId);
973
+ if (states.length > 1)
974
+ throw new ServiceError('MAP_STATE_MEMBERSHIP_AMBIGUOUS', `Province ${provinceId} belongs to multiple states`);
975
+ return states[0]?.id;
976
+ }
977
+ function commitState(changes, state, data, fields, operationId) {
978
+ addChange(changes, state.file.relativePath, patchState(state, data, fields), operationId, 'text/plain');
979
+ }
980
+ function commitRegion(changes, region, provinces, operationId) {
981
+ addChange(changes, region.file.relativePath, patchRegion(region, provinces), operationId, 'text/plain');
982
+ }
983
+ function remapBuildingPositionStates(index, changes, provinceIds, targetStateId, operationId) {
984
+ const documents = new Map(index.buildingPositions.map((record) => [record.document.file.relativePath, record.document]));
985
+ for (const [relativePath, document] of documents) {
986
+ const replacements = index.buildingPositions
987
+ .filter((record) => record.document === document && record.stateId !== targetStateId)
988
+ .flatMap((record) => {
989
+ const provinceId = index.provinceAtMapCoordinate(record.x, record.z);
990
+ if (provinceId === undefined || !provinceIds.has(provinceId))
991
+ return [];
992
+ const line = document.lines[record.line - 1];
993
+ return line === undefined
994
+ ? []
995
+ : [
996
+ {
997
+ start: line.start,
998
+ end: line.end,
999
+ text: buildingPositionLine({ ...record, stateId: targetStateId }),
1000
+ description: 'Remap building-position state reference',
1001
+ },
1002
+ ];
1003
+ });
1004
+ if (replacements.length > 0)
1005
+ addChange(changes, relativePath, applyTextRanges(document, replacements), operationId, 'text/plain');
1006
+ }
1007
+ }
1008
+ function exactNumericMap(values, label) {
1009
+ const result = new Map();
1010
+ for (const [key, value] of Object.entries(values)) {
1011
+ if (key === '' || !Number.isFinite(value) || value < 0)
1012
+ throw new ServiceError('MAP_STATE_VALUE_INVALID', `${label}.${key || '<empty>'} must be nonnegative and finite`);
1013
+ result.set(key, value);
1014
+ }
1015
+ return result;
1016
+ }
1017
+ function applyUpdateState(base, changes, operation) {
1018
+ const index = currentIndex(base, changes);
1019
+ const state = index.statesById.get(operation.stateId);
1020
+ if (state === undefined)
1021
+ throw new ServiceError('MAP_STATE_NOT_FOUND', `State ${operation.stateId} does not exist`);
1022
+ const desired = cloneState(state);
1023
+ const fields = {};
1024
+ const values = operation.changes;
1025
+ if (values.capital !== undefined) {
1026
+ if (values.victoryPoints === undefined)
1027
+ throw new ServiceError('MAP_STATE_CAPITAL_ASSERTION_REQUIRES_VICTORY_POINTS', 'A derived state-capital assertion requires an exact victoryPoints payload');
1028
+ const derived = derivedStateCapital(values.victoryPoints);
1029
+ if ((values.capital ?? undefined) !== derived)
1030
+ throw new ServiceError('MAP_STATE_CAPITAL_ASSERTION_MISMATCH', 'The asserted state capital must be the highest victory point, using the lowest province ID on ties', { asserted: values.capital, derived: derived ?? null });
1031
+ }
1032
+ if (values.manpower !== undefined) {
1033
+ if (!Number.isFinite(values.manpower) || values.manpower < 0)
1034
+ throw new ServiceError('MAP_STATE_MANPOWER_INVALID', 'State manpower must be nonnegative and finite');
1035
+ desired.manpower = values.manpower;
1036
+ fields.manpower = true;
1037
+ }
1038
+ if (values.category !== undefined) {
1039
+ if (values.category.trim() === '')
1040
+ throw new ServiceError('MAP_STATE_CATEGORY_INVALID', 'State category cannot be empty');
1041
+ desired.category = values.category;
1042
+ fields.category = true;
1043
+ }
1044
+ if (values.resources !== undefined) {
1045
+ desired.resources = exactNumericMap(values.resources, 'resources');
1046
+ fields.resources = true;
1047
+ }
1048
+ if (values.owner !== undefined) {
1049
+ desired.owner = values.owner ?? undefined;
1050
+ fields.owner = true;
1051
+ }
1052
+ if (values.controller !== undefined) {
1053
+ desired.controller = values.controller ?? undefined;
1054
+ fields.controller = true;
1055
+ }
1056
+ if (values.cores !== undefined) {
1057
+ desired.cores = uniqueStrings(values.cores);
1058
+ fields.cores = true;
1059
+ }
1060
+ if (values.claims !== undefined) {
1061
+ desired.claims = uniqueStrings(values.claims);
1062
+ fields.claims = true;
1063
+ }
1064
+ if (values.victoryPoints !== undefined) {
1065
+ const seen = new Set();
1066
+ desired.victoryPoints = values.victoryPoints.map(({ provinceId, value }) => {
1067
+ if (!state.provinces.includes(provinceId) ||
1068
+ seen.has(provinceId) ||
1069
+ !Number.isFinite(value) ||
1070
+ value < 0)
1071
+ throw new ServiceError('MAP_STATE_VICTORY_POINT_INVALID', 'Victory points require unique member provinces and nonnegative finite values');
1072
+ seen.add(provinceId);
1073
+ return { provinceId, value };
1074
+ });
1075
+ fields.victoryPoints = true;
1076
+ }
1077
+ if (values.stateBuildings !== undefined) {
1078
+ desired.stateBuildings = exactNumericMap(values.stateBuildings, 'stateBuildings');
1079
+ fields.buildings = true;
1080
+ }
1081
+ if (values.provinceBuildings !== undefined) {
1082
+ const provinceBuildings = new Map();
1083
+ for (const [rawProvinceId, buildings] of Object.entries(values.provinceBuildings)) {
1084
+ if (!/^\d+$/u.test(rawProvinceId))
1085
+ throw new ServiceError('MAP_STATE_BUILDING_PROVINCE_INVALID', `Invalid province key ${rawProvinceId}`);
1086
+ const provinceId = Number(rawProvinceId);
1087
+ if (!Number.isSafeInteger(provinceId) ||
1088
+ provinceId < 0 ||
1089
+ provinceId > 2_147_483_647 ||
1090
+ !state.provinces.includes(provinceId))
1091
+ throw new ServiceError('MAP_STATE_BUILDING_PROVINCE_INVALID', `Province ${provinceId} is outside state ${state.id}`);
1092
+ provinceBuildings.set(provinceId, exactNumericMap(buildings, `provinceBuildings.${provinceId}`));
1093
+ }
1094
+ desired.provinceBuildings = provinceBuildings;
1095
+ fields.buildings = true;
1096
+ }
1097
+ commitState(changes, state, desired, fields, operation.id);
1098
+ }
1099
+ function applyMoveStateProvinces(base, changes, operation) {
1100
+ requireMovePolicy(operation);
1101
+ const index = currentIndex(base, changes);
1102
+ const source = index.statesById.get(operation.sourceStateId);
1103
+ const target = index.statesById.get(operation.targetStateId);
1104
+ if (source === undefined || target === undefined || source.id === target.id) {
1105
+ throw new ServiceError('MAP_STATE_SELECTION_INVALID', 'Source and target must be different existing states');
1106
+ }
1107
+ const moving = uniqueSorted(operation.provinceIds);
1108
+ if (moving.length === 0 || moving.some((id) => !source.provinces.includes(id))) {
1109
+ throw new ServiceError('MAP_STATE_PROVINCE_SELECTION_INVALID', 'Every moved province must belong to the source state');
1110
+ }
1111
+ if (source.provinces.length === moving.length) {
1112
+ throw new ServiceError('MAP_STATE_WOULD_BE_EMPTY', 'Moving every province would leave the source state empty');
1113
+ }
1114
+ const sourceData = cloneState(source);
1115
+ const targetData = cloneState(target);
1116
+ const movingSet = new Set(moving);
1117
+ sourceData.provinces = sourceData.provinces.filter((id) => !movingSet.has(id));
1118
+ targetData.provinces = uniqueSorted([...targetData.provinces, ...moving]);
1119
+ const movedVps = sourceData.victoryPoints.filter(({ provinceId }) => movingSet.has(provinceId));
1120
+ sourceData.victoryPoints = sourceData.victoryPoints.filter(({ provinceId }) => !movingSet.has(provinceId));
1121
+ targetData.victoryPoints.push(...movedVps);
1122
+ for (const provinceId of moving) {
1123
+ const buildings = sourceData.provinceBuildings.get(provinceId);
1124
+ if (buildings !== undefined) {
1125
+ targetData.provinceBuildings.set(provinceId, buildings);
1126
+ sourceData.provinceBuildings.delete(provinceId);
1127
+ }
1128
+ }
1129
+ const targetRegionIds = new Set(target.provinces.map((id) => regionMembershipId(index, id)));
1130
+ const movingRegionIds = new Set(moving.map((id) => regionMembershipId(index, id)));
1131
+ if (targetRegionIds.size !== 1)
1132
+ throw new ServiceError('MAP_TARGET_REGION_AMBIGUOUS', 'Target state spans multiple strategic regions');
1133
+ const targetRegionId = required([...targetRegionIds][0], 'MAP_TARGET_REGION_AMBIGUOUS', 'Target state has no strategic region');
1134
+ if (operation.distribution.strategicRegion === 'require-same' &&
1135
+ [...movingRegionIds].some((id) => id !== targetRegionId)) {
1136
+ throw new ServiceError('MAP_STATE_REGION_DISTRIBUTION_CONFLICT', 'Moved provinces are not in the target strategic region');
1137
+ }
1138
+ commitState(changes, source, sourceData, { provinces: true, victoryPoints: true, buildings: true }, operation.id);
1139
+ commitState(changes, target, targetData, { provinces: true, victoryPoints: true, buildings: true }, operation.id);
1140
+ if (operation.distribution.strategicRegion === 'move-to-target-region') {
1141
+ for (const regionId of movingRegionIds) {
1142
+ if (regionId === targetRegionId)
1143
+ continue;
1144
+ const region = required(index.regionsById.get(regionId), 'MAP_REGION_NOT_FOUND', `Strategic region ${regionId} does not exist`);
1145
+ commitRegion(changes, region, region.provinces.filter((id) => !movingSet.has(id)), operation.id);
1146
+ }
1147
+ const targetRegion = required(index.regionsById.get(targetRegionId), 'MAP_REGION_NOT_FOUND', `Strategic region ${targetRegionId} does not exist`);
1148
+ commitRegion(changes, targetRegion, uniqueSorted([...targetRegion.provinces, ...moving]), operation.id);
1149
+ }
1150
+ remapBuildingPositionStates(index, changes, movingSet, target.id, operation.id);
1151
+ }
1152
+ function* applySplitState(base, changes, allocations, operation, signal) {
1153
+ const policy = requireSplitPolicy(operation);
1154
+ const index = currentIndex(base, changes);
1155
+ const source = index.statesById.get(operation.sourceStateId);
1156
+ if (source === undefined)
1157
+ throw new ServiceError('MAP_STATE_NOT_FOUND', `State ${operation.sourceStateId} does not exist`);
1158
+ const selected = uniqueSorted(operation.provinceIds);
1159
+ const selectedSet = new Set(selected);
1160
+ if (selected.length === 0 || selected.some((id) => !source.provinces.includes(id))) {
1161
+ throw new ServiceError('MAP_STATE_PROVINCE_SELECTION_INVALID', 'Every new-state province must belong to the source state');
1162
+ }
1163
+ if (selected.length === source.provinces.length)
1164
+ throw new ServiceError('MAP_STATE_WOULD_BE_EMPTY', 'A split must leave provinces in the source state');
1165
+ const allocation = yield* allocateStateId(index, operation.stateId, signal);
1166
+ allocations.push(allocation.evidence);
1167
+ const sourcePixels = provincePixelTotal(index, source.provinces);
1168
+ const destinationPixels = provincePixelTotal(index, selected);
1169
+ if (sourcePixels <= 0 || destinationPixels <= 0)
1170
+ throw new ServiceError('MAP_STATE_DISTRIBUTION_GEOMETRY_MISSING', 'Proportional state distribution requires province geometry');
1171
+ const ratio = destinationPixels / sourcePixels;
1172
+ const sourceData = cloneState(source);
1173
+ const destinationData = cloneState(source);
1174
+ destinationData.id = allocation.id;
1175
+ if (operation.name === undefined || operation.name.trim() === '')
1176
+ throw new ServiceError('MAP_STATE_NAME_REQUIRED', 'State creation requires an explicit localisation key');
1177
+ destinationData.name = operation.name;
1178
+ applyStateLocalisation(index, changes, operation, destinationData.name);
1179
+ sourceData.provinces = sourceData.provinces.filter((id) => !selectedSet.has(id));
1180
+ destinationData.provinces = selected;
1181
+ [sourceData.manpower, destinationData.manpower] = splitScalar(source.manpower, ratio, policy.manpower);
1182
+ [sourceData.resources, destinationData.resources] = splitMap(source.resources, ratio, policy.resources);
1183
+ [sourceData.stateBuildings, destinationData.stateBuildings] = splitMap(source.stateBuildings, ratio, policy.stateBuildings);
1184
+ [sourceData.owner, destinationData.owner] = splitTag(source.owner, policy.owner);
1185
+ [sourceData.controller, destinationData.controller] = splitTag(source.controller, policy.controller);
1186
+ [sourceData.cores, destinationData.cores] = splitTags(source.cores, policy.cores);
1187
+ [sourceData.claims, destinationData.claims] = splitTags(source.claims, policy.claims);
1188
+ sourceData.victoryPoints = sourceData.victoryPoints.filter(({ provinceId }) => !selectedSet.has(provinceId));
1189
+ destinationData.victoryPoints = destinationData.victoryPoints.filter(({ provinceId }) => selectedSet.has(provinceId));
1190
+ sourceData.provinceBuildings = new Map([...sourceData.provinceBuildings].filter(([provinceId]) => !selectedSet.has(provinceId)));
1191
+ destinationData.provinceBuildings = new Map([...destinationData.provinceBuildings].filter(([provinceId]) => selectedSet.has(provinceId)));
1192
+ commitState(changes, source, sourceData, {
1193
+ manpower: true,
1194
+ resources: true,
1195
+ provinces: true,
1196
+ owner: true,
1197
+ controller: true,
1198
+ cores: true,
1199
+ claims: true,
1200
+ victoryPoints: true,
1201
+ buildings: true,
1202
+ }, operation.id);
1203
+ const requestedName = operation.fileName ?? `${allocation.id}-AGENT_STATE.txt`;
1204
+ const safeName = requestedName.replaceAll('\\', '/').split('/').at(-1) ?? requestedName;
1205
+ if (!/^[-A-Za-z0-9_.]+\.txt$/u.test(safeName))
1206
+ throw new ServiceError('MAP_STATE_FILENAME_INVALID', 'State filename is not safe');
1207
+ const statesRoot = index.sourceRoots.states[0] ?? 'history/states';
1208
+ const relativePath = `${statesRoot}/${safeName}`;
1209
+ if (index.activeFiles.byRelativePath.has(normalizeRelative(relativePath))) {
1210
+ throw new ServiceError('MAP_STATE_FILENAME_COLLISION', `State file already exists: ${relativePath}`);
1211
+ }
1212
+ addChange(changes, relativePath, compileState(destinationData, source.document.newline), operation.id, 'text/plain');
1213
+ remapBuildingPositionStates(index, changes, selectedSet, allocation.id, operation.id);
1214
+ }
1215
+ function requireMergeStatePolicy(operation) {
1216
+ if (!samePolicy(operation.distribution, {
1217
+ stateValues: 'sum-into-target',
1218
+ ownership: 'retain-target',
1219
+ controller: 'retain-target',
1220
+ cores: 'union',
1221
+ claims: 'union',
1222
+ victoryPoints: 'follow-province',
1223
+ provinceBuildings: 'follow-province',
1224
+ ports: 'follow-province',
1225
+ supplyNodes: 'follow-province',
1226
+ railways: 'follow-province',
1227
+ positions: 'follow-province',
1228
+ strategicRegion: 'require-same',
1229
+ })) {
1230
+ throw new ServiceError('MAP_STATE_DISTRIBUTION_REQUIRED', 'State merge requires a complete explicit value, ownership, port, network, position, and province-data distribution policy');
1231
+ }
1232
+ }
1233
+ function removalContent(index, file, label) {
1234
+ const lowerExists = index.sourceFiles.some((candidate) => normalizeRelative(candidate.relativePath) === normalizeRelative(file.relativePath) &&
1235
+ candidate.displayPath !== file.displayPath &&
1236
+ candidate.loadOrder < file.loadOrder);
1237
+ return file.rootKind === 'mod' && !lowerExists
1238
+ ? null
1239
+ : Buffer.from(`# ${label}${file.bytes.includes(13) ? '\r\n' : '\n'}`, 'utf8');
1240
+ }
1241
+ function addMaps(target, source) {
1242
+ for (const [key, value] of source)
1243
+ target.set(key, (target.get(key) ?? 0) + value);
1244
+ }
1245
+ function applyMergeStates(base, changes, operation) {
1246
+ requireMergeStatePolicy(operation);
1247
+ const index = currentIndex(base, changes);
1248
+ const target = index.statesById.get(operation.targetStateId);
1249
+ const sourceIds = uniqueSorted(operation.sourceStateIds).filter((id) => id !== operation.targetStateId);
1250
+ const sources = sourceIds.flatMap((id) => {
1251
+ const state = index.statesById.get(id);
1252
+ return state === undefined ? [] : [state];
1253
+ });
1254
+ if (target === undefined || sourceIds.length === 0 || sources.length !== sourceIds.length) {
1255
+ throw new ServiceError('MAP_STATE_SELECTION_INVALID', 'Merge target and source states must exist');
1256
+ }
1257
+ const regionIds = new Set([...target.provinces, ...sources.flatMap((state) => state.provinces)].map((id) => regionMembershipId(index, id)));
1258
+ if (regionIds.size !== 1)
1259
+ throw new ServiceError('MAP_STATE_REGION_DISTRIBUTION_CONFLICT', 'Merged states must share one strategic region');
1260
+ const data = cloneState(target);
1261
+ for (const source of sources) {
1262
+ data.manpower += source.manpower;
1263
+ addMaps(data.resources, source.resources);
1264
+ addMaps(data.stateBuildings, source.stateBuildings);
1265
+ data.provinces.push(...source.provinces);
1266
+ data.cores.push(...source.cores);
1267
+ data.claims.push(...source.claims);
1268
+ data.victoryPoints.push(...source.victoryPoints.map(({ provinceId, value }) => ({ provinceId, value })));
1269
+ for (const [provinceId, buildings] of source.provinceBuildings)
1270
+ data.provinceBuildings.set(provinceId, new Map(buildings));
1271
+ }
1272
+ data.provinces = uniqueSorted(data.provinces);
1273
+ data.cores = uniqueStrings(data.cores);
1274
+ data.claims = uniqueStrings(data.claims);
1275
+ commitState(changes, target, data, {
1276
+ manpower: true,
1277
+ resources: true,
1278
+ provinces: true,
1279
+ cores: true,
1280
+ claims: true,
1281
+ victoryPoints: true,
1282
+ buildings: true,
1283
+ }, operation.id);
1284
+ for (const source of sources) {
1285
+ addChange(changes, source.file.relativePath, removalContent(index, source.file, `State ${source.id} merged into ${target.id}`), operation.id, 'text/plain');
1286
+ }
1287
+ remapBuildingPositionStates(index, changes, new Set(sources.flatMap(({ provinces }) => provinces)), target.id, operation.id);
1288
+ }
1289
+ function pointInEvenOddPolygon(x, y, points) {
1290
+ let inside = false;
1291
+ for (let current = 0, previous = points.length - 1; current < points.length; previous = current, current += 1) {
1292
+ const a = required(points[current], 'MAP_POLYGON_INVALID', 'Polygon vertex is missing');
1293
+ const b = required(points[previous], 'MAP_POLYGON_INVALID', 'Polygon vertex is missing');
1294
+ const intersects = a.y > y !== b.y > y && x < ((b.x - a.x) * (y - a.y)) / (b.y - a.y) + a.x;
1295
+ if (intersects)
1296
+ inside = !inside;
1297
+ }
1298
+ return inside;
1299
+ }
1300
+ function assertSelectedPixelBudget(selectedPixels, source) {
1301
+ if (!Number.isSafeInteger(selectedPixels) || selectedPixels < 0) {
1302
+ throw new ServiceError('MAP_SELECTED_PIXEL_BUDGET_BLOCKED', `${source} declares an invalid selected-pixel count`, { source, selectedPixels, maximumSelectedPixels: MAP_SELECTED_PIXEL_LIMIT });
1303
+ }
1304
+ if (selectedPixels > MAP_SELECTED_PIXEL_LIMIT) {
1305
+ throw new ServiceError('MAP_SELECTED_PIXEL_BUDGET_BLOCKED', `${source} exceeds the fixed selected-pixel memory budget`, {
1306
+ source,
1307
+ selectedPixels,
1308
+ maximumSelectedPixels: MAP_SELECTED_PIXEL_LIMIT,
1309
+ maximumOffsetBytes: MAP_SELECTED_PIXEL_LIMIT * MAP_SELECTED_OFFSET_BYTES,
1310
+ });
1311
+ }
1312
+ }
1313
+ function packedRgb({ r, g, b }) {
1314
+ return (r << 16) | (g << 8) | b;
1315
+ }
1316
+ function* rasterMaskPixels(raster, geometry, signal) {
1317
+ const { width, height, origin, selectedPixelCount, sha256, data } = geometry;
1318
+ assertSelectedPixelBudget(selectedPixelCount, 'Raster-mask geometry');
1319
+ if (!Number.isInteger(width) ||
1320
+ !Number.isInteger(height) ||
1321
+ width <= 0 ||
1322
+ height <= 0 ||
1323
+ !Number.isInteger(origin.x) ||
1324
+ !Number.isInteger(origin.y) ||
1325
+ origin.x < 0 ||
1326
+ origin.y < 0 ||
1327
+ origin.x + width > raster.width ||
1328
+ origin.y + height > raster.height) {
1329
+ throw new ServiceError('MAP_RASTER_MASK_DIMENSIONS_MISMATCH', 'Raster-mask dimensions and origin must describe a positive rectangle inside the active province raster', {
1330
+ mask: { width, height, origin },
1331
+ raster: { width: raster.width, height: raster.height },
1332
+ });
1333
+ }
1334
+ const cells = width * height;
1335
+ if (!Number.isSafeInteger(cells) || cells > MAP_MASK_CELL_LIMIT) {
1336
+ throw new ServiceError('MAP_RASTER_MASK_DIMENSIONS_MISMATCH', 'Raster-mask dimensions exceed the supported exact-pixel limit', { width, height, maximumCells: MAP_MASK_CELL_LIMIT });
1337
+ }
1338
+ if (data.length === 0 || data.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/u.test(data)) {
1339
+ throw new ServiceError('MAP_RASTER_MASK_ENCODING_INVALID', 'Raster-mask data must be canonical Base64');
1340
+ }
1341
+ const bytes = Buffer.from(data, 'base64');
1342
+ if (bytes.toString('base64') !== data || bytes.length !== cells) {
1343
+ throw new ServiceError('MAP_RASTER_MASK_DIMENSIONS_MISMATCH', 'Raster-mask byte length must equal width multiplied by height', { expectedBytes: cells, actualBytes: bytes.length });
1344
+ }
1345
+ let actualSelectedPixelCount = 0;
1346
+ for (const [index, value] of bytes.entries()) {
1347
+ yield* cancellationCheckpoint(signal, index, 4096);
1348
+ if (value !== 0 && value !== 1) {
1349
+ throw new ServiceError('MAP_RASTER_MASK_ENCODING_INVALID', 'Raster-mask bytes must contain only canonical 0 or 1 cell values');
1350
+ }
1351
+ actualSelectedPixelCount += value;
1352
+ }
1353
+ const actualSha256 = sha256Bytes(bytes);
1354
+ if (!/^[a-f0-9]{64}$/u.test(sha256) || actualSha256 !== sha256) {
1355
+ throw new ServiceError('MAP_RASTER_MASK_HASH_MISMATCH', 'Raster-mask SHA-256 does not match its decoded exact-pixel payload', { expected: sha256, actual: actualSha256 });
1356
+ }
1357
+ if (!Number.isInteger(selectedPixelCount) ||
1358
+ selectedPixelCount < 0 ||
1359
+ selectedPixelCount !== actualSelectedPixelCount) {
1360
+ throw new ServiceError('MAP_RASTER_MASK_COUNT_MISMATCH', 'Raster-mask selected-pixel count does not match its decoded payload', { expected: selectedPixelCount, actual: actualSelectedPixelCount });
1361
+ }
1362
+ const result = new Uint32Array(selectedPixelCount);
1363
+ let selectedIndex = 0;
1364
+ for (let localY = 0; localY < height; localY += 1) {
1365
+ yield* cancellationCheckpoint(signal, localY, 16);
1366
+ for (let localX = 0; localX < width; localX += 1) {
1367
+ if (bytes[localY * width + localX] === 1) {
1368
+ result[selectedIndex] = (origin.y + localY) * raster.width + (origin.x + localX);
1369
+ selectedIndex += 1;
1370
+ }
1371
+ }
1372
+ }
1373
+ return result;
1374
+ }
1375
+ function* selectedPixels(index, provinceId, geometry, signal) {
1376
+ yield* cancellationCheckpoint(signal);
1377
+ const raster = index.raster;
1378
+ if (raster === undefined)
1379
+ throw new ServiceError('MAP_PROVINCE_BITMAP_MISSING', 'Province geometry requires provinces.bmp');
1380
+ if (geometry.kind === 'pixels') {
1381
+ assertSelectedPixelBudget(geometry.pixels.length, 'Explicit pixel geometry');
1382
+ const offsets = new Uint32Array(geometry.pixels.length);
1383
+ for (const [pointIndex, point] of geometry.pixels.entries()) {
1384
+ yield* cancellationCheckpoint(signal, pointIndex, 4096);
1385
+ if (!Number.isInteger(point.x) ||
1386
+ !Number.isInteger(point.y) ||
1387
+ point.x < 0 ||
1388
+ point.y < 0 ||
1389
+ point.x >= raster.width ||
1390
+ point.y >= raster.height) {
1391
+ throw new ServiceError('MAP_GEOMETRY_OUT_OF_BOUNDS', 'Province geometry contains an out-of-bounds pixel', { ...point });
1392
+ }
1393
+ const offset = point.y * raster.width + point.x;
1394
+ if (raster.provinceIds[offset] !== provinceId) {
1395
+ throw new ServiceError('MAP_GEOMETRY_OUTSIDE_SOURCE', 'Every selected pixel must belong to the source province', { ...point, provinceId });
1396
+ }
1397
+ offsets[pointIndex] = offset;
1398
+ }
1399
+ offsets.sort();
1400
+ let uniqueCount = 0;
1401
+ let previous;
1402
+ for (const offset of offsets) {
1403
+ if (offset === previous)
1404
+ continue;
1405
+ offsets[uniqueCount] = offset;
1406
+ uniqueCount += 1;
1407
+ previous = offset;
1408
+ }
1409
+ return offsets.subarray(0, uniqueCount);
1410
+ }
1411
+ if (geometry.kind === 'mask') {
1412
+ const offsets = yield* rasterMaskPixels(raster, geometry, signal);
1413
+ for (let indexOffset = 0; indexOffset < offsets.length; indexOffset += 1) {
1414
+ yield* cancellationCheckpoint(signal, indexOffset, 4096);
1415
+ const offset = offsets[indexOffset];
1416
+ if (offset === undefined || raster.provinceIds[offset] !== provinceId) {
1417
+ const safeOffset = offset ?? 0;
1418
+ throw new ServiceError('MAP_GEOMETRY_OUTSIDE_SOURCE', 'Every selected pixel must belong to the source province', {
1419
+ x: safeOffset % raster.width,
1420
+ y: Math.floor(safeOffset / raster.width),
1421
+ provinceId,
1422
+ });
1423
+ }
1424
+ }
1425
+ return offsets;
1426
+ }
1427
+ {
1428
+ if (geometry.points.length < 3)
1429
+ throw new ServiceError('MAP_POLYGON_INVALID', 'Province polygon requires at least three points');
1430
+ if (geometry.fillRule !== 'even-odd') {
1431
+ throw new ServiceError('MAP_POLYGON_FILL_RULE_UNSUPPORTED', 'Province polygon geometry requires the explicit even-odd fill rule', { fillRule: geometry.fillRule ?? null });
1432
+ }
1433
+ for (const [vertexIndex, point] of geometry.points.entries()) {
1434
+ if (!Number.isInteger(point.x) || !Number.isInteger(point.y)) {
1435
+ throw new ServiceError('MAP_POLYGON_INVALID', 'Province polygon vertices must use integer raster-boundary coordinates', { vertexIndex, vertex: point });
1436
+ }
1437
+ if (point.x < 0 || point.x > raster.width || point.y < 0 || point.y > raster.height) {
1438
+ throw new ServiceError('MAP_GEOMETRY_OUT_OF_BOUNDS', 'Province polygon vertex is outside the active raster boundary coordinates', {
1439
+ vertexIndex,
1440
+ vertex: point,
1441
+ allowed: { minX: 0, minY: 0, maxX: raster.width, maxY: raster.height },
1442
+ });
1443
+ }
1444
+ }
1445
+ let rawMinX = Number.POSITIVE_INFINITY;
1446
+ let rawMaxX = Number.NEGATIVE_INFINITY;
1447
+ let rawMinY = Number.POSITIVE_INFINITY;
1448
+ let rawMaxY = Number.NEGATIVE_INFINITY;
1449
+ for (const point of geometry.points) {
1450
+ rawMinX = Math.min(rawMinX, point.x);
1451
+ rawMaxX = Math.max(rawMaxX, point.x);
1452
+ rawMinY = Math.min(rawMinY, point.y);
1453
+ rawMaxY = Math.max(rawMaxY, point.y);
1454
+ }
1455
+ // Vertices are cell-boundary coordinates. The maximum boundary is exclusive
1456
+ // for pixel centers, so width/height are valid vertices without clipping.
1457
+ const minX = rawMinX;
1458
+ const maxX = rawMaxX - 1;
1459
+ const minY = rawMinY;
1460
+ const maxY = rawMaxY - 1;
1461
+ const width = Math.max(0, maxX - minX + 1);
1462
+ const height = Math.max(0, maxY - minY + 1);
1463
+ const cells = width * height;
1464
+ const work = cells * geometry.points.length;
1465
+ if (!Number.isSafeInteger(work) || work > MAP_POLYGON_WORK_LIMIT) {
1466
+ throw new ServiceError('MAP_POLYGON_WORK_LIMIT', 'Province polygon exceeds the configured geometry work budget');
1467
+ }
1468
+ const offsets = new Uint32Array(Math.min(cells, MAP_SELECTED_PIXEL_LIMIT));
1469
+ let selectedCount = 0;
1470
+ let examined = 0;
1471
+ for (let y = minY; y <= maxY; y += 1) {
1472
+ yield* cancellationCheckpoint(signal, y - minY, 8);
1473
+ for (let x = minX; x <= maxX; x += 1) {
1474
+ yield* cancellationCheckpoint(signal, examined, 4_096);
1475
+ examined += 1;
1476
+ if (!pointInEvenOddPolygon(x + 0.5, y + 0.5, geometry.points))
1477
+ continue;
1478
+ if (selectedCount >= MAP_SELECTED_PIXEL_LIMIT) {
1479
+ assertSelectedPixelBudget(selectedCount + 1, 'Polygon geometry');
1480
+ }
1481
+ const offset = y * raster.width + x;
1482
+ if (raster.provinceIds[offset] !== provinceId) {
1483
+ throw new ServiceError('MAP_GEOMETRY_OUTSIDE_SOURCE', 'Every selected pixel must belong to the source province', { x, y, provinceId });
1484
+ }
1485
+ offsets[selectedCount] = offset;
1486
+ selectedCount += 1;
1487
+ }
1488
+ }
1489
+ return offsets.subarray(0, selectedCount);
1490
+ }
1491
+ }
1492
+ function* provincePixelOffsets(raster, provinceIds, source, signal) {
1493
+ let expectedPixelCount = 0;
1494
+ for (const provinceId of provinceIds) {
1495
+ const pixelCount = raster.geometry.get(provinceId)?.pixelCount ?? 0;
1496
+ if (!Number.isSafeInteger(pixelCount) || pixelCount < 0) {
1497
+ throw new ServiceError('MAP_RASTER_GEOMETRY_MISMATCH', `${source} cannot trust the indexed province pixel count`, { source, provinceId, pixelCount });
1498
+ }
1499
+ expectedPixelCount += pixelCount;
1500
+ }
1501
+ assertSelectedPixelBudget(expectedPixelCount, source);
1502
+ const offsets = new Uint32Array(expectedPixelCount);
1503
+ let selectedCount = 0;
1504
+ for (let offset = 0; offset < raster.provinceIds.length; offset += 1) {
1505
+ yield* cancellationCheckpoint(signal, offset, 4096);
1506
+ if (!provinceIds.has(raster.provinceIds[offset] ?? -1))
1507
+ continue;
1508
+ if (selectedCount >= offsets.length) {
1509
+ assertSelectedPixelBudget(selectedCount + 1, source);
1510
+ throw new ServiceError('MAP_RASTER_GEOMETRY_MISMATCH', `${source} found more pixels than the indexed province geometry declared`, { source, expectedPixelCount, observedPixelCount: selectedCount + 1 });
1511
+ }
1512
+ offsets[selectedCount] = offset;
1513
+ selectedCount += 1;
1514
+ }
1515
+ if (selectedCount !== expectedPixelCount) {
1516
+ throw new ServiceError('MAP_RASTER_GEOMETRY_MISMATCH', `${source} found fewer pixels than the indexed province geometry declared`, { source, expectedPixelCount, observedPixelCount: selectedCount });
1517
+ }
1518
+ return offsets;
1519
+ }
1520
+ function definitionFromOperation(source, operation, id, allocatedColor) {
1521
+ const values = operation.definition.method === 'inherit-source'
1522
+ ? {
1523
+ type: operation.definition.overrides?.type ?? source.type,
1524
+ coastal: operation.definition.overrides?.coastal ?? source.coastal,
1525
+ terrain: operation.definition.overrides?.terrain ?? source.terrain,
1526
+ continent: operation.definition.overrides?.continent ?? source.continent,
1527
+ color: operation.definition.overrides?.color ?? allocatedColor,
1528
+ }
1529
+ : {
1530
+ ...operation.definition.value,
1531
+ color: operation.definition.value.color ?? allocatedColor,
1532
+ };
1533
+ return {
1534
+ id,
1535
+ color: values.color,
1536
+ type: values.type,
1537
+ coastal: values.coastal,
1538
+ terrain: values.terrain,
1539
+ continent: values.continent,
1540
+ line: source.document.lines.length + 1,
1541
+ document: source.document,
1542
+ };
1543
+ }
1544
+ function requireSplitProvincePolicy(operation, source) {
1545
+ const policy = operation.distribution;
1546
+ if (policy?.strategicRegion !== 'inherit-source' ||
1547
+ policy.victoryPoints !== 'retain-source' ||
1548
+ policy.provinceBuildings !== 'retain-source' ||
1549
+ policy.ports !== 'retain-source' ||
1550
+ policy.supplyNodes !== 'retain-source' ||
1551
+ policy.railways !== 'retain-source' ||
1552
+ policy.adjacencies !== 'retain-source' ||
1553
+ policy.positions !== 'retain-source' ||
1554
+ policy.entityLocators !== 'retain-source' ||
1555
+ (source.type === 'land' ? policy.state !== 'inherit-source' : policy.state !== 'none')) {
1556
+ throw new ServiceError('MAP_PROVINCE_DISTRIBUTION_REQUIRED', 'Province creation requires explicit compatible state, region, data, port, network, adjacency, position, and locator policies');
1557
+ }
1558
+ }
1559
+ function* applySplitProvince(base, changes, allocations, operation, signal) {
1560
+ const index = currentIndex(base, changes);
1561
+ const source = index.definitionsById.get(operation.sourceProvinceId);
1562
+ const bitmap = index.provinceBitmap;
1563
+ if (source === undefined || bitmap?.bitsPerPixel !== 24) {
1564
+ throw new ServiceError('MAP_PROVINCE_SOURCE_INVALID', 'Province split requires an existing province and 24-bit provinces.bmp');
1565
+ }
1566
+ requireSplitProvincePolicy(operation, source);
1567
+ const pixels = yield* selectedPixels(index, source.id, operation.geometry, signal);
1568
+ const total = index.raster?.geometry.get(source.id)?.pixelCount ?? 0;
1569
+ if (pixels.length === 0 || pixels.length >= total)
1570
+ throw new ServiceError('MAP_PROVINCE_SPLIT_GEOMETRY_INVALID', 'Split geometry must select some but not all source pixels');
1571
+ const allocation = yield* allocateProvince(index, operation.provinceId, signal);
1572
+ const definition = definitionFromOperation(source, operation, allocation.id, allocation.color);
1573
+ if (definition.color.r === 0 && definition.color.g === 0 && definition.color.b === 0)
1574
+ throw new ServiceError('MAP_PROVINCE_COLOR_INVALID', 'Black cannot be allocated to a province');
1575
+ const existingColor = index.definitionsAcrossRoots.find(({ color }) => rgbKey(color) === rgbKey(definition.color));
1576
+ if (existingColor !== undefined ||
1577
+ index.raster?.unknownColors.some(({ color }) => rgbKey(color) === rgbKey(definition.color))) {
1578
+ throw new ServiceError(existingColor !== undefined && !index.definitionsByColor.has(rgbKey(definition.color))
1579
+ ? 'MAP_DEPENDENCY_PROVINCE_COLOR_CONFLICT'
1580
+ : 'MAP_PROVINCE_COLOR_DUPLICATE', `Province color ${rgbKey(definition.color)} is already used`, {
1581
+ ...(existingColor === undefined ? {} : { source: existingColor.document.file.displayPath }),
1582
+ });
1583
+ }
1584
+ allocations.push(...allocation.evidence.map((evidence) => evidence.kind === 'province-color'
1585
+ ? { ...evidence, allocated: rgbKey(definition.color) }
1586
+ : evidence));
1587
+ const changedBitmap = bitmap.withRgbOffsets(pixels, definition.color);
1588
+ addChange(changes, required(index.provinceBitmapFile, 'MAP_PROVINCE_BITMAP_MISSING', 'Province bitmap file is missing').relativePath, changedBitmap.encode(), operation.id, 'image/bmp');
1589
+ addChange(changes, required(index.definitionFile, 'MAP_DEFINITION_FILE_MISSING', 'Definition file is missing')
1590
+ .relativePath, patchDefinitions(index, new Map(), new Set(), [definition]), operation.id, 'text/plain');
1591
+ if (source.type === 'land') {
1592
+ const stateId = stateMembershipId(index, source.id);
1593
+ if (stateId === undefined)
1594
+ throw new ServiceError('MAP_STATE_MEMBERSHIP_MISSING', `Land province ${source.id} has no state`);
1595
+ const state = required(index.statesById.get(stateId), 'MAP_STATE_NOT_FOUND', `State ${stateId} does not exist`);
1596
+ const data = cloneState(state);
1597
+ data.provinces = uniqueSorted([...data.provinces, definition.id]);
1598
+ commitState(changes, state, data, { provinces: true }, operation.id);
1599
+ }
1600
+ const sourceRegionId = regionMembershipId(index, source.id);
1601
+ const region = required(index.regionsById.get(sourceRegionId), 'MAP_REGION_NOT_FOUND', `Strategic region ${sourceRegionId} does not exist`);
1602
+ commitRegion(changes, region, uniqueSorted([...region.provinces, definition.id]), operation.id);
1603
+ return required(bitmap.diffBounds(changedBitmap), 'MAP_PROVINCE_SPLIT_NO_CHANGE', 'Province split did not change any bitmap pixel');
1604
+ }
1605
+ function updateDefinitionValue(source, changes) {
1606
+ return {
1607
+ ...source,
1608
+ color: changes.color ?? source.color,
1609
+ type: changes.type ?? source.type,
1610
+ coastal: changes.coastal ?? source.coastal,
1611
+ terrain: changes.terrain ?? source.terrain,
1612
+ continent: changes.continent ?? source.continent,
1613
+ };
1614
+ }
1615
+ function requireProvinceTypeDistribution(operation) {
1616
+ const distribution = operation.distribution;
1617
+ const expectedKeys = [
1618
+ 'stateMembership',
1619
+ 'stateValues',
1620
+ 'strategicRegion',
1621
+ 'victoryPoints',
1622
+ 'provinceBuildings',
1623
+ 'ports',
1624
+ 'supplyNodes',
1625
+ 'railways',
1626
+ 'buildingPositions',
1627
+ 'unitPositions',
1628
+ 'entityLocators',
1629
+ 'adjacencies',
1630
+ ].sort();
1631
+ if (distribution === null || typeof distribution !== 'object' || Array.isArray(distribution)) {
1632
+ throw new ServiceError('MAP_PROVINCE_TYPE_DISTRIBUTION_REQUIRED', 'Province type migration requires a complete explicit dependency distribution policy', { missing: expectedKeys });
1633
+ }
1634
+ const object = distribution;
1635
+ const actualKeys = Object.keys(object).sort();
1636
+ const missing = expectedKeys.filter((key) => !actualKeys.includes(key));
1637
+ const unexpected = actualKeys.filter((key) => !expectedKeys.includes(key));
1638
+ const stateMembership = object.stateMembership;
1639
+ const stateMembershipValid = (() => {
1640
+ if (stateMembership === null ||
1641
+ typeof stateMembership !== 'object' ||
1642
+ Array.isArray(stateMembership))
1643
+ return false;
1644
+ const value = stateMembership;
1645
+ const keys = Object.keys(value).sort();
1646
+ if (value.method === 'retain')
1647
+ return keys.join(',') === 'method';
1648
+ return ((value.method === 'remove' || value.method === 'assign') &&
1649
+ keys.join(',') === 'method,stateId' &&
1650
+ Number.isSafeInteger(value.stateId) &&
1651
+ Number(value.stateId) > 0);
1652
+ })();
1653
+ const literalValid = object.stateValues === 'retain-in-current-states' &&
1654
+ object.strategicRegion === 'retain-membership' &&
1655
+ (object.victoryPoints === 'retain-if-valid' || object.victoryPoints === 'remove') &&
1656
+ (object.provinceBuildings === 'retain-if-valid' || object.provinceBuildings === 'remove') &&
1657
+ (object.ports === 'retain-if-valid' || object.ports === 'remove') &&
1658
+ (object.supplyNodes === 'retain-if-valid' || object.supplyNodes === 'remove') &&
1659
+ (object.railways === 'retain-if-valid' || object.railways === 'remove-containing') &&
1660
+ (object.buildingPositions === 'retain-if-valid' || object.buildingPositions === 'remove') &&
1661
+ (object.unitPositions === 'retain-if-valid' || object.unitPositions === 'remove') &&
1662
+ object.entityLocators === 'retain-at-coordinate' &&
1663
+ (object.adjacencies === 'retain-if-valid' || object.adjacencies === 'remove-referencing');
1664
+ if (missing.length > 0 || unexpected.length > 0 || !stateMembershipValid || !literalValid) {
1665
+ throw new ServiceError('MAP_PROVINCE_TYPE_DISTRIBUTION_REQUIRED', 'Province type migration requires every dependency policy with no unknown fields', {
1666
+ missing,
1667
+ unexpected,
1668
+ stateMembershipValid,
1669
+ literalPoliciesValid: literalValid,
1670
+ });
1671
+ }
1672
+ return distribution;
1673
+ }
1674
+ function provinceTypeDistributionConflict(source, targetType, field, message, details = {}) {
1675
+ throw new ServiceError('MAP_PROVINCE_TYPE_DISTRIBUTION_INCONSISTENT', message, {
1676
+ provinceId: source.id,
1677
+ sourceType: source.type,
1678
+ targetType,
1679
+ field,
1680
+ ...details,
1681
+ });
1682
+ }
1683
+ function* applyUpdateProvinceDefinition(base, changes, operation, signal) {
1684
+ const index = currentIndex(base, changes);
1685
+ const source = index.definitionsById.get(operation.provinceId);
1686
+ if (source === undefined)
1687
+ throw new ServiceError('MAP_PROVINCE_NOT_FOUND', `Province ${operation.provinceId} does not exist`);
1688
+ const updated = updateDefinitionValue(source, operation.changes);
1689
+ const typeChanged = updated.type !== source.type;
1690
+ if (!typeChanged && operation.distribution !== undefined)
1691
+ throw new ServiceError('MAP_PROVINCE_TYPE_DISTRIBUTION_UNEXPECTED', 'A province type distribution policy is only valid when the type actually changes', { provinceId: source.id, type: source.type });
1692
+ const supportedTypes = ['land', 'sea', 'lake'];
1693
+ if (typeChanged && !supportedTypes.includes(updated.type))
1694
+ throw new ServiceError('MAP_PROVINCE_TYPE_INVALID', 'Province type must be land, sea, or lake', { provinceId: source.id, type: updated.type });
1695
+ const targetType = updated.type;
1696
+ if (typeChanged && !supportedTypes.includes(source.type))
1697
+ provinceTypeDistributionConflict(source, targetType, 'type', 'Cannot migrate from an unsupported source province type');
1698
+ if (typeChanged &&
1699
+ targetType === 'sea' &&
1700
+ (updated.terrain !== 'ocean' || updated.continent !== 0))
1701
+ provinceTypeDistributionConflict(source, targetType, 'definition', 'Sea migration requires ocean terrain and continent zero', { terrain: updated.terrain, continent: updated.continent });
1702
+ if (typeChanged &&
1703
+ targetType === 'lake' &&
1704
+ (updated.terrain !== 'lakes' || updated.continent !== 0))
1705
+ provinceTypeDistributionConflict(source, targetType, 'definition', 'Lake migration requires lakes terrain and continent zero', { terrain: updated.terrain, continent: updated.continent });
1706
+ if (typeChanged &&
1707
+ targetType === 'land' &&
1708
+ (updated.continent === 0 || updated.terrain === 'ocean' || updated.terrain === 'lakes'))
1709
+ provinceTypeDistributionConflict(source, targetType, 'definition', 'Land migration requires a nonzero continent and non-water terrain', { terrain: updated.terrain, continent: updated.continent });
1710
+ const statePatches = new Map();
1711
+ const statePatch = (state) => {
1712
+ const current = statePatches.get(state.id);
1713
+ if (current !== undefined)
1714
+ return current;
1715
+ const created = { state, data: cloneState(state), fields: {} };
1716
+ statePatches.set(state.id, created);
1717
+ return created;
1718
+ };
1719
+ const buildingPositionRemovals = new Set();
1720
+ const unitPositionRemovals = new Set();
1721
+ const supplyNodeRemovals = new Set();
1722
+ const railwayRemovals = new Set();
1723
+ const adjacencyRemovals = new Set();
1724
+ if (typeChanged) {
1725
+ const policy = requireProvinceTypeDistribution(operation);
1726
+ const sourceIsLand = source.type === 'land';
1727
+ const targetIsLand = targetType === 'land';
1728
+ const memberships = index.stateForProvince(source.id);
1729
+ if (memberships.length > 1)
1730
+ provinceTypeDistributionConflict(source, targetType, 'stateMembership', 'Province type migration cannot resolve multiple source-state memberships', { stateIds: memberships.map(({ id }) => id).sort((left, right) => left - right) });
1731
+ const regionId = regionMembershipId(index, source.id);
1732
+ let projectedStateId;
1733
+ if (sourceIsLand && !targetIsLand) {
1734
+ const sourceState = memberships[0];
1735
+ if (sourceState === undefined ||
1736
+ policy.stateMembership.method !== 'remove' ||
1737
+ policy.stateMembership.stateId !== sourceState.id)
1738
+ provinceTypeDistributionConflict(source, targetType, 'stateMembership', 'Land-to-water migration must remove the province from its exact indexed state', {
1739
+ indexedStateId: sourceState?.id ?? null,
1740
+ policy: policy.stateMembership,
1741
+ });
1742
+ const patch = statePatch(sourceState);
1743
+ patch.data.provinces = patch.data.provinces.filter((id) => id !== source.id);
1744
+ if (patch.data.provinces.length === 0 ||
1745
+ !patch.data.provinces.some((id) => index.definitionsById.get(id)?.type === 'land'))
1746
+ provinceTypeDistributionConflict(source, targetType, 'stateMembership', 'Type migration cannot leave the source state without a land province', { stateId: sourceState.id });
1747
+ patch.fields.provinces = true;
1748
+ }
1749
+ else if (!sourceIsLand && targetIsLand) {
1750
+ if (memberships.length !== 0 || policy.stateMembership.method !== 'assign')
1751
+ provinceTypeDistributionConflict(source, targetType, 'stateMembership', 'Water-to-land migration requires one explicit target state and no existing state membership', {
1752
+ indexedStateIds: memberships.map(({ id }) => id).sort((left, right) => left - right),
1753
+ policy: policy.stateMembership,
1754
+ });
1755
+ const targetState = index.statesById.get(policy.stateMembership.stateId);
1756
+ if (targetState === undefined)
1757
+ provinceTypeDistributionConflict(source, targetType, 'stateMembership', 'The explicit target state does not exist', { stateId: policy.stateMembership.stateId });
1758
+ const targetRegionIds = new Set(targetState.provinces.map((id) => regionMembershipId(index, id)));
1759
+ if (targetRegionIds.size > 1 || [...targetRegionIds].some((id) => id !== regionId))
1760
+ provinceTypeDistributionConflict(source, targetType, 'strategicRegion', 'The assigned state must remain inside the province strategic region', {
1761
+ stateId: targetState.id,
1762
+ provinceRegionId: regionId,
1763
+ stateRegionIds: [...targetRegionIds].sort((left, right) => left - right),
1764
+ });
1765
+ const patch = statePatch(targetState);
1766
+ patch.data.provinces = uniqueSorted([...patch.data.provinces, source.id]);
1767
+ patch.fields.provinces = true;
1768
+ projectedStateId = targetState.id;
1769
+ }
1770
+ else {
1771
+ if (memberships.length !== 0 || policy.stateMembership.method !== 'retain')
1772
+ provinceTypeDistributionConflict(source, targetType, 'stateMembership', 'Water-to-water migration must explicitly retain an empty state membership', {
1773
+ indexedStateIds: memberships.map(({ id }) => id).sort((left, right) => left - right),
1774
+ policy: policy.stateMembership,
1775
+ });
1776
+ }
1777
+ const victoryPoints = index.victoryPointsByProvince.get(source.id) ?? [];
1778
+ if (policy.victoryPoints === 'retain-if-valid' &&
1779
+ victoryPoints.some(({ stateId }) => stateId !== projectedStateId))
1780
+ provinceTypeDistributionConflict(source, targetType, 'victoryPoints', 'Retained victory points would not belong to the projected land state', { stateIds: victoryPoints.map(({ stateId }) => stateId) });
1781
+ if (policy.victoryPoints === 'remove') {
1782
+ for (const { stateId } of victoryPoints) {
1783
+ const state = index.statesById.get(stateId);
1784
+ if (state === undefined)
1785
+ continue;
1786
+ const patch = statePatch(state);
1787
+ patch.data.victoryPoints = patch.data.victoryPoints.filter(({ provinceId }) => provinceId !== source.id);
1788
+ patch.fields.victoryPoints = true;
1789
+ }
1790
+ }
1791
+ const provinceBuildingEntries = index.provinceBuildingsByProvince.get(source.id) ?? [];
1792
+ const nonPortBuildingEntries = provinceBuildingEntries.filter(({ buildings }) => [...buildings.keys()].some((building) => building !== 'naval_base'));
1793
+ if (policy.provinceBuildings === 'retain-if-valid' &&
1794
+ nonPortBuildingEntries.some(({ stateId }) => stateId !== projectedStateId))
1795
+ provinceTypeDistributionConflict(source, targetType, 'provinceBuildings', 'Retained province buildings would not belong to the projected land state', { stateIds: nonPortBuildingEntries.map(({ stateId }) => stateId) });
1796
+ if (policy.provinceBuildings === 'remove') {
1797
+ for (const { stateId } of nonPortBuildingEntries) {
1798
+ const state = index.statesById.get(stateId);
1799
+ if (state === undefined)
1800
+ continue;
1801
+ const patch = statePatch(state);
1802
+ const buildings = new Map(patch.data.provinceBuildings.get(source.id) ?? []);
1803
+ for (const building of [...buildings.keys()])
1804
+ if (building !== 'naval_base')
1805
+ buildings.delete(building);
1806
+ if (buildings.size === 0)
1807
+ patch.data.provinceBuildings.delete(source.id);
1808
+ else
1809
+ patch.data.provinceBuildings.set(source.id, buildings);
1810
+ patch.fields.buildings = true;
1811
+ }
1812
+ }
1813
+ const isPortPosition = ({ building }) => building === 'naval_base_spawn' || building === 'floating_harbor';
1814
+ const dependentPortPositions = index.buildingPositions.filter((position) => {
1815
+ if (!isPortPosition(position))
1816
+ return false;
1817
+ return (index.provinceAtMapCoordinate(position.x, position.z) === source.id ||
1818
+ position.adjacentSeaProvince === source.id);
1819
+ });
1820
+ const dependentPortBuildings = new Map();
1821
+ for (const { stateId, buildings } of provinceBuildingEntries) {
1822
+ if ((buildings.get('naval_base') ?? 0) > 0)
1823
+ dependentPortBuildings.set(`${stateId}:${source.id}`, {
1824
+ stateId,
1825
+ provinceId: source.id,
1826
+ });
1827
+ }
1828
+ for (const position of dependentPortPositions) {
1829
+ const provinceId = index.provinceAtMapCoordinate(position.x, position.z);
1830
+ const buildings = provinceId === undefined
1831
+ ? undefined
1832
+ : index.statesById.get(position.stateId)?.provinceBuildings.get(provinceId);
1833
+ if (provinceId !== undefined && (buildings?.get('naval_base') ?? 0) > 0)
1834
+ dependentPortBuildings.set(`${position.stateId}:${provinceId}`, {
1835
+ stateId: position.stateId,
1836
+ provinceId,
1837
+ });
1838
+ }
1839
+ if (policy.ports === 'retain-if-valid') {
1840
+ for (const position of dependentPortPositions) {
1841
+ const locatedProvince = index.provinceAtMapCoordinate(position.x, position.z);
1842
+ if ((locatedProvince === source.id &&
1843
+ (targetType !== 'land' ||
1844
+ position.stateId !== projectedStateId ||
1845
+ !updated.coastal ||
1846
+ index.definitionsById.get(position.adjacentSeaProvince)?.type !== 'sea')) ||
1847
+ (position.adjacentSeaProvince === source.id && targetType !== 'sea'))
1848
+ provinceTypeDistributionConflict(source, targetType, 'ports', 'A retained port placement would no longer identify compatible land and sea provinces', {
1849
+ stateId: position.stateId,
1850
+ locatedProvince: locatedProvince ?? null,
1851
+ adjacentSeaProvince: position.adjacentSeaProvince,
1852
+ });
1853
+ }
1854
+ for (const { stateId, provinceId } of dependentPortBuildings.values()) {
1855
+ if (provinceId === source.id
1856
+ ? targetType !== 'land' || stateId !== projectedStateId || !updated.coastal
1857
+ : targetType !== 'sea')
1858
+ provinceTypeDistributionConflict(source, targetType, 'ports', 'A retained naval base would not have compatible projected province types', { stateId, portProvinceId: provinceId });
1859
+ }
1860
+ }
1861
+ else {
1862
+ for (const position of dependentPortPositions)
1863
+ buildingPositionRemovals.add(position);
1864
+ for (const { stateId, provinceId } of dependentPortBuildings.values()) {
1865
+ const state = index.statesById.get(stateId);
1866
+ if (state === undefined)
1867
+ continue;
1868
+ const patch = statePatch(state);
1869
+ const buildings = new Map(patch.data.provinceBuildings.get(provinceId) ?? []);
1870
+ buildings.delete('naval_base');
1871
+ if (buildings.size === 0)
1872
+ patch.data.provinceBuildings.delete(provinceId);
1873
+ else
1874
+ patch.data.provinceBuildings.set(provinceId, buildings);
1875
+ patch.fields.buildings = true;
1876
+ }
1877
+ }
1878
+ const nonPortPositions = index.buildingPositions.filter((position) => {
1879
+ if (isPortPosition(position))
1880
+ return false;
1881
+ return (index.provinceAtMapCoordinate(position.x, position.z) === source.id ||
1882
+ position.adjacentSeaProvince === source.id);
1883
+ });
1884
+ if (policy.buildingPositions === 'retain-if-valid') {
1885
+ for (const position of nonPortPositions) {
1886
+ const locatedProvince = index.provinceAtMapCoordinate(position.x, position.z);
1887
+ if ((locatedProvince === source.id &&
1888
+ (targetType !== 'land' || position.stateId !== projectedStateId)) ||
1889
+ (position.adjacentSeaProvince === source.id && targetType !== 'sea'))
1890
+ provinceTypeDistributionConflict(source, targetType, 'buildingPositions', 'A retained building position would no longer resolve in a compatible state/province', { stateId: position.stateId, locatedProvince: locatedProvince ?? null });
1891
+ }
1892
+ }
1893
+ else {
1894
+ for (const position of nonPortPositions)
1895
+ buildingPositionRemovals.add(position);
1896
+ }
1897
+ const unitPositions = index.unitPositions.filter(({ provinceId }) => provinceId === source.id);
1898
+ if (policy.unitPositions === 'remove')
1899
+ for (const position of unitPositions)
1900
+ unitPositionRemovals.add(position);
1901
+ const supplyNodes = index.supplyNodes.filter(({ provinceId }) => provinceId === source.id);
1902
+ if (policy.supplyNodes === 'retain-if-valid' && targetType !== 'land' && supplyNodes.length > 0)
1903
+ provinceTypeDistributionConflict(source, targetType, 'supplyNodes', 'Supply nodes can only be retained in a stateful land province', { count: supplyNodes.length });
1904
+ if (policy.supplyNodes === 'remove')
1905
+ for (const node of supplyNodes)
1906
+ supplyNodeRemovals.add(node);
1907
+ const railways = index.railways.filter(({ provinces }) => provinces.includes(source.id));
1908
+ if (policy.railways === 'retain-if-valid' && targetType !== 'land' && railways.length > 0)
1909
+ provinceTypeDistributionConflict(source, targetType, 'railways', 'Railway routes can only be retained through a stateful land province', { indexes: railways.map((railway) => index.railways.indexOf(railway)) });
1910
+ if (policy.railways === 'remove-containing')
1911
+ for (const railway of railways)
1912
+ railwayRemovals.add(railway);
1913
+ const adjacencies = index.adjacencies.filter(({ from, to, through }) => from === source.id || to === source.id || through === source.id);
1914
+ if (policy.adjacencies === 'retain-if-valid') {
1915
+ for (const adjacency of adjacencies) {
1916
+ const otherId = adjacency.from === source.id ? adjacency.to : adjacency.from;
1917
+ const otherType = index.definitionsById.get(otherId)?.type;
1918
+ const incompatible = adjacency.type === 'sea'
1919
+ ? ((adjacency.from === source.id || adjacency.to === source.id) &&
1920
+ otherType !== targetType) ||
1921
+ (adjacency.through === source.id && targetType !== 'sea')
1922
+ : adjacency.type === 'impassable' &&
1923
+ (adjacency.from === source.id || adjacency.to === source.id) &&
1924
+ targetType !== 'land';
1925
+ if (incompatible)
1926
+ provinceTypeDistributionConflict(source, targetType, 'adjacencies', 'A retained special adjacency would be incompatible with the target province type', {
1927
+ from: adjacency.from,
1928
+ to: adjacency.to,
1929
+ through: adjacency.through,
1930
+ type: adjacency.type,
1931
+ });
1932
+ }
1933
+ }
1934
+ else {
1935
+ for (const adjacency of adjacencies)
1936
+ adjacencyRemovals.add(adjacency);
1937
+ }
1938
+ }
1939
+ const colorChanged = rgbKey(updated.color) !== rgbKey(source.color);
1940
+ if (colorChanged) {
1941
+ const crossRootCollision = index.definitionsAcrossRoots.find(({ id, color }) => id !== source.id && rgbKey(color) === rgbKey(updated.color));
1942
+ if (rgbKey(updated.color) === '0,0,0' || crossRootCollision !== undefined) {
1943
+ throw new ServiceError(crossRootCollision !== undefined && !index.definitionsByColor.has(rgbKey(updated.color))
1944
+ ? 'MAP_DEPENDENCY_PROVINCE_COLOR_CONFLICT'
1945
+ : 'MAP_PROVINCE_COLOR_DUPLICATE', `Province color ${rgbKey(updated.color)} is invalid or already used`, {
1946
+ ...(crossRootCollision === undefined
1947
+ ? {}
1948
+ : { source: crossRootCollision.document.file.displayPath }),
1949
+ });
1950
+ }
1951
+ }
1952
+ for (const { state, data, fields } of [...statePatches.values()].sort((left, right) => left.state.id - right.state.id))
1953
+ commitState(changes, state, data, fields, operation.id);
1954
+ const removeRows = (records) => {
1955
+ const groups = new Map();
1956
+ for (const record of records) {
1957
+ const key = record.document.file.relativePath;
1958
+ const group = groups.get(key) ?? { document: record.document, lines: new Set() };
1959
+ group.lines.add(record.line);
1960
+ groups.set(key, group);
1961
+ }
1962
+ for (const [relativePath, { document, lines }] of groups)
1963
+ addChange(changes, relativePath, deleteLines(document, [...lines].sort((left, right) => left - right)), operation.id, 'text/plain');
1964
+ };
1965
+ removeRows(buildingPositionRemovals);
1966
+ removeRows(unitPositionRemovals);
1967
+ removeRows(supplyNodeRemovals);
1968
+ removeRows(railwayRemovals);
1969
+ removeRows(adjacencyRemovals);
1970
+ addChange(changes, required(index.definitionFile, 'MAP_DEFINITION_FILE_MISSING', 'Definition file is missing')
1971
+ .relativePath, patchDefinitions(index, new Map([[source.id, updated]]), new Set(), []), operation.id, 'text/plain');
1972
+ if (!colorChanged)
1973
+ return undefined;
1974
+ const bitmap = index.provinceBitmap;
1975
+ const raster = index.raster;
1976
+ if (bitmap === undefined || raster === undefined || bitmap.bitsPerPixel !== 24)
1977
+ throw new ServiceError('MAP_PROVINCE_BITMAP_MISSING', 'Color change requires a 24-bit province bitmap');
1978
+ const pixels = yield* provincePixelOffsets(raster, new Set([source.id]), 'Province color update', signal);
1979
+ const next = bitmap.withRgbOffsets(pixels, updated.color);
1980
+ addChange(changes, required(index.provinceBitmapFile, 'MAP_PROVINCE_BITMAP_MISSING', 'Province bitmap file is missing').relativePath, next.encode(), operation.id, 'image/bmp');
1981
+ return bitmap.diffBounds(next);
1982
+ }
1983
+ function remapStateProvinceData(data, provinceIds) {
1984
+ const fields = {};
1985
+ if (data.provinces.some((id) => remapProvinceId(provinceIds, id) !== id)) {
1986
+ data.provinces = uniqueSorted(data.provinces.map((id) => remapProvinceId(provinceIds, id)));
1987
+ fields.provinces = true;
1988
+ }
1989
+ if (data.victoryPoints.some(({ provinceId }) => remapProvinceId(provinceIds, provinceId) !== provinceId)) {
1990
+ const vpTotals = new Map();
1991
+ for (const point of data.victoryPoints) {
1992
+ const provinceId = remapProvinceId(provinceIds, point.provinceId);
1993
+ vpTotals.set(provinceId, (vpTotals.get(provinceId) ?? 0) + point.value);
1994
+ }
1995
+ data.victoryPoints = [...vpTotals].map(([provinceId, value]) => ({ provinceId, value }));
1996
+ fields.victoryPoints = true;
1997
+ }
1998
+ if ([...data.provinceBuildings.keys()].some((provinceId) => remapProvinceId(provinceIds, provinceId) !== provinceId)) {
1999
+ const buildingTotals = new Map();
2000
+ for (const [provinceId, buildings] of data.provinceBuildings) {
2001
+ const remappedId = remapProvinceId(provinceIds, provinceId);
2002
+ const totals = buildingTotals.get(remappedId) ?? new Map();
2003
+ addMaps(totals, buildings);
2004
+ buildingTotals.set(remappedId, totals);
2005
+ }
2006
+ data.provinceBuildings = buildingTotals;
2007
+ fields.buildings = true;
2008
+ }
2009
+ return fields;
2010
+ }
2011
+ function remapProvinceId(provinceIds, provinceId) {
2012
+ return provinceIds.get(provinceId) ?? provinceId;
2013
+ }
2014
+ function compactedProvinceIds(index, sources, target) {
2015
+ const ids = uniqueSorted(index.definitions.map(({ id }) => id));
2016
+ const duplicateIds = [...index.duplicateDefinitionIds]
2017
+ .filter(([, definitions]) => definitions.length > 1)
2018
+ .map(([id]) => id);
2019
+ const duplicateColors = [...index.duplicateColors]
2020
+ .filter(([, definitions]) => definitions.length > 1)
2021
+ .map(([color]) => color);
2022
+ if (duplicateIds.length > 0 || duplicateColors.length > 0)
2023
+ throw new ServiceError('MAP_PROVINCE_ID_RENUMBER_AMBIGUOUS', 'Province IDs cannot be compacted while the active definition table has duplicate IDs or colors', { duplicateIds, duplicateColors });
2024
+ if (!contiguous(ids, 0))
2025
+ throw new ServiceError('MAP_PROVINCE_ID_GAP', 'Province IDs must be contiguous from zero before a merge can compact them safely', { ids });
2026
+ const result = new Map();
2027
+ let nextId = 0;
2028
+ for (const id of ids) {
2029
+ if (sources.has(id))
2030
+ continue;
2031
+ result.set(id, nextId);
2032
+ nextId += 1;
2033
+ }
2034
+ const remappedTarget = result.get(target);
2035
+ if (remappedTarget === undefined)
2036
+ throw new ServiceError('MAP_PROVINCE_ID_RENUMBER_AMBIGUOUS', 'Province merge target did not survive ID compaction', { target });
2037
+ for (const source of sources)
2038
+ result.set(source, remappedTarget);
2039
+ return result;
2040
+ }
2041
+ function* applyMergeProvinces(base, changes, operation, signal) {
2042
+ if (!samePolicy(operation.distribution, {
2043
+ membership: 'require-same',
2044
+ victoryPoints: 'sum-into-target',
2045
+ provinceBuildings: 'sum-into-target',
2046
+ references: 'remap-to-target-and-deduplicate',
2047
+ })) {
2048
+ throw new ServiceError('MAP_PROVINCE_DISTRIBUTION_REQUIRED', 'Province merge requires complete explicit membership, data, and reference policies');
2049
+ }
2050
+ const index = currentIndex(base, changes);
2051
+ const sourceIds = operation.kind === 'remove_province'
2052
+ ? [operation.provinceId]
2053
+ : uniqueSorted(operation.sourceProvinceIds);
2054
+ const targetId = operation.kind === 'remove_province'
2055
+ ? operation.mergeIntoProvinceId
2056
+ : operation.targetProvinceId;
2057
+ const sources = sourceIds.filter((id) => id !== targetId);
2058
+ const target = index.definitionsById.get(targetId);
2059
+ if (target === undefined ||
2060
+ sources.length === 0 ||
2061
+ sources.some((id) => !index.definitionsById.has(id))) {
2062
+ throw new ServiceError('MAP_PROVINCE_SELECTION_INVALID', 'Merge target and source provinces must exist and differ');
2063
+ }
2064
+ const sourceSet = new Set(sources);
2065
+ const provinceIds = compactedProvinceIds(index, sourceSet, targetId);
2066
+ const definitions = [
2067
+ target,
2068
+ ...sources.map((id) => required(index.definitionsById.get(id), 'MAP_PROVINCE_NOT_FOUND', `Province ${id} does not exist`)),
2069
+ ];
2070
+ if (definitions.some(({ type }) => type !== target.type))
2071
+ throw new ServiceError('MAP_PROVINCE_TYPE_MISMATCH', 'Merged provinces must have the same type');
2072
+ const stateIds = new Set(definitions.map(({ id }) => stateMembershipId(index, id) ?? -1));
2073
+ const regionIds = new Set(definitions.map(({ id }) => regionMembershipId(index, id)));
2074
+ if (stateIds.size !== 1 || regionIds.size !== 1)
2075
+ throw new ServiceError('MAP_PROVINCE_MEMBERSHIP_MISMATCH', 'Merged provinces must share state and strategic-region membership');
2076
+ const bitmap = index.provinceBitmap;
2077
+ const raster = index.raster;
2078
+ if (bitmap === undefined || raster === undefined || bitmap.bitsPerPixel !== 24)
2079
+ throw new ServiceError('MAP_PROVINCE_BITMAP_MISSING', 'Province merge requires a 24-bit province bitmap');
2080
+ const pixelOffsets = yield* provincePixelOffsets(raster, sourceSet, 'Province merge recolor', signal);
2081
+ const nextBitmap = bitmap.withRgbOffsets(pixelOffsets, target.color);
2082
+ addChange(changes, required(index.provinceBitmapFile, 'MAP_PROVINCE_BITMAP_MISSING', 'Province bitmap file is missing').relativePath, nextBitmap.encode(), operation.id, 'image/bmp');
2083
+ const mergedCoastal = definitions.some(({ coastal }) => coastal);
2084
+ const definitionUpdates = new Map();
2085
+ for (const definition of index.definitions) {
2086
+ if (sourceSet.has(definition.id))
2087
+ continue;
2088
+ const remappedId = required(provinceIds.get(definition.id), 'MAP_PROVINCE_ID_RENUMBER_AMBIGUOUS', `Province ${definition.id} has no compacted ID`);
2089
+ const coastal = definition.id === targetId ? mergedCoastal : definition.coastal;
2090
+ if (remappedId !== definition.id || coastal !== definition.coastal)
2091
+ definitionUpdates.set(definition.id, { ...definition, id: remappedId, coastal });
2092
+ }
2093
+ addChange(changes, required(index.definitionFile, 'MAP_DEFINITION_FILE_MISSING', 'Definition file is missing')
2094
+ .relativePath, patchDefinitions(index, definitionUpdates, sourceSet, []), operation.id, 'text/plain');
2095
+ for (const state of index.states) {
2096
+ const data = cloneState(state);
2097
+ const fields = remapStateProvinceData(data, provinceIds);
2098
+ if (Object.values(fields).some((changed) => changed === true))
2099
+ commitState(changes, state, data, fields, operation.id);
2100
+ }
2101
+ for (const region of index.regions) {
2102
+ if (!region.provinces.some((id) => remapProvinceId(provinceIds, id) !== id))
2103
+ continue;
2104
+ commitRegion(changes, region, uniqueSorted(region.provinces.map((id) => remapProvinceId(provinceIds, id))), operation.id);
2105
+ }
2106
+ remapReferenceFiles(index, changes, provinceIds, operation.id);
2107
+ return required(bitmap.diffBounds(nextBitmap), 'MAP_PROVINCE_MERGE_NO_CHANGE', 'Province merge did not change any bitmap pixel');
2108
+ }
2109
+ function applyMoveRegions(base, changes, operation) {
2110
+ if (operation.distribution !== 'move-membership')
2111
+ throw new ServiceError('MAP_REGION_DISTRIBUTION_REQUIRED', 'Region move requires explicit move-membership policy');
2112
+ const index = currentIndex(base, changes);
2113
+ const source = index.regionsById.get(operation.sourceRegionId);
2114
+ const target = index.regionsById.get(operation.targetRegionId);
2115
+ const ids = uniqueSorted(operation.provinceIds);
2116
+ if (source === undefined ||
2117
+ target === undefined ||
2118
+ source.id === target.id ||
2119
+ ids.length === 0 ||
2120
+ ids.some((id) => !source.provinces.includes(id))) {
2121
+ throw new ServiceError('MAP_REGION_SELECTION_INVALID', 'Region source, target, and provinces are invalid');
2122
+ }
2123
+ const selected = new Set(ids);
2124
+ commitRegion(changes, source, source.provinces.filter((id) => !selected.has(id)), operation.id);
2125
+ commitRegion(changes, target, uniqueSorted([...target.provinces, ...ids]), operation.id);
2126
+ }
2127
+ function hasNormalAdjacency(index, from, to) {
2128
+ return index.raster?.adjacency.get(from)?.has(to) ?? false;
2129
+ }
2130
+ function* applyNormalAdjacency(base, changes, operation, signal) {
2131
+ const index = currentIndex(base, changes);
2132
+ const bitmap = index.provinceBitmap;
2133
+ const raster = index.raster;
2134
+ if (bitmap?.bitsPerPixel !== 24 || raster === undefined) {
2135
+ throw new ServiceError('MAP_PROVINCE_BITMAP_MISSING', 'Normal adjacency edits require an active 24-bit provinces bitmap');
2136
+ }
2137
+ if (operation.from === operation.to ||
2138
+ !index.definitionsById.has(operation.from) ||
2139
+ !index.definitionsById.has(operation.to)) {
2140
+ throw new ServiceError('MAP_NORMAL_ADJACENCY_PROVINCE_INVALID', 'Normal adjacency endpoints must be different existing provinces', { from: operation.from, to: operation.to });
2141
+ }
2142
+ const existsBefore = hasNormalAdjacency(index, operation.from, operation.to);
2143
+ if (operation.kind === 'add_normal_adjacency' && existsBefore) {
2144
+ throw new ServiceError('MAP_NORMAL_ADJACENCY_ALREADY_EXISTS', 'Requested normal adjacency already exists in the active province raster', { from: operation.from, to: operation.to });
2145
+ }
2146
+ if (operation.kind === 'remove_normal_adjacency' && !existsBefore) {
2147
+ throw new ServiceError('MAP_NORMAL_ADJACENCY_NOT_FOUND', 'Requested normal adjacency does not exist in the active province raster', { from: operation.from, to: operation.to });
2148
+ }
2149
+ if (!Array.isArray(operation.pixelTransfers) || operation.pixelTransfers.length === 0) {
2150
+ throw new ServiceError('MAP_NORMAL_ADJACENCY_GEOMETRY_REQUIRED', 'Normal adjacency edits require one or more exact pixel transfers');
2151
+ }
2152
+ assertSelectedPixelBudget(operation.pixelTransfers.length, 'Normal adjacency pixel transfers');
2153
+ const pair = new Set([operation.from, operation.to]);
2154
+ const seen = new Set();
2155
+ const pixelOffsets = new Uint32Array(operation.pixelTransfers.length);
2156
+ const packedColors = new Uint32Array(operation.pixelTransfers.length);
2157
+ for (const [transferIndex, transfer] of operation.pixelTransfers.entries()) {
2158
+ yield* cancellationCheckpoint(signal, transferIndex, 4096);
2159
+ if (!Number.isInteger(transfer.x) ||
2160
+ !Number.isInteger(transfer.y) ||
2161
+ transfer.x < 0 ||
2162
+ transfer.y < 0 ||
2163
+ transfer.x >= raster.width ||
2164
+ transfer.y >= raster.height) {
2165
+ throw new ServiceError('MAP_GEOMETRY_OUT_OF_BOUNDS', 'Normal adjacency pixel transfer is outside the active province raster', { x: transfer.x, y: transfer.y });
2166
+ }
2167
+ const pixelOffset = transfer.y * raster.width + transfer.x;
2168
+ if (seen.has(pixelOffset)) {
2169
+ throw new ServiceError('MAP_NORMAL_ADJACENCY_TRANSFER_DUPLICATE', 'Normal adjacency pixel transfers must have unique coordinates', { x: transfer.x, y: transfer.y });
2170
+ }
2171
+ seen.add(pixelOffset);
2172
+ if (transfer.sourceProvinceId === transfer.targetProvinceId ||
2173
+ !index.definitionsById.has(transfer.sourceProvinceId) ||
2174
+ !index.definitionsById.has(transfer.targetProvinceId)) {
2175
+ throw new ServiceError('MAP_NORMAL_ADJACENCY_TRANSFER_PROVINCE_INVALID', 'Normal adjacency transfer source and target must be different existing provinces', { ...transfer });
2176
+ }
2177
+ if (!pair.has(transfer.sourceProvinceId) && !pair.has(transfer.targetProvinceId)) {
2178
+ throw new ServiceError('MAP_NORMAL_ADJACENCY_TRANSFER_UNRELATED', 'Every normal adjacency transfer must involve at least one requested endpoint', { ...transfer, from: operation.from, to: operation.to });
2179
+ }
2180
+ const observed = raster.provinceIds[pixelOffset];
2181
+ if (observed !== transfer.sourceProvinceId) {
2182
+ throw new ServiceError('MAP_NORMAL_ADJACENCY_TRANSFER_SOURCE_MISMATCH', 'Normal adjacency transfer source does not match the active province raster', { ...transfer, observedProvinceId: observed });
2183
+ }
2184
+ const target = required(index.definitionsById.get(transfer.targetProvinceId), 'MAP_NORMAL_ADJACENCY_TRANSFER_PROVINCE_INVALID', 'Normal adjacency transfer target province does not exist');
2185
+ pixelOffsets[transferIndex] = pixelOffset;
2186
+ packedColors[transferIndex] = packedRgb(target.color);
2187
+ }
2188
+ const nextBitmap = bitmap.withPackedRgbOffsetChanges(pixelOffsets, packedColors);
2189
+ const bounds = required(bitmap.diffBounds(nextBitmap), 'MAP_NORMAL_ADJACENCY_GEOMETRY_REQUIRED', 'Normal adjacency pixel transfers do not change the province raster');
2190
+ const bitmapFile = required(index.provinceBitmapFile, 'MAP_PROVINCE_BITMAP_MISSING', 'Province bitmap file is missing');
2191
+ addChange(changes, bitmapFile.relativePath, nextBitmap.encode(), operation.id, 'image/bmp');
2192
+ const proposed = currentIndex(base, changes);
2193
+ const existsAfter = hasNormalAdjacency(proposed, operation.from, operation.to);
2194
+ const expectedAfter = operation.kind === 'add_normal_adjacency';
2195
+ if (existsAfter !== expectedAfter) {
2196
+ throw new ServiceError('MAP_NORMAL_ADJACENCY_RESULT_MISMATCH', 'Exact pixel transfers did not produce the requested normal adjacency result', {
2197
+ from: operation.from,
2198
+ to: operation.to,
2199
+ expectedAfter,
2200
+ actualAfter: existsAfter,
2201
+ changedPixels: bounds.count,
2202
+ });
2203
+ }
2204
+ return bounds;
2205
+ }
2206
+ function formatAdjacency(adjacency) {
2207
+ return [
2208
+ adjacency.from,
2209
+ adjacency.to,
2210
+ adjacency.type,
2211
+ adjacency.through,
2212
+ numberText(adjacency.startX),
2213
+ numberText(adjacency.startY),
2214
+ numberText(adjacency.stopX),
2215
+ numberText(adjacency.stopY),
2216
+ adjacency.rule,
2217
+ adjacency.comment,
2218
+ ].join(';');
2219
+ }
2220
+ function findActiveFile(index, relativePath) {
2221
+ const normalized = normalizeRelative(relativePath);
2222
+ let file = index.activeFiles.byRelativePath.get(normalized);
2223
+ if (file === undefined && normalized.startsWith('map/')) {
2224
+ const filename = normalized.slice('map/'.length);
2225
+ for (const root of index.sourceRoots.map) {
2226
+ file = index.activeFiles.byRelativePath.get(normalizeRelative(`${root}/${filename}`));
2227
+ if (file !== undefined)
2228
+ break;
2229
+ }
2230
+ }
2231
+ if (file === undefined)
2232
+ throw new ServiceError('MAP_FILE_MISSING', `Required map file is missing: ${relativePath}`);
2233
+ return file;
2234
+ }
2235
+ function appendBeforeSentinel(document, text) {
2236
+ const sentinel = document.lines.find((line) => line.text.trim().startsWith('-1;'));
2237
+ const offset = sentinel?.start ?? document.text.length;
2238
+ const prefix = offset === 0 || /(?:\r\n|\n|\r)$/u.test(document.text.slice(0, offset)) ? '' : document.newline;
2239
+ return applyTextRanges(document, [
2240
+ {
2241
+ start: offset,
2242
+ end: offset,
2243
+ text: `${prefix}${text}${document.newline}`,
2244
+ description: 'Append map row',
2245
+ },
2246
+ ]);
2247
+ }
2248
+ function deleteLines(document, lineNumbers) {
2249
+ const wanted = new Set(lineNumbers);
2250
+ return applyTextRanges(document, document.lines
2251
+ .filter((line) => wanted.has(line.index + 1))
2252
+ .map((line) => ({
2253
+ start: line.start,
2254
+ end: line.fullEnd,
2255
+ text: '',
2256
+ description: 'Remove map row',
2257
+ })));
2258
+ }
2259
+ function replaceLine(document, lineNumber, text) {
2260
+ const line = document.lines[lineNumber - 1];
2261
+ if (line === undefined)
2262
+ throw new ServiceError('MAP_POSITION_ROW_MISSING', `Map row ${lineNumber} does not exist`);
2263
+ return applyTextRanges(document, [
2264
+ { start: line.start, end: line.end, text, description: 'Update map row' },
2265
+ ]);
2266
+ }
2267
+ function appendLine(document, text) {
2268
+ const prefix = document.text.length === 0 || /(?:\r\n|\n|\r)$/u.test(document.text) ? '' : document.newline;
2269
+ return applyTextRanges(document, [
2270
+ {
2271
+ start: document.text.length,
2272
+ end: document.text.length,
2273
+ text: `${prefix}${text}${document.newline}`,
2274
+ description: 'Append map row',
2275
+ },
2276
+ ]);
2277
+ }
2278
+ function occurrence(values, predicate, selected = 0) {
2279
+ return values.filter(predicate)[selected];
2280
+ }
2281
+ function handleSimpleOperation(base, changes, operation) {
2282
+ const index = currentIndex(base, changes);
2283
+ if (operation.kind === 'add_adjacency') {
2284
+ const file = required(index.adjacencyFile, 'MAP_ADJACENCY_FILE_MISSING', 'The default.map adjacency file is missing');
2285
+ const document = parseTextDocument(file);
2286
+ addChange(changes, file.relativePath, appendBeforeSentinel(document, formatAdjacency(operation.adjacency)), operation.id, 'text/plain');
2287
+ }
2288
+ else if (operation.kind === 'remove_adjacency') {
2289
+ const matching = index.adjacencies.filter(({ from, to, type }) => ((from === operation.from && to === operation.to) ||
2290
+ (from === operation.to && to === operation.from)) &&
2291
+ (operation.type === undefined || type === operation.type));
2292
+ if (matching.length === 0)
2293
+ throw new ServiceError('MAP_ADJACENCY_NOT_FOUND', 'No matching adjacency exists');
2294
+ const document = required(matching[0], 'MAP_ADJACENCY_NOT_FOUND', 'No matching adjacency exists').document;
2295
+ addChange(changes, document.file.relativePath, deleteLines(document, matching.map(({ line }) => line)), operation.id, 'text/plain');
2296
+ }
2297
+ else if (operation.kind === 'add_supply_node') {
2298
+ const file = required(index.supplyNodeFile, 'MAP_SUPPLY_NODE_FILE_MISSING', 'The default.map supply-node file is missing');
2299
+ const document = parseTextDocument(file);
2300
+ addChange(changes, file.relativePath, appendLine(document, `${operation.level} ${operation.provinceId}`), operation.id, 'text/plain');
2301
+ }
2302
+ else if (operation.kind === 'remove_supply_node') {
2303
+ const matching = index.supplyNodes.filter(({ provinceId }) => provinceId === operation.provinceId);
2304
+ if (matching.length === 0)
2305
+ throw new ServiceError('MAP_SUPPLY_NODE_NOT_FOUND', 'No matching supply node exists');
2306
+ const document = required(matching[0], 'MAP_SUPPLY_NODE_NOT_FOUND', 'No matching supply node exists').document;
2307
+ addChange(changes, document.file.relativePath, deleteLines(document, matching.map(({ line }) => line)), operation.id, 'text/plain');
2308
+ }
2309
+ else if (operation.kind === 'add_railway') {
2310
+ const file = required(index.railwayFile, 'MAP_RAILWAY_FILE_MISSING', 'The default.map railway file is missing');
2311
+ const document = parseTextDocument(file);
2312
+ const provinces = uniqueConsecutive(operation.provinces);
2313
+ addChange(changes, file.relativePath, appendLine(document, `${operation.level} ${provinces.length} ${provinces.join(' ')}`), operation.id, 'text/plain');
2314
+ }
2315
+ else if (operation.kind === 'remove_railway') {
2316
+ const railway = index.railways[operation.index];
2317
+ if (railway === undefined)
2318
+ throw new ServiceError('MAP_RAILWAY_NOT_FOUND', `Railway index ${operation.index} does not exist`);
2319
+ addChange(changes, railway.document.file.relativePath, deleteLines(railway.document, [railway.line]), operation.id, 'text/plain');
2320
+ }
2321
+ else if (operation.kind === 'upsert_building_position' ||
2322
+ operation.kind === 'remove_building_position') {
2323
+ const record = occurrence(index.buildingPositions, ({ stateId, building }) => stateId === operation.match.stateId && building === operation.match.building, operation.match.occurrence);
2324
+ const file = record?.document.file ?? findActiveFile(index, 'map/buildings.txt');
2325
+ const document = record?.document ?? parseTextDocument(file);
2326
+ const content = operation.kind === 'remove_building_position'
2327
+ ? record === undefined
2328
+ ? (() => {
2329
+ throw new ServiceError('MAP_BUILDING_POSITION_NOT_FOUND', 'Building position does not exist');
2330
+ })()
2331
+ : deleteLines(document, [record.line])
2332
+ : record === undefined
2333
+ ? appendLine(document, buildingPositionLine(operation.value))
2334
+ : replaceLine(document, record.line, buildingPositionLine(operation.value));
2335
+ addChange(changes, file.relativePath, content, operation.id, 'text/plain');
2336
+ }
2337
+ else if (operation.kind === 'upsert_unit_position' ||
2338
+ operation.kind === 'remove_unit_position') {
2339
+ const record = occurrence(index.unitPositions, ({ provinceId, type }) => provinceId === operation.match.provinceId && type === operation.match.type, operation.match.occurrence);
2340
+ const file = record?.document.file ?? findActiveFile(index, 'map/unitstacks.txt');
2341
+ const document = record?.document ?? parseTextDocument(file);
2342
+ const content = operation.kind === 'remove_unit_position'
2343
+ ? record === undefined
2344
+ ? (() => {
2345
+ throw new ServiceError('MAP_UNIT_POSITION_NOT_FOUND', 'Unit position does not exist');
2346
+ })()
2347
+ : deleteLines(document, [record.line])
2348
+ : record === undefined
2349
+ ? appendLine(document, unitPositionLine(operation.value))
2350
+ : replaceLine(document, record.line, unitPositionLine(operation.value));
2351
+ addChange(changes, file.relativePath, content, operation.id, 'text/plain');
2352
+ }
2353
+ else {
2354
+ const record = occurrence(index.weatherPositions, ({ strategicRegionId, size }) => strategicRegionId === operation.match.strategicRegionId && size === operation.match.size, operation.match.occurrence);
2355
+ const file = record?.document.file ?? findActiveFile(index, 'map/weatherpositions.txt');
2356
+ const document = record?.document ?? parseTextDocument(file);
2357
+ const content = operation.kind === 'remove_weather_position'
2358
+ ? record === undefined
2359
+ ? (() => {
2360
+ throw new ServiceError('MAP_WEATHER_POSITION_NOT_FOUND', 'Weather position does not exist');
2361
+ })()
2362
+ : deleteLines(document, [record.line])
2363
+ : record === undefined
2364
+ ? appendLine(document, weatherPositionLine(operation.value))
2365
+ : replaceLine(document, record.line, weatherPositionLine(operation.value));
2366
+ addChange(changes, file.relativePath, content, operation.id, 'text/plain');
2367
+ }
2368
+ }
2369
+ function buildingPositionLine(value) {
2370
+ return [
2371
+ value.stateId,
2372
+ value.building,
2373
+ value.x,
2374
+ value.y,
2375
+ value.z,
2376
+ value.rotation,
2377
+ value.adjacentSeaProvince,
2378
+ ]
2379
+ .map((part) => (typeof part === 'number' ? numberText(part) : part))
2380
+ .join(';');
2381
+ }
2382
+ function unitPositionLine(value) {
2383
+ return [value.provinceId, value.type, value.x, value.y, value.z, value.rotation, value.offset]
2384
+ .map(numberText)
2385
+ .join(';');
2386
+ }
2387
+ function weatherPositionLine(value) {
2388
+ return [
2389
+ numberText(value.strategicRegionId),
2390
+ numberText(value.x),
2391
+ numberText(value.y),
2392
+ numberText(value.z),
2393
+ value.size,
2394
+ ].join(';');
2395
+ }
2396
+ function uniqueConsecutive(values) {
2397
+ const result = [];
2398
+ for (const value of values)
2399
+ if (result.at(-1) !== value)
2400
+ result.push(value);
2401
+ return result;
2402
+ }
2403
+ function replaceWhitespaceData(line, value) {
2404
+ const commentStart = line.indexOf('#');
2405
+ const data = commentStart < 0 ? line : line.slice(0, commentStart);
2406
+ const comment = commentStart < 0 ? '' : line.slice(commentStart);
2407
+ const leading = /^\s*/u.exec(data)?.[0] ?? '';
2408
+ const trailing = /\s*$/u.exec(data)?.[0] ?? '';
2409
+ return `${leading}${value}${trailing}${comment}`;
2410
+ }
2411
+ function remapReferenceFiles(index, changes, provinceIds, operationId) {
2412
+ if (index.adjacencies.length > 0) {
2413
+ const document = required(index.adjacencies[0], 'MAP_ADJACENCY_FILE_MISSING', 'Adjacency source row is missing').document;
2414
+ const mapped = index.adjacencies.map((adjacency) => {
2415
+ const from = remapProvinceId(provinceIds, adjacency.from);
2416
+ const to = remapProvinceId(provinceIds, adjacency.to);
2417
+ const through = remapProvinceId(provinceIds, adjacency.through);
2418
+ const value = { ...adjacency, from, to, through };
2419
+ const key = `${Math.min(from, to)}:${Math.max(from, to)}:${value.type}:${through}`;
2420
+ return {
2421
+ record: adjacency,
2422
+ value,
2423
+ key,
2424
+ affected: from !== adjacency.from || to !== adjacency.to || through !== adjacency.through,
2425
+ };
2426
+ });
2427
+ const groups = new Map();
2428
+ for (const entry of mapped) {
2429
+ const group = groups.get(entry.key) ?? [];
2430
+ group.push(entry);
2431
+ groups.set(entry.key, group);
2432
+ }
2433
+ const replacements = [];
2434
+ for (const group of groups.values()) {
2435
+ if (!group.some(({ affected }) => affected))
2436
+ continue;
2437
+ const keep = group.find(({ affected }) => !affected) ?? group[0];
2438
+ for (const entry of group) {
2439
+ const line = document.lines[entry.record.line - 1];
2440
+ if (line === undefined)
2441
+ continue;
2442
+ if (entry.value.from === entry.value.to || entry !== keep) {
2443
+ replacements.push({
2444
+ start: line.start,
2445
+ end: line.fullEnd,
2446
+ text: '',
2447
+ description: 'Remove remapped adjacency collision',
2448
+ });
2449
+ }
2450
+ else if (entry.affected) {
2451
+ const fields = new Map();
2452
+ if (entry.value.from !== entry.record.from)
2453
+ fields.set(0, String(entry.value.from));
2454
+ if (entry.value.to !== entry.record.to)
2455
+ fields.set(1, String(entry.value.to));
2456
+ if (entry.value.through !== entry.record.through)
2457
+ fields.set(3, String(entry.value.through));
2458
+ replacements.push({
2459
+ start: line.start,
2460
+ end: line.end,
2461
+ text: replaceDelimitedFields(line.text, fields),
2462
+ description: 'Remap adjacency province reference',
2463
+ });
2464
+ }
2465
+ }
2466
+ }
2467
+ if (replacements.length > 0)
2468
+ addChange(changes, document.file.relativePath, applyTextRanges(document, replacements), operationId, 'text/plain');
2469
+ }
2470
+ if (index.supplyNodes.length > 0) {
2471
+ const document = required(index.supplyNodes[0], 'MAP_SUPPLY_NODE_FILE_MISSING', 'Supply-node source row is missing').document;
2472
+ const groups = new Map();
2473
+ for (const record of index.supplyNodes) {
2474
+ const provinceId = remapProvinceId(provinceIds, record.provinceId);
2475
+ const group = groups.get(provinceId) ?? [];
2476
+ group.push({ record, provinceId, affected: provinceId !== record.provinceId });
2477
+ groups.set(provinceId, group);
2478
+ }
2479
+ const replacements = [];
2480
+ for (const group of groups.values()) {
2481
+ if (!group.some(({ affected }) => affected))
2482
+ continue;
2483
+ const keep = group.find(({ affected }) => !affected) ?? group[0];
2484
+ const level = group.reduce((maximum, { record }) => Math.max(maximum, record.level), Number.NEGATIVE_INFINITY);
2485
+ for (const entry of group) {
2486
+ const line = document.lines[entry.record.line - 1];
2487
+ if (line === undefined)
2488
+ continue;
2489
+ replacements.push(entry === keep
2490
+ ? {
2491
+ start: line.start,
2492
+ end: line.end,
2493
+ text: replaceWhitespaceData(line.text, `${level} ${entry.provinceId}`),
2494
+ description: 'Remap supply-node province reference',
2495
+ }
2496
+ : {
2497
+ start: line.start,
2498
+ end: line.fullEnd,
2499
+ text: '',
2500
+ description: 'Remove remapped supply-node collision',
2501
+ });
2502
+ }
2503
+ }
2504
+ if (replacements.length > 0)
2505
+ addChange(changes, document.file.relativePath, applyTextRanges(document, replacements), operationId, 'text/plain');
2506
+ }
2507
+ if (index.railways.length > 0) {
2508
+ const document = required(index.railways[0], 'MAP_RAILWAY_FILE_MISSING', 'Railway source row is missing').document;
2509
+ const replacements = [];
2510
+ for (const railway of index.railways) {
2511
+ const provinces = uniqueConsecutive(railway.provinces.map((id) => remapProvinceId(provinceIds, id)));
2512
+ if (provinces.length === railway.provinces.length &&
2513
+ provinces.every((id, position) => id === railway.provinces[position]))
2514
+ continue;
2515
+ const line = document.lines[railway.line - 1];
2516
+ if (line === undefined)
2517
+ continue;
2518
+ replacements.push(provinces.length < 2
2519
+ ? {
2520
+ start: line.start,
2521
+ end: line.fullEnd,
2522
+ text: '',
2523
+ description: 'Remove collapsed railway',
2524
+ }
2525
+ : {
2526
+ start: line.start,
2527
+ end: line.end,
2528
+ text: replaceWhitespaceData(line.text, `${railway.level} ${provinces.length} ${provinces.join(' ')}`),
2529
+ description: 'Remap railway province references',
2530
+ });
2531
+ }
2532
+ if (replacements.length > 0)
2533
+ addChange(changes, document.file.relativePath, applyTextRanges(document, replacements), operationId, 'text/plain');
2534
+ }
2535
+ if (index.unitPositions.length > 0) {
2536
+ const documents = new Map(index.unitPositions.map((record) => [record.document.file.relativePath, record.document]));
2537
+ for (const [relativePath, document] of documents) {
2538
+ const replacements = index.unitPositions
2539
+ .filter((record) => record.document === document)
2540
+ .flatMap((record) => {
2541
+ const provinceId = remapProvinceId(provinceIds, record.provinceId);
2542
+ if (provinceId === record.provinceId)
2543
+ return [];
2544
+ const line = document.lines[record.line - 1];
2545
+ return line === undefined
2546
+ ? []
2547
+ : [
2548
+ {
2549
+ start: line.start,
2550
+ end: line.end,
2551
+ text: replaceDelimitedFields(line.text, new Map([[0, numberText(provinceId)]])),
2552
+ description: 'Remap unit-position province reference',
2553
+ },
2554
+ ];
2555
+ });
2556
+ if (replacements.length > 0)
2557
+ addChange(changes, relativePath, applyTextRanges(document, replacements), operationId, 'text/plain');
2558
+ }
2559
+ }
2560
+ if (index.buildingPositions.length > 0) {
2561
+ const document = required(index.buildingPositions[0], 'MAP_BUILDING_POSITION_FILE_MISSING', 'Building-position source row is missing').document;
2562
+ const replacements = index.buildingPositions.flatMap((record) => {
2563
+ const adjacentSeaProvince = remapProvinceId(provinceIds, record.adjacentSeaProvince);
2564
+ if (adjacentSeaProvince === record.adjacentSeaProvince)
2565
+ return [];
2566
+ const line = document.lines[record.line - 1];
2567
+ return line === undefined
2568
+ ? []
2569
+ : [
2570
+ {
2571
+ start: line.start,
2572
+ end: line.end,
2573
+ text: replaceDelimitedFields(line.text, new Map([[6, numberText(adjacentSeaProvince)]])),
2574
+ description: 'Remap building-position sea reference',
2575
+ },
2576
+ ];
2577
+ });
2578
+ if (replacements.length > 0)
2579
+ addChange(changes, document.file.relativePath, applyTextRanges(document, replacements), operationId, 'text/plain');
2580
+ }
2581
+ }
2582
+ function applyEntityLocator(base, changes, operation) {
2583
+ const index = currentIndex(base, changes);
2584
+ const matching = index.entityLocators.filter(({ entity, name }) => entity === operation.entity && name === operation.name);
2585
+ if (matching.length !== 1)
2586
+ throw new ServiceError('MAP_ENTITY_LOCATOR_AMBIGUOUS', `Entity locator ${operation.entity}:${operation.name} must resolve exactly once`);
2587
+ const locator = required(matching[0], 'MAP_ENTITY_LOCATOR_AMBIGUOUS', `Entity locator ${operation.entity}:${operation.name} did not resolve`);
2588
+ const indent = lineIndent(locator.document.text, locator.positionBlock.open?.start ?? locator.positionBlock.start);
2589
+ const rendered = `{ ${operation.position.map(numberText).join(' ')} }`;
2590
+ const assignment = assignments(locator.assignment.value.type === 'block' ? locator.assignment.value : locator.document.root, 'position')[0];
2591
+ if (assignment === undefined)
2592
+ throw new ServiceError('MAP_ENTITY_LOCATOR_MALFORMED', 'Entity locator position assignment is missing');
2593
+ const output = applyReplacements(locator.document, [
2594
+ {
2595
+ start: assignment.value.start,
2596
+ end: assignment.value.end,
2597
+ text: rendered,
2598
+ description: `Update entity locator at ${indent.length}`,
2599
+ },
2600
+ ]);
2601
+ addChange(changes, locator.file.relativePath, output, operation.id, 'text/plain');
2602
+ }
2603
+ function* planMapOperationSteps(base, operations, signal) {
2604
+ yield* cancellationCheckpoint(signal);
2605
+ const changes = new Map();
2606
+ const diagnostics = [];
2607
+ const blockers = [];
2608
+ const allocations = [];
2609
+ let expectedChangedBounds;
2610
+ const manifestOperations = [];
2611
+ const ids = new Set();
2612
+ for (const [operationIndex, operation] of operations.entries()) {
2613
+ yield* cancellationCheckpoint(signal, operationIndex);
2614
+ if (ids.has(operation.id)) {
2615
+ blockers.push({
2616
+ code: 'MAP_OPERATION_ID_DUPLICATE',
2617
+ message: `Operation ID ${operation.id} is duplicated`,
2618
+ operationId: operation.id,
2619
+ });
2620
+ continue;
2621
+ }
2622
+ ids.add(operation.id);
2623
+ manifestOperations.push({
2624
+ id: operation.id,
2625
+ kind: operation.kind,
2626
+ summary: operation.summary ?? operation.kind.replaceAll('_', ' '),
2627
+ data: JSON.parse(JSON.stringify(operation)),
2628
+ });
2629
+ const changesCheckpoint = new Map([...changes].map(([key, change]) => [
2630
+ key,
2631
+ {
2632
+ ...change,
2633
+ content: change.content === null ? null : Buffer.from(change.content),
2634
+ operationIds: new Set(change.operationIds),
2635
+ },
2636
+ ]));
2637
+ const allocationsCheckpoint = allocations.length;
2638
+ const boundsCheckpoint = expectedChangedBounds;
2639
+ try {
2640
+ if (operation.kind === 'move_state_provinces')
2641
+ applyMoveStateProvinces(base, changes, operation);
2642
+ else if (operation.kind === 'update_state')
2643
+ applyUpdateState(base, changes, operation);
2644
+ else if (operation.kind === 'split_state' || operation.kind === 'create_state')
2645
+ yield* applySplitState(base, changes, allocations, operation, signal);
2646
+ else if (operation.kind === 'merge_states')
2647
+ applyMergeStates(base, changes, operation);
2648
+ else if (operation.kind === 'split_province' || operation.kind === 'create_province')
2649
+ expectedChangedBounds = mergeBounds(expectedChangedBounds, yield* applySplitProvince(base, changes, allocations, operation, signal));
2650
+ else if (operation.kind === 'merge_provinces' || operation.kind === 'remove_province')
2651
+ expectedChangedBounds = mergeBounds(expectedChangedBounds, yield* applyMergeProvinces(base, changes, operation, signal));
2652
+ else if (operation.kind === 'update_province_definition')
2653
+ expectedChangedBounds = mergeBounds(expectedChangedBounds, yield* applyUpdateProvinceDefinition(base, changes, operation, signal));
2654
+ else if (operation.kind === 'move_region_provinces')
2655
+ applyMoveRegions(base, changes, operation);
2656
+ else if (operation.kind === 'add_normal_adjacency' ||
2657
+ operation.kind === 'remove_normal_adjacency')
2658
+ expectedChangedBounds = mergeBounds(expectedChangedBounds, yield* applyNormalAdjacency(base, changes, operation, signal));
2659
+ else if (operation.kind === 'update_entity_locator')
2660
+ applyEntityLocator(base, changes, operation);
2661
+ else
2662
+ handleSimpleOperation(base, changes, operation);
2663
+ }
2664
+ catch (error) {
2665
+ if (signal?.aborted)
2666
+ throw error;
2667
+ changes.clear();
2668
+ for (const [key, change] of changesCheckpoint)
2669
+ changes.set(key, change);
2670
+ allocations.length = allocationsCheckpoint;
2671
+ expectedChangedBounds = boundsCheckpoint;
2672
+ const serviceError = error instanceof ServiceError
2673
+ ? error
2674
+ : new ServiceError('MAP_OPERATION_FAILED', error instanceof Error ? error.message : String(error));
2675
+ const blocker = {
2676
+ code: serviceError.code,
2677
+ message: serviceError.message,
2678
+ operationId: operation.id,
2679
+ ...(Object.keys(serviceError.details).length === 0
2680
+ ? {}
2681
+ : { details: serviceError.details }),
2682
+ };
2683
+ blockers.push(blocker);
2684
+ diagnostics.push({
2685
+ code: blocker.code,
2686
+ severity: 'blocker',
2687
+ category: 'map',
2688
+ message: blocker.message,
2689
+ operationId: blocker.operationId,
2690
+ ...(blocker.details === undefined ? {} : { details: blocker.details }),
2691
+ });
2692
+ }
2693
+ }
2694
+ const finalized = proposedChanges(changes);
2695
+ yield* cancellationCheckpoint(signal);
2696
+ const finalIndex = indexWithProposedChanges(base, finalized);
2697
+ return {
2698
+ changes: finalized,
2699
+ diagnostics,
2700
+ blockers,
2701
+ allocations,
2702
+ ...(expectedChangedBounds === undefined ? {} : { expectedChangedBounds }),
2703
+ finalIndex,
2704
+ operations: manifestOperations,
2705
+ };
2706
+ }
2707
+ export function planMapOperations(base, operations, signal) {
2708
+ return completeSynchronously(planMapOperationSteps(base, operations, signal));
2709
+ }
2710
+ /** Plans map mutations while periodically yielding so protocol cancellation can be delivered. */
2711
+ export async function planMapOperationsAsync(base, operations, signal) {
2712
+ return completeCooperatively(planMapOperationSteps(base, operations, signal), signal);
2713
+ }
2714
+ export function mapOperationId(operation) {
2715
+ return deterministicId('map_operation', operation);
2716
+ }
2717
+ //# sourceMappingURL=operations.js.map