@nekosuneprojects/gaming-infos 2.2.0 → 2.4.0

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 (716) hide show
  1. package/.github/workflows/sync-api-cache.yml +3 -2
  2. package/CACHE_SYNC.md +84 -54
  3. package/CHANGELOG.md +34 -3
  4. package/README.md +210 -371
  5. package/data/.cache-manifest.json +112 -0
  6. package/data/arknightsendfield/characters/akekuri.json +75 -0
  7. package/data/arknightsendfield/characters/alesh.json +75 -0
  8. package/data/arknightsendfield/characters/antal.json +75 -0
  9. package/data/arknightsendfield/characters/arcane.json +75 -0
  10. package/data/arknightsendfield/characters/arclight.json +75 -0
  11. package/data/arknightsendfield/characters/ardelia.json +75 -0
  12. package/data/arknightsendfield/characters/avywenna.json +76 -0
  13. package/data/arknightsendfield/characters/camille.json +77 -0
  14. package/data/arknightsendfield/characters/catcher.json +76 -0
  15. package/data/arknightsendfield/characters/chenqianyu.json +76 -0
  16. package/data/arknightsendfield/characters/dapan.json +76 -0
  17. package/data/arknightsendfield/characters/ember.json +76 -0
  18. package/data/arknightsendfield/characters/endministrator.json +63 -0
  19. package/data/arknightsendfield/characters/estella.json +76 -0
  20. package/data/arknightsendfield/characters/fluorite.json +75 -0
  21. package/data/arknightsendfield/characters/gilberta.json +76 -0
  22. package/data/arknightsendfield/characters/laevatain.json +76 -0
  23. package/data/arknightsendfield/characters/lastrite.json +76 -0
  24. package/data/arknightsendfield/characters/lifeng.json +75 -0
  25. package/data/arknightsendfield/characters/liino.json +76 -0
  26. package/data/arknightsendfield/characters/mifu.json +75 -0
  27. package/data/arknightsendfield/characters/perlica.json +76 -0
  28. package/data/arknightsendfield/characters/pogranichnik.json +75 -0
  29. package/data/arknightsendfield/characters/purrchena.json +50 -0
  30. package/data/arknightsendfield/characters/rossi.json +77 -0
  31. package/data/arknightsendfield/characters/snowshine.json +75 -0
  32. package/data/arknightsendfield/characters/tangtang.json +76 -0
  33. package/data/arknightsendfield/characters/typhoeus.json +57 -0
  34. package/data/arknightsendfield/characters/wulfgard.json +77 -0
  35. package/data/arknightsendfield/characters/xaihi.json +76 -0
  36. package/data/arknightsendfield/characters/yvonne.json +76 -0
  37. package/data/arknightsendfield/characters/zhuangfangyi.json +76 -0
  38. package/data/fortnite/characters/agentjones.json +43 -0
  39. package/data/fortnite/characters/agentpeely.json +43 -0
  40. package/data/fortnite/characters/andyfangerson.json +42 -0
  41. package/data/fortnite/characters/antonia.json +43 -0
  42. package/data/fortnite/characters/anwar.json +44 -0
  43. package/data/fortnite/characters/aphrodite.json +43 -0
  44. package/data/fortnite/characters/apollo.json +36 -0
  45. package/data/fortnite/characters/arcticassassin.json +43 -0
  46. package/data/fortnite/characters/artemis.json +36 -0
  47. package/data/fortnite/characters/battlewoodboulevard.json +43 -0
  48. package/data/fortnite/characters/beastmode.json +45 -0
  49. package/data/fortnite/characters/bogstick.json +43 -0
  50. package/data/fortnite/characters/boxy.json +44 -0
  51. package/data/fortnite/characters/bravoleader.json +44 -0
  52. package/data/fortnite/characters/briteraider.json +43 -0
  53. package/data/fortnite/characters/brutalboxcars.json +46 -0
  54. package/data/fortnite/characters/bryne.json +44 -0
  55. package/data/fortnite/characters/buildersbarracks.json +38 -0
  56. package/data/fortnite/characters/bullshark.json +43 -0
  57. package/data/fortnite/characters/bumpybay.json +44 -0
  58. package/data/fortnite/characters/burd.json +44 -0
  59. package/data/fortnite/characters/burdemart.json +39 -0
  60. package/data/fortnite/characters/burdtogo.json +42 -0
  61. package/data/fortnite/characters/busansanctuary.json +39 -0
  62. package/data/fortnite/characters/calamaricanyon.json +41 -0
  63. package/data/fortnite/characters/canyoncrossing.json +45 -0
  64. package/data/fortnite/characters/captainjones.json +40 -0
  65. package/data/fortnite/characters/centurion.json +44 -0
  66. package/data/fortnite/characters/cerberus.json +43 -0
  67. package/data/fortnite/characters/charon.json +43 -0
  68. package/data/fortnite/characters/chiara.json +43 -0
  69. package/data/fortnite/characters/clara.json +43 -0
  70. package/data/fortnite/characters/classifiedcanyon.json +43 -0
  71. package/data/fortnite/characters/clawsylodge.json +41 -0
  72. package/data/fortnite/characters/cletuscornhole.json +43 -0
  73. package/data/fortnite/characters/contractgiller.json +44 -0
  74. package/data/fortnite/characters/corruptioncorners.json +42 -0
  75. package/data/fortnite/characters/countessdaraku.json +45 -0
  76. package/data/fortnite/characters/crackshotscabin.json +40 -0
  77. package/data/fortnite/characters/creepycamps.json +36 -0
  78. package/data/fortnite/characters/crimecity.json +47 -0
  79. package/data/fortnite/characters/dara.json +42 -0
  80. package/data/fortnite/characters/darkruby.json +43 -0
  81. package/data/fortnite/characters/demonsdojo.json +46 -0
  82. package/data/fortnite/characters/diamonddiva.json +43 -0
  83. package/data/fortnite/characters/donutdistrict.json +43 -0
  84. package/data/fortnite/characters/drakonsteelrider.json +43 -0
  85. package/data/fortnite/characters/dummy.json +45 -0
  86. package/data/fortnite/characters/edwardscissorhands.json +39 -0
  87. package/data/fortnite/characters/etheria.json +43 -0
  88. package/data/fortnite/characters/evergreenterrace.json +41 -0
  89. package/data/fortnite/characters/fishmarket.json +39 -0
  90. package/data/fortnite/characters/fishstick.json +45 -0
  91. package/data/fortnite/characters/fishthicc.json +43 -0
  92. package/data/fortnite/characters/fortressofsolitude.json +37 -0
  93. package/data/fortnite/characters/foxyfloodgate.json +43 -0
  94. package/data/fortnite/characters/freakyfields.json +38 -0
  95. package/data/fortnite/characters/goldengrove.json +41 -0
  96. package/data/fortnite/characters/graveheart.json +43 -0
  97. package/data/fortnite/characters/grievouscampsite.json +38 -0
  98. package/data/fortnite/characters/guardianamara.json +44 -0
  99. package/data/fortnite/characters/gwenpool.json +41 -0
  100. package/data/fortnite/characters/hanamuracourtyard.json +27 -0
  101. package/data/fortnite/characters/heartbreakranger.json +42 -0
  102. package/data/fortnite/characters/heatwaveharbor.json +41 -0
  103. package/data/fortnite/characters/heroichope.json +44 -0
  104. package/data/fortnite/characters/hope.json +43 -0
  105. package/data/fortnite/characters/hopefulheights.json +46 -0
  106. package/data/fortnite/characters/humblehills.json +41 -0
  107. package/data/fortnite/characters/innovatorslone.json +43 -0
  108. package/data/fortnite/characters/insight.json +43 -0
  109. package/data/fortnite/characters/ironman.json +40 -0
  110. package/data/fortnite/characters/jittershock.json +44 -0
  111. package/data/fortnite/characters/joeycaliente.json +44 -0
  112. package/data/fortnite/characters/jonithescarlet.json +43 -0
  113. package/data/fortnite/characters/khabylame.json +40 -0
  114. package/data/fortnite/characters/khari.json +44 -0
  115. package/data/fortnite/characters/kitbash.json +44 -0
  116. package/data/fortnite/characters/kitesflight.json +41 -0
  117. package/data/fortnite/characters/knightmare.json +43 -0
  118. package/data/fortnite/characters/laguna.json +43 -0
  119. package/data/fortnite/characters/lakespringfield.json +35 -0
  120. package/data/fortnite/characters/lakespringfieldgasstation.json +39 -0
  121. package/data/fortnite/characters/lanallane.json +42 -0
  122. package/data/fortnite/characters/lattelanding.json +41 -0
  123. package/data/fortnite/characters/lavendra.json +44 -0
  124. package/data/fortnite/characters/leelah.json +45 -0
  125. package/data/fortnite/characters/liftylodge.json +42 -0
  126. package/data/fortnite/characters/lonewolflair.json +42 -0
  127. package/data/fortnite/characters/longshot.json +41 -0
  128. package/data/fortnite/characters/lostlake.json +44 -0
  129. package/data/fortnite/characters/loveranger.json +44 -0
  130. package/data/fortnite/characters/mae.json +43 -0
  131. package/data/fortnite/characters/magicmosses.json +42 -0
  132. package/data/fortnite/characters/maskedmeadows.json +48 -0
  133. package/data/fortnite/characters/mechateamshadow.json +44 -0
  134. package/data/fortnite/characters/medusa.json +43 -0
  135. package/data/fortnite/characters/meowscles.json +42 -0
  136. package/data/fortnite/characters/mephisto.json +41 -0
  137. package/data/fortnite/characters/metalmouth.json +44 -0
  138. package/data/fortnite/characters/mincemeat.json +43 -0
  139. package/data/fortnite/characters/munitionsexpert.json +42 -0
  140. package/data/fortnite/characters/myna.json +42 -0
  141. package/data/fortnite/characters/newsanctuary.json +43 -0
  142. package/data/fortnite/characters/nia.json +42 -0
  143. package/data/fortnite/characters/nightshiftforest.json +43 -0
  144. package/data/fortnite/characters/nisha.json +42 -0
  145. package/data/fortnite/characters/nolanchance.json +43 -0
  146. package/data/fortnite/characters/noorah.json +44 -0
  147. package/data/fortnite/characters/odyssey.json +43 -0
  148. package/data/fortnite/characters/openaironsen.json +40 -0
  149. package/data/fortnite/characters/oscar.json +42 -0
  150. package/data/fortnite/characters/outlawoasis.json +40 -0
  151. package/data/fortnite/characters/outpostenclave.json +27 -0
  152. package/data/fortnite/characters/oxrhq.json +37 -0
  153. package/data/fortnite/characters/paintedpalms.json +41 -0
  154. package/data/fortnite/characters/peabody.json +42 -0
  155. package/data/fortnite/characters/peelosopherbananocrates.json +43 -0
  156. package/data/fortnite/characters/peely.json +41 -0
  157. package/data/fortnite/characters/peelybone.json +43 -0
  158. package/data/fortnite/characters/persephone.json +42 -0
  159. package/data/fortnite/characters/phaedra.json +42 -0
  160. package/data/fortnite/characters/phantommeowscles.json +43 -0
  161. package/data/fortnite/characters/piperpace.json +43 -0
  162. package/data/fortnite/characters/poseidon.json +42 -0
  163. package/data/fortnite/characters/pumpedpower.json +40 -0
  164. package/data/fortnite/characters/pumpinpipes.json +39 -0
  165. package/data/fortnite/characters/pumpkinking.json +40 -0
  166. package/data/fortnite/characters/purradisemeowscles.json +43 -0
  167. package/data/fortnite/characters/rangersrest.json +36 -0
  168. package/data/fortnite/characters/rebeloutpost.json +37 -0
  169. package/data/fortnite/characters/renegadeshadow.json +44 -0
  170. package/data/fortnite/characters/resistancebase.json +27 -0
  171. package/data/fortnite/characters/rippedtides.json +43 -0
  172. package/data/fortnite/characters/rollingblossomsfarm.json +41 -0
  173. package/data/fortnite/characters/royalebomber.json +38 -0
  174. package/data/fortnite/characters/ruckus.json +42 -0
  175. package/data/fortnite/characters/rust.json +41 -0
  176. package/data/fortnite/characters/sakuracircle.json +36 -0
  177. package/data/fortnite/characters/sandystrip.json +44 -0
  178. package/data/fortnite/characters/saskia.json +45 -0
  179. package/data/fortnite/characters/seaportcity.json +48 -0
  180. package/data/fortnite/characters/shakensanctuary.json +42 -0
  181. package/data/fortnite/characters/shimmerdusk.json +41 -0
  182. package/data/fortnite/characters/shinyshafts.json +38 -0
  183. package/data/fortnite/characters/shogunssolitude.json +46 -0
  184. package/data/fortnite/characters/shuri.json +39 -0
  185. package/data/fortnite/characters/silashesk.json +42 -0
  186. package/data/fortnite/characters/slurproom.json +40 -0
  187. package/data/fortnite/characters/snowheart.json +44 -0
  188. package/data/fortnite/characters/snowstriker.json +43 -0
  189. package/data/fortnite/characters/solidsnake.json +39 -0
  190. package/data/fortnite/characters/sonor.json +46 -0
  191. package/data/fortnite/characters/spartanassassin.json +43 -0
  192. package/data/fortnite/characters/spirithuntersaeko.json +41 -0
  193. package/data/fortnite/characters/springfieldnationalparkstore.json +38 -0
  194. package/data/fortnite/characters/springfieldnuclearpowerplant.json +39 -0
  195. package/data/fortnite/characters/springfieldslurpworks.json +40 -0
  196. package/data/fortnite/characters/squibblyshores.json +42 -0
  197. package/data/fortnite/characters/squibblyspointlighthouse.json +37 -0
  198. package/data/fortnite/characters/starroakley.json +45 -0
  199. package/data/fortnite/characters/steelsight.json +43 -0
  200. package/data/fortnite/characters/sticks.json +35 -0
  201. package/data/fortnite/characters/sunkenshores.json +42 -0
  202. package/data/fortnite/characters/sunstrider.json +42 -0
  203. package/data/fortnite/characters/suntanspecialist.json +43 -0
  204. package/data/fortnite/characters/supernovaacademy.json +27 -0
  205. package/data/fortnite/characters/t60powerarmor.json +39 -0
  206. package/data/fortnite/characters/thebattlewoods.json +44 -0
  207. package/data/fortnite/characters/thegreatturtle.json +37 -0
  208. package/data/fortnite/characters/theoperator.json +42 -0
  209. package/data/fortnite/characters/thequackaway.json +39 -0
  210. package/data/fortnite/characters/thestabilizer.json +38 -0
  211. package/data/fortnite/characters/theunderwriter.json +36 -0
  212. package/data/fortnite/characters/toyboxdocks.json +40 -0
  213. package/data/fortnite/characters/trace.json +43 -0
  214. package/data/fortnite/characters/triagetrooper.json +43 -0
  215. package/data/fortnite/characters/triggerfish.json +44 -0
  216. package/data/fortnite/characters/trogscave.json +38 -0
  217. package/data/fortnite/characters/utopiacity.json +27 -0
  218. package/data/fortnite/characters/vengeancejones.json +42 -0
  219. package/data/fortnite/characters/victoriasaint.json +43 -0
  220. package/data/fortnite/characters/vineyshafts.json +35 -0
  221. package/data/fortnite/characters/volpez.json +41 -0
  222. package/data/fortnite/characters/warmachine.json +40 -0
  223. package/data/fortnite/characters/wastelandermagneto.json +40 -0
  224. package/data/fortnite/characters/wonkeeland.json +41 -0
  225. package/data/game-codes/.cache-manifest.json +148 -0
  226. package/data/game-codes/directory.json +1196 -0
  227. package/data/game-codes/games/arknights-endfield.json +958 -0
  228. package/data/game-codes/games/borderlands-1.json +414 -0
  229. package/data/game-codes/games/borderlands-2.json +340 -0
  230. package/data/game-codes/games/borderlands-3.json +1025 -0
  231. package/data/game-codes/games/borderlands-4.json +3128 -0
  232. package/data/game-codes/games/borderlands-the-pre-sequel.json +599 -0
  233. package/data/game-codes/games/brawl-stars.json +4766 -0
  234. package/data/game-codes/games/clash-of-clans.json +4766 -0
  235. package/data/game-codes/games/clash-royale.json +5 -0
  236. package/data/game-codes/games/cookie-run-kingdom.json +5366 -0
  237. package/data/game-codes/games/fortnite.json +897 -0
  238. package/data/game-codes/games/genshin-impact.json +8520 -0
  239. package/data/game-codes/games/honkai-impact-3rd.json +405 -0
  240. package/data/game-codes/games/honkai-star-rail.json +9863 -0
  241. package/data/game-codes/games/neverness-to-everness.json +1695 -0
  242. package/data/game-codes/games/roblox-doors.json +1842 -0
  243. package/data/game-codes/games/roblox-rivals.json +2919 -0
  244. package/data/game-codes/games/tower-of-fantasy.json +2839 -0
  245. package/data/game-codes/games/warframe.json +20824 -0
  246. package/data/game-codes/games/wuthering-waves.json +2477 -0
  247. package/data/game-codes/games/zenless-zone-zero.json +3681 -0
  248. package/data/game-codes/status.json +580 -0
  249. package/data/genshinimpact/characters/aino.json +34 -0
  250. package/data/genshinimpact/characters/albedo.json +10 -9
  251. package/data/genshinimpact/characters/alhaitham.json +11 -10
  252. package/data/genshinimpact/characters/aloy.json +10 -9
  253. package/data/genshinimpact/characters/alyosha.json +34 -0
  254. package/data/genshinimpact/characters/amber.json +11 -10
  255. package/data/genshinimpact/characters/aratakiitto.json +11 -10
  256. package/data/genshinimpact/characters/arlecchino.json +11 -10
  257. package/data/genshinimpact/characters/baizhu.json +11 -10
  258. package/data/genshinimpact/characters/barbara.json +10 -9
  259. package/data/genshinimpact/characters/beidou.json +11 -10
  260. package/data/genshinimpact/characters/bennett.json +10 -9
  261. package/data/genshinimpact/characters/candace.json +11 -10
  262. package/data/genshinimpact/characters/charlotte.json +10 -9
  263. package/data/genshinimpact/characters/chasca.json +10 -9
  264. package/data/genshinimpact/characters/chevreuse.json +10 -9
  265. package/data/genshinimpact/characters/chiori.json +10 -9
  266. package/data/genshinimpact/characters/chongyun.json +11 -10
  267. package/data/genshinimpact/characters/citlali.json +10 -9
  268. package/data/genshinimpact/characters/clorinde.json +10 -9
  269. package/data/genshinimpact/characters/collei.json +11 -10
  270. package/data/genshinimpact/characters/columbina.json +34 -0
  271. package/data/genshinimpact/characters/cyno.json +11 -10
  272. package/data/genshinimpact/characters/dahlia.json +9 -8
  273. package/data/genshinimpact/characters/dehya.json +10 -9
  274. package/data/genshinimpact/characters/diluc.json +10 -9
  275. package/data/genshinimpact/characters/diona.json +10 -9
  276. package/data/genshinimpact/characters/dori.json +10 -9
  277. package/data/genshinimpact/characters/durin.json +10 -9
  278. package/data/genshinimpact/characters/emilie.json +10 -9
  279. package/data/genshinimpact/characters/escoffier.json +11 -10
  280. package/data/genshinimpact/characters/eula.json +10 -9
  281. package/data/genshinimpact/characters/faruzan.json +11 -10
  282. package/data/genshinimpact/characters/fischl.json +11 -10
  283. package/data/genshinimpact/characters/flins.json +34 -0
  284. package/data/genshinimpact/characters/freminet.json +10 -9
  285. package/data/genshinimpact/characters/furina.json +11 -10
  286. package/data/genshinimpact/characters/gaming.json +10 -9
  287. package/data/genshinimpact/characters/ganyu.json +10 -9
  288. package/data/genshinimpact/characters/gorou.json +11 -10
  289. package/data/genshinimpact/characters/hutao.json +11 -10
  290. package/data/genshinimpact/characters/iansan.json +10 -9
  291. package/data/genshinimpact/characters/ifa.json +34 -0
  292. package/data/genshinimpact/characters/illuga.json +34 -0
  293. package/data/genshinimpact/characters/ineffa.json +10 -9
  294. package/data/genshinimpact/characters/jahoda.json +34 -0
  295. package/data/genshinimpact/characters/jean.json +10 -9
  296. package/data/genshinimpact/characters/kachina.json +10 -9
  297. package/data/genshinimpact/characters/kaedeharakazuha.json +11 -10
  298. package/data/genshinimpact/characters/kaeya.json +10 -9
  299. package/data/genshinimpact/characters/kamisatoayaka.json +10 -9
  300. package/data/genshinimpact/characters/kamisatoayato.json +10 -9
  301. package/data/genshinimpact/characters/kaveh.json +11 -10
  302. package/data/genshinimpact/characters/keqing.json +10 -9
  303. package/data/genshinimpact/characters/kinich.json +10 -9
  304. package/data/genshinimpact/characters/kirara.json +10 -9
  305. package/data/genshinimpact/characters/klee.json +10 -9
  306. package/data/genshinimpact/characters/kujousara.json +10 -9
  307. package/data/genshinimpact/characters/kukishinobu.json +11 -10
  308. package/data/genshinimpact/characters/lanyan.json +10 -9
  309. package/data/genshinimpact/characters/lauma.json +34 -0
  310. package/data/genshinimpact/characters/layla.json +10 -9
  311. package/data/genshinimpact/characters/linnea.json +34 -0
  312. package/data/genshinimpact/characters/lisa.json +11 -10
  313. package/data/genshinimpact/characters/lohen.json +10 -9
  314. package/data/genshinimpact/characters/lynette.json +10 -9
  315. package/data/genshinimpact/characters/lyney.json +10 -9
  316. package/data/genshinimpact/characters/mavuika.json +10 -9
  317. package/data/genshinimpact/characters/mika.json +34 -0
  318. package/data/genshinimpact/characters/mikaschmidt.json +10 -9
  319. package/data/genshinimpact/characters/mona.json +11 -10
  320. package/data/genshinimpact/characters/mualani.json +10 -9
  321. package/data/genshinimpact/characters/nahida.json +11 -10
  322. package/data/genshinimpact/characters/navia.json +10 -9
  323. package/data/genshinimpact/characters/nefer.json +34 -0
  324. package/data/genshinimpact/characters/neuvillette.json +10 -9
  325. package/data/genshinimpact/characters/nicole.json +34 -0
  326. package/data/genshinimpact/characters/nilou.json +10 -9
  327. package/data/genshinimpact/characters/ningguang.json +10 -9
  328. package/data/genshinimpact/characters/noelle.json +11 -10
  329. package/data/genshinimpact/characters/odette.json +34 -0
  330. package/data/genshinimpact/characters/ororon.json +10 -9
  331. package/data/genshinimpact/characters/prune.json +9 -8
  332. package/data/genshinimpact/characters/qiqi.json +11 -10
  333. package/data/genshinimpact/characters/raidenshogun.json +11 -10
  334. package/data/genshinimpact/characters/razor.json +10 -9
  335. package/data/genshinimpact/characters/rosaria.json +11 -10
  336. package/data/genshinimpact/characters/sandrone.json +34 -0
  337. package/data/genshinimpact/characters/sangonomiyakokomi.json +11 -10
  338. package/data/genshinimpact/characters/sayu.json +10 -9
  339. package/data/genshinimpact/characters/sethos.json +10 -9
  340. package/data/genshinimpact/characters/shenhe.json +11 -10
  341. package/data/genshinimpact/characters/shikanoinheizou.json +11 -10
  342. package/data/genshinimpact/characters/sigewinne.json +10 -9
  343. package/data/genshinimpact/characters/skirk.json +10 -9
  344. package/data/genshinimpact/characters/sucrose.json +11 -10
  345. package/data/genshinimpact/characters/tartaglia.json +34 -0
  346. package/data/genshinimpact/characters/thoma.json +10 -9
  347. package/data/genshinimpact/characters/tighnari.json +11 -10
  348. package/data/genshinimpact/characters/traveler.json +34 -0
  349. package/data/genshinimpact/characters/varesa.json +10 -9
  350. package/data/genshinimpact/characters/varka.json +9 -8
  351. package/data/genshinimpact/characters/venti.json +11 -10
  352. package/data/genshinimpact/characters/wanderer.json +10 -9
  353. package/data/genshinimpact/characters/wonderlandmanekin.json +34 -0
  354. package/data/genshinimpact/characters/wriothesley.json +11 -10
  355. package/data/genshinimpact/characters/xiangling.json +11 -10
  356. package/data/genshinimpact/characters/xianyun.json +10 -9
  357. package/data/genshinimpact/characters/xiao.json +10 -9
  358. package/data/genshinimpact/characters/xilonen.json +10 -9
  359. package/data/genshinimpact/characters/xingqiu.json +11 -10
  360. package/data/genshinimpact/characters/xinyan.json +11 -10
  361. package/data/genshinimpact/characters/yaemiko.json +11 -10
  362. package/data/genshinimpact/characters/yanfei.json +11 -10
  363. package/data/genshinimpact/characters/yaoyao.json +11 -10
  364. package/data/genshinimpact/characters/yelan.json +10 -9
  365. package/data/genshinimpact/characters/yoimiya.json +11 -10
  366. package/data/genshinimpact/characters/yumemizukimizuki.json +10 -9
  367. package/data/genshinimpact/characters/yunjin.json +10 -9
  368. package/data/genshinimpact/characters/zhongli.json +10 -9
  369. package/data/genshinimpact/characters/zibai.json +10 -9
  370. package/data/genshinimpact/meta.json +12 -12
  371. package/data/honkaistarrail/characters/acheron.json +11 -10
  372. package/data/honkaistarrail/characters/aglaea.json +11 -10
  373. package/data/honkaistarrail/characters/anaxa.json +11 -10
  374. package/data/honkaistarrail/characters/archer.json +29 -0
  375. package/data/honkaistarrail/characters/argenti.json +11 -10
  376. package/data/honkaistarrail/characters/arlan.json +11 -10
  377. package/data/honkaistarrail/characters/ashveil.json +11 -10
  378. package/data/honkaistarrail/characters/asta.json +11 -10
  379. package/data/honkaistarrail/characters/aventurine.json +11 -10
  380. package/data/honkaistarrail/characters/bailu.json +11 -10
  381. package/data/honkaistarrail/characters/blackswan.json +11 -10
  382. package/data/honkaistarrail/characters/blade.json +11 -10
  383. package/data/honkaistarrail/characters/boothill.json +11 -10
  384. package/data/honkaistarrail/characters/bronya.json +11 -10
  385. package/data/honkaistarrail/characters/castorice.json +11 -10
  386. package/data/honkaistarrail/characters/cerydra.json +11 -10
  387. package/data/honkaistarrail/characters/cipher.json +11 -10
  388. package/data/honkaistarrail/characters/clara.json +11 -10
  389. package/data/honkaistarrail/characters/constance.json +11 -10
  390. package/data/honkaistarrail/characters/cyrene.json +11 -10
  391. package/data/honkaistarrail/characters/danheng.json +11 -10
  392. package/data/honkaistarrail/characters/danhengimbibitorlunae.json +29 -0
  393. package/data/honkaistarrail/characters/danhengpermansorterrae.json +29 -0
  394. package/data/honkaistarrail/characters/drratio.json +11 -10
  395. package/data/honkaistarrail/characters/evanescia.json +29 -0
  396. package/data/honkaistarrail/characters/evernight.json +29 -0
  397. package/data/honkaistarrail/characters/feixiao.json +11 -10
  398. package/data/honkaistarrail/characters/firefly.json +11 -10
  399. package/data/honkaistarrail/characters/fugue.json +29 -0
  400. package/data/honkaistarrail/characters/fuxuan.json +11 -10
  401. package/data/honkaistarrail/characters/gallagher.json +11 -10
  402. package/data/honkaistarrail/characters/gepard.json +11 -10
  403. package/data/honkaistarrail/characters/gilgamesh.json +29 -0
  404. package/data/honkaistarrail/characters/guinaifen.json +11 -10
  405. package/data/honkaistarrail/characters/hanya.json +11 -10
  406. package/data/honkaistarrail/characters/herta.json +11 -10
  407. package/data/honkaistarrail/characters/himeko.json +12 -11
  408. package/data/honkaistarrail/characters/himekonova.json +29 -0
  409. package/data/honkaistarrail/characters/hook.json +11 -10
  410. package/data/honkaistarrail/characters/huohuo.json +11 -10
  411. package/data/honkaistarrail/characters/hyacine.json +11 -10
  412. package/data/honkaistarrail/characters/hysilens.json +11 -10
  413. package/data/honkaistarrail/characters/jade.json +11 -10
  414. package/data/honkaistarrail/characters/jiaoqiu.json +11 -10
  415. package/data/honkaistarrail/characters/jingliu.json +11 -10
  416. package/data/honkaistarrail/characters/jingyuan.json +11 -10
  417. package/data/honkaistarrail/characters/kafka.json +11 -10
  418. package/data/honkaistarrail/characters/lingsha.json +11 -10
  419. package/data/honkaistarrail/characters/lisushang.json +11 -10
  420. package/data/honkaistarrail/characters/luka.json +11 -10
  421. package/data/honkaistarrail/characters/luocha.json +11 -10
  422. package/data/honkaistarrail/characters/lynx.json +11 -10
  423. package/data/honkaistarrail/characters/march7th.json +11 -10
  424. package/data/honkaistarrail/characters/misha.json +11 -10
  425. package/data/honkaistarrail/characters/mortenaxblade.json +29 -0
  426. package/data/honkaistarrail/characters/moze.json +11 -10
  427. package/data/honkaistarrail/characters/mydei.json +11 -10
  428. package/data/honkaistarrail/characters/natasha.json +11 -10
  429. package/data/honkaistarrail/characters/pela.json +11 -10
  430. package/data/honkaistarrail/characters/phainon.json +11 -10
  431. package/data/honkaistarrail/characters/qingque.json +11 -10
  432. package/data/honkaistarrail/characters/rappa.json +11 -10
  433. package/data/honkaistarrail/characters/rintohsaka.json +29 -0
  434. package/data/honkaistarrail/characters/robin.json +11 -10
  435. package/data/honkaistarrail/characters/ruanmei.json +11 -10
  436. package/data/honkaistarrail/characters/saber.json +29 -0
  437. package/data/honkaistarrail/characters/sampo.json +11 -10
  438. package/data/honkaistarrail/characters/seele.json +11 -10
  439. package/data/honkaistarrail/characters/serval.json +11 -10
  440. package/data/honkaistarrail/characters/silverwolf.json +11 -10
  441. package/data/honkaistarrail/characters/silverwolflv999.json +29 -0
  442. package/data/honkaistarrail/characters/sparkle.json +11 -10
  443. package/data/honkaistarrail/characters/sparxie.json +29 -0
  444. package/data/honkaistarrail/characters/sunday.json +29 -0
  445. package/data/honkaistarrail/characters/thedahlia.json +29 -0
  446. package/data/honkaistarrail/characters/theherta.json +29 -0
  447. package/data/honkaistarrail/characters/tingyun.json +11 -10
  448. package/data/honkaistarrail/characters/topaz.json +11 -10
  449. package/data/honkaistarrail/characters/topaznumby.json +29 -0
  450. package/data/honkaistarrail/characters/trailblazer.json +29 -0
  451. package/data/honkaistarrail/characters/tribbie.json +11 -10
  452. package/data/honkaistarrail/characters/welt.json +29 -0
  453. package/data/honkaistarrail/characters/weltyang.json +11 -10
  454. package/data/honkaistarrail/characters/xueyi.json +11 -10
  455. package/data/honkaistarrail/characters/yanqing.json +11 -10
  456. package/data/honkaistarrail/characters/yaoguang.json +11 -10
  457. package/data/honkaistarrail/characters/yukong.json +11 -10
  458. package/data/honkaistarrail/characters/yunli.json +11 -10
  459. package/data/honkaistarrail/meta.json +12 -12
  460. package/data/nte/characters/adler.json +13 -12
  461. package/data/nte/characters/aurelia.json +13 -12
  462. package/data/nte/characters/baicang.json +13 -12
  463. package/data/nte/characters/chaos.json +13 -12
  464. package/data/nte/characters/chiz.json +13 -12
  465. package/data/nte/characters/daffodill.json +13 -12
  466. package/data/nte/characters/edgar.json +13 -12
  467. package/data/nte/characters/esperzero.json +13 -12
  468. package/data/nte/characters/fadia.json +13 -12
  469. package/data/nte/characters/haniel.json +13 -12
  470. package/data/nte/characters/hathor.json +13 -12
  471. package/data/nte/characters/hotori.json +13 -12
  472. package/data/nte/characters/iroi.json +13 -12
  473. package/data/nte/characters/jiuyuan.json +13 -12
  474. package/data/nte/characters/lacrimosa.json +13 -12
  475. package/data/nte/characters/mint.json +13 -12
  476. package/data/nte/characters/nanally.json +13 -12
  477. package/data/nte/characters/sakiri.json +13 -12
  478. package/data/nte/characters/shinku.json +13 -12
  479. package/data/nte/characters/skia.json +13 -12
  480. package/data/nte/characters/zankou.json +51 -0
  481. package/data/nte/meta.json +12 -12
  482. package/data/toweroffantasy/characters/alyss.json +27 -0
  483. package/data/toweroffantasy/characters/annabella.json +23 -0
  484. package/data/toweroffantasy/characters/asuka.json +23 -0
  485. package/data/toweroffantasy/characters/bailing.json +23 -0
  486. package/data/toweroffantasy/characters/brevey.json +23 -0
  487. package/data/toweroffantasy/characters/claudia.json +23 -0
  488. package/data/toweroffantasy/characters/cobaltb.json +23 -0
  489. package/data/toweroffantasy/characters/cocoritter.json +23 -0
  490. package/data/toweroffantasy/characters/crow.json +23 -0
  491. package/data/toweroffantasy/characters/echo.json +23 -0
  492. package/data/toweroffantasy/characters/ene.json +23 -0
  493. package/data/toweroffantasy/characters/feise.json +25 -0
  494. package/data/toweroffantasy/characters/fenrir.json +23 -0
  495. package/data/toweroffantasy/characters/fiona.json +25 -0
  496. package/data/toweroffantasy/characters/frigg.json +23 -0
  497. package/data/toweroffantasy/characters/gnonno.json +23 -0
  498. package/data/toweroffantasy/characters/hilda.json +23 -0
  499. package/data/toweroffantasy/characters/huang.json +25 -0
  500. package/data/toweroffantasy/characters/huma.json +25 -0
  501. package/data/toweroffantasy/characters/icarus.json +23 -0
  502. package/data/toweroffantasy/characters/jiyu.json +25 -0
  503. package/data/toweroffantasy/characters/king.json +23 -0
  504. package/data/toweroffantasy/characters/lan.json +25 -0
  505. package/data/toweroffantasy/characters/lin.json +23 -0
  506. package/data/toweroffantasy/characters/linghan.json +23 -0
  507. package/data/toweroffantasy/characters/liuhuo.json +23 -0
  508. package/data/toweroffantasy/characters/lyra.json +23 -0
  509. package/data/toweroffantasy/characters/meryl.json +23 -0
  510. package/data/toweroffantasy/characters/mingjing.json +27 -0
  511. package/data/toweroffantasy/characters/nanyin.json +25 -0
  512. package/data/toweroffantasy/characters/nemesis.json +25 -0
  513. package/data/toweroffantasy/characters/pepper.json +23 -0
  514. package/data/toweroffantasy/characters/plotti.json +25 -0
  515. package/data/toweroffantasy/characters/roslyn.json +23 -0
  516. package/data/toweroffantasy/characters/rubilia.json +25 -0
  517. package/data/toweroffantasy/characters/ruby.json +25 -0
  518. package/data/toweroffantasy/characters/sakifuwa.json +23 -0
  519. package/data/toweroffantasy/characters/samir.json +23 -0
  520. package/data/toweroffantasy/characters/shiro.json +25 -0
  521. package/data/toweroffantasy/characters/tianlang.json +25 -0
  522. package/data/toweroffantasy/characters/tsubasa.json +23 -0
  523. package/data/toweroffantasy/characters/umi.json +23 -0
  524. package/data/toweroffantasy/characters/yanmiao.json +25 -0
  525. package/data/toweroffantasy/characters/yanuo.json +23 -0
  526. package/data/toweroffantasy/characters/yulan.json +23 -0
  527. package/data/toweroffantasy/characters/zero.json +23 -0
  528. package/data/vrchat/groups/clubzodiac.json +2 -1
  529. package/data/vrchat/groups/dirtydancers.json +2 -1
  530. package/data/vrchat/groups/drumsynews.json +2 -1
  531. package/data/vrchat/groups/ebola20.json +2 -1
  532. package/data/vrchat/groups/internationaldancersassociation.json +2 -1
  533. package/data/vrchat/groups/kannagang.json +2 -1
  534. package/data/vrchat/groups/lolisquad.json +2 -1
  535. package/data/vrchat/groups/nekonightsnightclub.json +2 -1
  536. package/data/vrchat/groups/nekostudios.json +2 -1
  537. package/data/vrchat/groups/nekosunecommunity.json +2 -1
  538. package/data/vrchat/groups/voidclub.json +2 -1
  539. package/data/vrchat/groups/voltdanceclub.json +2 -1
  540. package/data/vrchat/groups/vrceventsgroup.json +2 -1
  541. package/data/vrchat/groups/vrchatcommunitydiscord.json +2 -1
  542. package/data/vrchat/meta.json +11 -11
  543. package/data/vrchat/players/alto.json +8 -7
  544. package/data/vrchat/players/arcadum.json +9 -8
  545. package/data/vrchat/players/chairhandler.json +8 -7
  546. package/data/vrchat/players/chipz.json +8 -7
  547. package/data/vrchat/players/hikarivr.json +10 -9
  548. package/data/vrchat/players/kimple.json +9 -8
  549. package/data/vrchat/players/linkzr.json +7 -6
  550. package/data/vrchat/players/nagzz21.json +8 -7
  551. package/data/vrchat/players/nebride.json +9 -8
  552. package/data/vrchat/players/nekosunevr.json +10 -9
  553. package/data/vrchat/players/pokelawls.json +8 -7
  554. package/data/vrchat/players/qwonk.json +7 -6
  555. package/data/vrchat/players/roflgator.json +9 -8
  556. package/data/vrchat/players/scifri.json +6 -5
  557. package/data/vrchat/players/sodapoppin.json +8 -7
  558. package/data/vrchat/players/woops.json +8 -7
  559. package/data/vrchat/players/zentreya.json +9 -8
  560. package/data/vrchat/worlds/ascensionacademy.json +6 -5
  561. package/data/vrchat/worlds/clubvgl.json +7 -6
  562. package/data/vrchat/worlds/fightfinder.json +7 -6
  563. package/data/vrchat/worlds/hangouthouse.json +6 -5
  564. package/data/vrchat/worlds/nekostudioshalloween.json +8 -7
  565. package/data/vrchat/worlds/nekostudiosnewyear.json +8 -7
  566. package/data/vrchat/worlds/nekostudiosxmas.json +8 -7
  567. package/data/vrchat/worlds/nekosunevranimations2023.json +8 -7
  568. package/data/vrchat/worlds/nekosunevranimations20232024.json +10 -0
  569. package/data/vrchat/worlds/nekosunevranimations2024.json +8 -7
  570. package/data/vrchat/worlds/nekosunevranimations2025.json +8 -7
  571. package/data/vrchat/worlds/rumhamsavatarworld.json +6 -5
  572. package/data/vrchat/worlds/smewsranch.json +6 -5
  573. package/data/vrchat/worlds/smewsroom.json +6 -5
  574. package/data/vrchat/worlds/sunsetbar.json +6 -5
  575. package/data/vrchat/worlds/theblackcat.json +9 -8
  576. package/data/vrchat/worlds/thegreatpug.json +7 -6
  577. package/data/vrchat/worlds/themidnightbar.json +6 -5
  578. package/data/vrchat/worlds/voidclub.json +6 -5
  579. package/data/vrchat/worlds/voltdanceclub.json +6 -5
  580. package/data/vrchat/worlds/vrchathome.json +7 -6
  581. package/data/vrchat/worlds/whatalegend.json +5 -4
  582. package/data/warframe/characters/amesha.json +226 -0
  583. package/data/warframe/characters/ash.json +1354 -0
  584. package/data/warframe/characters/ashprime.json +1994 -0
  585. package/data/warframe/characters/atlas.json +916 -0
  586. package/data/warframe/characters/atlasprime.json +1632 -0
  587. package/data/warframe/characters/banshee.json +1036 -0
  588. package/data/warframe/characters/bansheeprime.json +1750 -0
  589. package/data/warframe/characters/baruuk.json +555 -0
  590. package/data/warframe/characters/baruukprime.json +1298 -0
  591. package/data/warframe/characters/bonewidow.json +354 -0
  592. package/data/warframe/characters/caliban.json +434 -0
  593. package/data/warframe/characters/calibanprime.json +683 -0
  594. package/data/warframe/characters/chroma.json +1344 -0
  595. package/data/warframe/characters/chromaprime.json +1977 -0
  596. package/data/warframe/characters/citrine.json +220 -0
  597. package/data/warframe/characters/cyte09.json +218 -0
  598. package/data/warframe/characters/dagath.json +194 -0
  599. package/data/warframe/characters/dante.json +223 -0
  600. package/data/warframe/characters/elytron.json +300 -0
  601. package/data/warframe/characters/ember.json +1209 -0
  602. package/data/warframe/characters/emberprime.json +1678 -0
  603. package/data/warframe/characters/equinox.json +958 -0
  604. package/data/warframe/characters/equinoxprime.json +1644 -0
  605. package/data/warframe/characters/excalibur.json +1603 -0
  606. package/data/warframe/characters/excaliburprime.json +1524 -0
  607. package/data/warframe/characters/excaliburumbra.json +397 -0
  608. package/data/warframe/characters/follie.json +246 -0
  609. package/data/warframe/characters/frost.json +1330 -0
  610. package/data/warframe/characters/frostprime.json +1686 -0
  611. package/data/warframe/characters/gara.json +925 -0
  612. package/data/warframe/characters/garaprime.json +1494 -0
  613. package/data/warframe/characters/garuda.json +851 -0
  614. package/data/warframe/characters/garudaprime.json +1449 -0
  615. package/data/warframe/characters/gauss.json +506 -0
  616. package/data/warframe/characters/gaussprime.json +1077 -0
  617. package/data/warframe/characters/grendel.json +463 -0
  618. package/data/warframe/characters/grendelprime.json +772 -0
  619. package/data/warframe/characters/gyre.json +381 -0
  620. package/data/warframe/characters/gyreprime.json +403 -0
  621. package/data/warframe/characters/harrow.json +916 -0
  622. package/data/warframe/characters/harrowprime.json +1700 -0
  623. package/data/warframe/characters/helminth.json +220 -0
  624. package/data/warframe/characters/hildryn.json +627 -0
  625. package/data/warframe/characters/hildrynprime.json +996 -0
  626. package/data/warframe/characters/hydroid.json +1028 -0
  627. package/data/warframe/characters/hydroidprime.json +1621 -0
  628. package/data/warframe/characters/inaros.json +976 -0
  629. package/data/warframe/characters/inarosprime.json +1721 -0
  630. package/data/warframe/characters/itzal.json +465 -0
  631. package/data/warframe/characters/ivara.json +1349 -0
  632. package/data/warframe/characters/ivaraprime.json +1921 -0
  633. package/data/warframe/characters/jade.json +240 -0
  634. package/data/warframe/characters/khora.json +804 -0
  635. package/data/warframe/characters/khoraprime.json +1403 -0
  636. package/data/warframe/characters/koumei.json +221 -0
  637. package/data/warframe/characters/kullervo.json +191 -0
  638. package/data/warframe/characters/lavos.json +511 -0
  639. package/data/warframe/characters/lavosprime.json +711 -0
  640. package/data/warframe/characters/limbo.json +1363 -0
  641. package/data/warframe/characters/limboprime.json +2084 -0
  642. package/data/warframe/characters/loki.json +1209 -0
  643. package/data/warframe/characters/lokiprime.json +1457 -0
  644. package/data/warframe/characters/mag.json +2089 -0
  645. package/data/warframe/characters/magprime.json +2532 -0
  646. package/data/warframe/characters/mesa.json +1195 -0
  647. package/data/warframe/characters/mesaprime.json +2059 -0
  648. package/data/warframe/characters/mirage.json +1561 -0
  649. package/data/warframe/characters/mirageprime.json +2257 -0
  650. package/data/warframe/characters/nekros.json +1235 -0
  651. package/data/warframe/characters/nekrosprime.json +1882 -0
  652. package/data/warframe/characters/nezha.json +868 -0
  653. package/data/warframe/characters/nezhaprime.json +1695 -0
  654. package/data/warframe/characters/nidus.json +1027 -0
  655. package/data/warframe/characters/nidusprime.json +1706 -0
  656. package/data/warframe/characters/nokko.json +308 -0
  657. package/data/warframe/characters/nova.json +1226 -0
  658. package/data/warframe/characters/novaprime.json +1667 -0
  659. package/data/warframe/characters/nyx.json +1610 -0
  660. package/data/warframe/characters/nyxprime.json +1967 -0
  661. package/data/warframe/characters/oberon.json +1249 -0
  662. package/data/warframe/characters/oberonprime.json +1872 -0
  663. package/data/warframe/characters/octavia.json +1049 -0
  664. package/data/warframe/characters/octaviaprime.json +1735 -0
  665. package/data/warframe/characters/odonata.json +336 -0
  666. package/data/warframe/characters/odonataprime.json +787 -0
  667. package/data/warframe/characters/oraxia.json +246 -0
  668. package/data/warframe/characters/orionsirius.json +134 -0
  669. package/data/warframe/characters/protea.json +521 -0
  670. package/data/warframe/characters/proteaprime.json +992 -0
  671. package/data/warframe/characters/qorvex.json +214 -0
  672. package/data/warframe/characters/revenant.json +931 -0
  673. package/data/warframe/characters/revenantprime.json +1494 -0
  674. package/data/warframe/characters/rhino.json +1344 -0
  675. package/data/warframe/characters/rhinoprime.json +1675 -0
  676. package/data/warframe/characters/saryn.json +1210 -0
  677. package/data/warframe/characters/sarynprime.json +2042 -0
  678. package/data/warframe/characters/sevagoth.json +478 -0
  679. package/data/warframe/characters/sevagothprime.json +910 -0
  680. package/data/warframe/characters/siriusorion.json +247 -0
  681. package/data/warframe/characters/styanax.json +320 -0
  682. package/data/warframe/characters/styanaxprime.json +321 -0
  683. package/data/warframe/characters/temple.json +271 -0
  684. package/data/warframe/characters/titania.json +1227 -0
  685. package/data/warframe/characters/titaniaprime.json +2000 -0
  686. package/data/warframe/characters/trinity.json +1097 -0
  687. package/data/warframe/characters/trinityprime.json +1849 -0
  688. package/data/warframe/characters/uriel.json +215 -0
  689. package/data/warframe/characters/valkyr.json +1165 -0
  690. package/data/warframe/characters/valkyrprime.json +1969 -0
  691. package/data/warframe/characters/vauban.json +1067 -0
  692. package/data/warframe/characters/vaubanprime.json +1614 -0
  693. package/data/warframe/characters/voidrig.json +355 -0
  694. package/data/warframe/characters/volt.json +1324 -0
  695. package/data/warframe/characters/voltprime.json +1843 -0
  696. package/data/warframe/characters/voruna.json +293 -0
  697. package/data/warframe/characters/vorunaprime.json +431 -0
  698. package/data/warframe/characters/wisp.json +824 -0
  699. package/data/warframe/characters/wispprime.json +908 -0
  700. package/data/warframe/characters/wukong.json +927 -0
  701. package/data/warframe/characters/wukongprime.json +1754 -0
  702. package/data/warframe/characters/xaku.json +620 -0
  703. package/data/warframe/characters/xakuprime.json +879 -0
  704. package/data/warframe/characters/yareli.json +409 -0
  705. package/data/warframe/characters/yareliprime.json +683 -0
  706. package/data/warframe/characters/zephyr.json +1149 -0
  707. package/data/warframe/characters/zephyrprime.json +1864 -0
  708. package/gamecodes.js +30 -4
  709. package/index.js +5 -3
  710. package/package.json +15 -3
  711. package/scripts/cache-sync-smoke.js +50 -3
  712. package/scripts/game-code-cache-smoke.js +46 -6
  713. package/scripts/sync-api-cache.js +87 -38
  714. package/scripts/sync-game-code-cache.js +42 -3
  715. package/data/game-codes/.gitkeep +0 -0
  716. package/data/vrchat/avatars/.gitkeep +0 -0
package/gamecodes.js CHANGED
@@ -91,6 +91,31 @@ async function resolveCachedGame(game) {
91
91
  return slug;
92
92
  }
93
93
 
94
+ function hydrateCachedExpiry(entry) {
95
+ if (!entry || typeof entry !== 'object') return entry;
96
+ const output = { ...entry };
97
+ if (!entry.expiry || typeof entry.expiry !== 'object' || Array.isArray(entry.expiry)) return output;
98
+
99
+ const expiry = { ...entry.expiry };
100
+ const expiresAtMs = expiry.expiresAt ? new Date(expiry.expiresAt).getTime() : NaN;
101
+ if (!Number.isNaN(expiresAtMs)) {
102
+ const remainingSeconds = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1000));
103
+ expiry.expiresInSeconds = remainingSeconds;
104
+ expiry.expired = expiresAtMs <= Date.now();
105
+ expiry.countdown = {
106
+ days: Math.floor(remainingSeconds / 86400),
107
+ hours: Math.floor((remainingSeconds % 86400) / 3600),
108
+ minutes: Math.floor((remainingSeconds % 3600) / 60),
109
+ seconds: remainingSeconds % 60
110
+ };
111
+ } else {
112
+ expiry.expiresInSeconds = null;
113
+ expiry.countdown = null;
114
+ }
115
+ output.expiry = expiry;
116
+ return output;
117
+ }
118
+
94
119
  function filterCachedList(data, options = {}) {
95
120
  if (!data || typeof data !== 'object') return data;
96
121
  const includeUnknown = Boolean(options.includeUnknown);
@@ -104,11 +129,12 @@ function filterCachedList(data, options = {}) {
104
129
  return true;
105
130
  };
106
131
 
132
+ const hydrate = (rows) => Array.isArray(rows) ? rows.filter(matches).map(hydrateCachedExpiry) : [];
107
133
  const result = {
108
- Active: Array.isArray(data.Active) ? data.Active.filter(matches) : [],
109
- Expired: Array.isArray(data.Expired) ? data.Expired.filter(matches) : []
134
+ Active: hydrate(data.Active),
135
+ Expired: hydrate(data.Expired)
110
136
  };
111
- if (includeUnknown) result.Unknown = Array.isArray(data.Unknown) ? data.Unknown.filter(matches) : [];
137
+ if (includeUnknown) result.Unknown = hydrate(data.Unknown);
112
138
  return result;
113
139
  }
114
140
 
@@ -159,7 +185,7 @@ async function gameCode(game, code, options = {}) {
159
185
  const wanted = normalizeCode(codeValue);
160
186
  for (const bucket of ['Active', 'Expired', 'Unknown']) {
161
187
  for (const entry of Array.isArray(cached[bucket]) ? cached[bucket] : []) {
162
- if (normalizeCode(entry?.code) === wanted) return entry;
188
+ if (normalizeCode(entry?.code) === wanted) return hydrateCachedExpiry(entry);
163
189
  }
164
190
  }
165
191
  return { error: `Code "${code}" not found for game "${game}".` };
package/index.js CHANGED
@@ -31,8 +31,8 @@ function withLocalSlug(filePath, data) {
31
31
 
32
32
  // Live lookups go through the NekoSuneVR V5 API first so data can be updated
33
33
  // in real time without republishing this package. The bundled JSON under
34
- // data/ is the offline fallback and is mirrored from APINODE by the scheduled
35
- // sync workflow.
34
+ // data/ is the offline fallback and is mirrored from APINODE by the hourly
35
+ // change-watch workflow.
36
36
  async function fetchFromApi(game, type, name) {
37
37
  if (apiDisabled() || typeof fetch !== 'function') return null;
38
38
 
@@ -210,12 +210,14 @@ module.exports = {
210
210
  Players: (game, name) => load(game, 'players', name),
211
211
  Avatars: (game, name) => load(game, 'avatars', name),
212
212
  Characters: (game, name) => load(game, 'characters', name),
213
+ NPCs: (game, name) => load(game, 'npcs', name),
214
+ Maps: (game, name) => load(game, 'maps', name),
213
215
  Get: (game, type, name) => load(game, type, name),
214
216
  List: (game, type) => list(game, type),
215
217
  CacheInfo: () => cacheInfo(),
216
218
 
217
219
  // Game redeem/creator code API helpers. These also use live APINODE first and
218
- // automatically fall back to the daily mirrored data/game-codes snapshot.
220
+ // automatically fall back to the hourly mirrored data/game-codes snapshot.
219
221
  Codes: gameCodes.Codes,
220
222
  CodesStatus: gameCodes.CodesStatus,
221
223
  GameCodes: gameCodes.GameCodes,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nekosuneprojects/gaming-infos",
3
- "version": "2.2.0",
4
- "description": "Multi-game data loader for VRChat, character datasets, and game redeem/creator codes. Uses the NekoSuneVR V5 API first with automatically mirrored local JSON fallbacks.",
3
+ "version": "2.4.0",
4
+ "description": "Multi-game data loader for VRChat, Fortnite, Call of Duty, character/NPC/map datasets, and game redeem/creator codes. Uses the NekoSuneVR V5 API first with automatically mirrored local JSON fallbacks.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "engines": {
@@ -34,6 +34,16 @@
34
34
  "wuthering-waves",
35
35
  "warframe",
36
36
  "fortnite",
37
+ "fortnite-skins",
38
+ "fortnite-npcs",
39
+ "fortnite-maps",
40
+ "call-of-duty",
41
+ "cod",
42
+ "modern-warfare",
43
+ "black-ops",
44
+ "call-of-duty-maps",
45
+ "call-of-duty-characters",
46
+ "call-of-duty-npcs",
37
47
  "zenless-zone-zero",
38
48
  "tower-of-fantasy",
39
49
  "arknights-endfield",
@@ -41,7 +51,9 @@
41
51
  "groups",
42
52
  "players",
43
53
  "avatars",
44
- "characters"
54
+ "characters",
55
+ "npcs",
56
+ "maps"
45
57
  ],
46
58
  "author": "NekoSuneVR",
47
59
  "license": "MIT"
@@ -5,7 +5,12 @@ const fs = require('fs');
5
5
  const http = require('http');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
- const { syncApiCache } = require('./sync-api-cache');
8
+ const {
9
+ DEFAULT_MANIFEST,
10
+ mergeDiscoveredManifest,
11
+ validateRootSyncState,
12
+ syncApiCache
13
+ } = require('./sync-api-cache');
9
14
 
10
15
  function json(res, status, body) {
11
16
  const payload = JSON.stringify(body);
@@ -14,6 +19,40 @@ function json(res, status, body) {
14
19
  }
15
20
 
16
21
  async function main() {
22
+ assert.deepStrictEqual(
23
+ DEFAULT_MANIFEST.fortnite,
24
+ ['characters', 'npcs', 'maps'],
25
+ 'Fortnite fallback mirror must keep skins, NPCs, and maps as separate datasets'
26
+ );
27
+
28
+ const discovered = mergeDiscoveredManifest(
29
+ [
30
+ { slug: 'codblackops2', name: 'Call of Duty: Black Ops II', types: ['characters', 'npcs', 'maps'] },
31
+ { slug: 'futuregame', name: 'Future Public Game', types: ['characters', 'maps'] }
32
+ ],
33
+ { testgame: ['characters'] }
34
+ );
35
+ assert.deepStrictEqual(discovered.testgame, ['characters']);
36
+ assert.deepStrictEqual(discovered.codblackops2, ['characters', 'npcs', 'maps']);
37
+ assert.deepStrictEqual(discovered.futuregame, ['characters', 'maps']);
38
+
39
+ assert.doesNotThrow(() => validateRootSyncState({
40
+ sync: {
41
+ running: false,
42
+ syncSchemaVersion: 2,
43
+ state: { lastStatus: 'success', syncSchemaVersion: 2, requiredSyncSchemaVersion: 2 }
44
+ }
45
+ }));
46
+ assert.throws(() => validateRootSyncState({ sync: { running: true, state: { lastStatus: 'running' } } }), /currently running/);
47
+ assert.throws(() => validateRootSyncState({ sync: { running: false, state: { lastStatus: 'failed' } } }), /state is failed/);
48
+ assert.throws(() => validateRootSyncState({
49
+ sync: {
50
+ running: false,
51
+ syncSchemaVersion: 2,
52
+ state: { lastStatus: 'success', syncSchemaVersion: 1, requiredSyncSchemaVersion: 2 }
53
+ }
54
+ }), /schema catch-up is incomplete/);
55
+
17
56
  const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'gaming-infos-cache-test-'));
18
57
  const dataDir = path.join(root, 'data');
19
58
  let failCharacters = false;
@@ -24,10 +63,18 @@ async function main() {
24
63
 
25
64
  const server = http.createServer((req, res) => {
26
65
  if (req.url === '/v5/games/api/gaming-infos') {
27
- return json(res, 200, { success: true, data: [{ name: 'Test Game' }] });
66
+ return json(res, 200, {
67
+ success: true,
68
+ data: [{ slug: 'testgame', name: 'Test Game', types: ['characters'] }],
69
+ sync: {
70
+ running: false,
71
+ syncSchemaVersion: 2,
72
+ state: { lastStatus: 'success', syncSchemaVersion: 2, requiredSyncSchemaVersion: 2 }
73
+ }
74
+ });
28
75
  }
29
76
  if (req.url === '/v5/games/api/gaming-infos/testgame/meta/testgame') {
30
- return json(res, 200, { success: true, data: { name: 'Test Game', description: 'fixture' } });
77
+ return json(res, 200, { success: true, data: { slug: 'testgame', name: 'Test Game', description: 'fixture', types: ['characters'] } });
31
78
  }
32
79
  if (req.url === '/v5/games/api/gaming-infos/testgame/characters') {
33
80
  if (failCharacters) return json(res, 503, { success: false, error: 'fixture outage' });
@@ -18,21 +18,51 @@ async function main() {
18
18
  const temp = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'game-code-cache-'));
19
19
  const dataDir = path.join(temp, 'game-codes');
20
20
  let phase = 1;
21
+ let requestTick = 0;
21
22
 
22
23
  const directory = () => ({
23
24
  success: true,
24
25
  games: [
25
- { game: 'alpha', name: 'Alpha', counts: { total: phase === 1 ? 2 : 2 } },
26
- { game: 'beta', name: 'Beta', counts: { total: 1 } }
26
+ { game: 'alpha', name: 'Alpha', counts: { total: 2 }, lastUpdatedAt: '2026-08-24T10:00:00.000Z' },
27
+ { game: 'beta', name: 'Beta', counts: { total: 1 }, lastUpdatedAt: '2026-08-24T10:05:00.000Z' }
27
28
  ],
28
- aliases: { a: 'alpha', b: 'beta' }
29
+ aliases: { a: 'alpha', b: 'beta' },
30
+ // Simulates APINODE's request-time directory timestamp. This must not make
31
+ // an otherwise-identical fallback snapshot look changed.
32
+ updatedAt: new Date(Date.UTC(2026, 7, 24, 10, 10, requestTick++)).toISOString()
29
33
  });
30
34
 
35
+ function expiry() {
36
+ return {
37
+ type: 'fixed',
38
+ expiresAt: '2030-12-31T23:59:59.000Z',
39
+ local: '31/12/2030, 23:59:59 UTC',
40
+ expired: false,
41
+ // These are deliberately volatile and should be stripped by the mirror.
42
+ expiresInSeconds: 100000 - requestTick,
43
+ countdown: { days: 1, hours: 2, minutes: 3, seconds: 59 - (requestTick % 50) }
44
+ };
45
+ }
46
+
31
47
  const datasets = () => ({
32
48
  alpha: phase === 1
33
- ? { Active: [{ code: 'KEEP', status: 'active' }, { code: 'REMOVE', status: 'active' }], Expired: [], Unknown: [] }
34
- : { Active: [{ code: 'KEEP', status: 'active', notes: 'changed' }, { code: 'ADD', status: 'active' }], Expired: [], Unknown: [] },
35
- beta: { Active: [], Expired: [{ code: 'OLD', status: 'expired' }], Unknown: [] }
49
+ ? {
50
+ Active: [
51
+ { code: 'KEEP', status: 'active', expiry: expiry() },
52
+ { code: 'REMOVE', status: 'active', expiry: expiry() }
53
+ ],
54
+ Expired: [],
55
+ Unknown: []
56
+ }
57
+ : {
58
+ Active: [
59
+ { code: 'KEEP', status: 'active', notes: 'changed', expiry: expiry() },
60
+ { code: 'ADD', status: 'active', expiry: expiry() }
61
+ ],
62
+ Expired: [],
63
+ Unknown: []
64
+ },
65
+ beta: { Active: [], Expired: [{ code: 'OLD', status: 'expired', expiry: expiry() }], Unknown: [] }
36
66
  });
37
67
 
38
68
  const originalFetch = global.fetch;
@@ -52,15 +82,25 @@ async function main() {
52
82
  assert.equal(fs.existsSync(path.join(dataDir, 'games', 'alpha.json')), true);
53
83
  let alpha = JSON.parse(await fs.promises.readFile(path.join(dataDir, 'games', 'alpha.json'), 'utf8'));
54
84
  assert.deepEqual(alpha.Active.map((row) => row.code), ['KEEP', 'REMOVE']);
85
+ assert.equal(alpha.Active[0].expiry.expiresAt, '2030-12-31T23:59:59.000Z');
86
+ assert.equal('expiresInSeconds' in alpha.Active[0].expiry, false);
87
+ assert.equal('countdown' in alpha.Active[0].expiry, false);
55
88
 
56
89
  phase = 2;
57
90
  const second = await syncGameCodeCache({ dataDir, apiBase: 'https://example.test', apiPath: '/v5/games/api/codes', retries: 0, delayMs: 0 });
58
91
  assert.equal(second.success, true);
92
+ assert.equal(second.changed, true);
59
93
  alpha = JSON.parse(await fs.promises.readFile(path.join(dataDir, 'games', 'alpha.json'), 'utf8'));
60
94
  assert.deepEqual(alpha.Active.map((row) => row.code), ['KEEP', 'ADD']);
61
95
  assert.equal(alpha.Active[0].notes, 'changed');
62
96
  assert.equal(alpha.Active.some((row) => row.code === 'REMOVE'), false);
63
97
 
98
+ // Same real API data, but request-time updatedAt/countdowns change again.
99
+ // The stable snapshot must detect this as unchanged.
100
+ const unchanged = await syncGameCodeCache({ dataDir, apiBase: 'https://example.test', apiPath: '/v5/games/api/codes', retries: 0, delayMs: 0 });
101
+ assert.equal(unchanged.success, true);
102
+ assert.equal(unchanged.changed, false, 'volatile countdown/request timestamps must not create cache commits');
103
+
64
104
  const beforeFailure = await fs.promises.readFile(path.join(dataDir, 'games', 'alpha.json'), 'utf8');
65
105
  phase = 3;
66
106
  await assert.rejects(
@@ -9,9 +9,10 @@ const DEFAULT_DATA_DIR = path.join(ROOT_DIR, 'data');
9
9
  const DEFAULT_API_BASE = 'https://api.nekosunevr.co.uk';
10
10
  const DEFAULT_API_PATH = '/v5/games/api/gaming-infos';
11
11
 
12
- // APINODE currently exposes these game/type pairs. The mirror is authoritative:
13
- // after a fully successful sync, anything no longer returned by APINODE is removed
14
- // from data/ so the bundled fallback matches the API snapshot exactly.
12
+ // Safety baseline for older APINODE deployments. Newer APINODE root responses
13
+ // expose { slug, types } for every game, so the mirror automatically adds any
14
+ // newly published datasets (including the full Call of Duty catalog) without a
15
+ // package-code change. Explicit GAMING_INFOS_CACHE_MANIFEST_JSON still works.
15
16
  const DEFAULT_MANIFEST = Object.freeze({
16
17
  vrchat: ['players', 'groups', 'worlds', 'avatars'],
17
18
  genshinimpact: ['characters'],
@@ -19,7 +20,7 @@ const DEFAULT_MANIFEST = Object.freeze({
19
20
  nte: ['characters'],
20
21
  wutheringwaves: ['characters'],
21
22
  warframe: ['characters'],
22
- fortnite: ['characters'],
23
+ fortnite: ['characters', 'npcs', 'maps'],
23
24
  zenlesszonezero: ['characters'],
24
25
  toweroffantasy: ['characters'],
25
26
  arknightsendfield: ['characters']
@@ -61,11 +62,9 @@ function fallbackSlug(name) {
61
62
  return safeSlug(slug, 'derived slug');
62
63
  }
63
64
 
64
- function loadManifest(raw = process.env.GAMING_INFOS_CACHE_MANIFEST_JSON) {
65
- if (!raw) return { ...DEFAULT_MANIFEST };
66
- const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
65
+ function normalizeManifestObject(parsed) {
67
66
  if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
68
- throw new Error('GAMING_INFOS_CACHE_MANIFEST_JSON must be a JSON object');
67
+ throw new Error('Gaming Infos cache manifest must be a JSON object');
69
68
  }
70
69
  const result = {};
71
70
  for (const [gameRaw, typesRaw] of Object.entries(parsed)) {
@@ -76,6 +75,23 @@ function loadManifest(raw = process.env.GAMING_INFOS_CACHE_MANIFEST_JSON) {
76
75
  return result;
77
76
  }
78
77
 
78
+ function loadManifest(raw = process.env.GAMING_INFOS_CACHE_MANIFEST_JSON) {
79
+ if (!raw) return { ...DEFAULT_MANIFEST };
80
+ return normalizeManifestObject(typeof raw === 'string' ? JSON.parse(raw) : raw);
81
+ }
82
+
83
+ function mergeDiscoveredManifest(rootData, configuredManifest = DEFAULT_MANIFEST) {
84
+ const result = normalizeManifestObject(configuredManifest);
85
+ for (const row of Array.isArray(rootData) ? rootData : []) {
86
+ if (!row || typeof row !== 'object' || Array.isArray(row)) continue;
87
+ if (!row.slug || !Array.isArray(row.types) || !row.types.length) continue;
88
+ const game = safeSlug(row.slug, 'root game slug');
89
+ const types = [...new Set(row.types.map((type) => safeSlug(type, `type for ${game}`)))];
90
+ if (types.length) result[game] = types;
91
+ }
92
+ return result;
93
+ }
94
+
79
95
  async function requestJson(url, options = {}) {
80
96
  const timeoutMs = numberOption(options.timeoutMs ?? process.env.GAMING_INFOS_CACHE_TIMEOUT_MS, 15000, 1000, 120000);
81
97
  const retries = numberOption(options.retries ?? process.env.GAMING_INFOS_CACHE_RETRIES, 2, 0, 5);
@@ -104,6 +120,7 @@ async function requestJson(url, options = {}) {
104
120
  if (!body || body.success === false) {
105
121
  throw new Error(`API returned an unsuccessful response for ${url}`);
106
122
  }
123
+ if (options.rawEnvelope) return body;
107
124
  return body.data !== undefined ? body.data : body;
108
125
  } catch (error) {
109
126
  lastError = error;
@@ -123,6 +140,26 @@ function apiUrl(base, apiPath, suffix = '') {
123
140
  return `${normalizeBase(base)}${normalizeApiPath(apiPath)}${suffix}`;
124
141
  }
125
142
 
143
+ function validateRootSyncState(rootEnvelope) {
144
+ const sync = rootEnvelope && typeof rootEnvelope === 'object' ? rootEnvelope.sync : null;
145
+ if (!sync || typeof sync !== 'object') return;
146
+
147
+ const state = sync.state && typeof sync.state === 'object' ? sync.state : null;
148
+ const status = String(state?.lastStatus || '').toLowerCase();
149
+ if (sync.running || status === 'running') {
150
+ throw new Error('APINODE content synchronization is currently running; preserving the previous fallback until it is idle');
151
+ }
152
+ if (state && ['never-run', 'failed'].includes(status)) {
153
+ throw new Error(`APINODE content synchronization state is ${status}; preserving the previous fallback`);
154
+ }
155
+
156
+ const persistedSchema = Number(state?.syncSchemaVersion || 0);
157
+ const requiredSchema = Number(state?.requiredSyncSchemaVersion || sync.syncSchemaVersion || 0);
158
+ if (requiredSchema > 0 && persistedSchema < requiredSchema) {
159
+ throw new Error(`APINODE content schema catch-up is incomplete (${persistedSchema}/${requiredSchema}); preserving the previous fallback`);
160
+ }
161
+ }
162
+
126
163
  async function writeJson(filePath, value) {
127
164
  await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
128
165
  await fs.promises.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
@@ -196,40 +233,48 @@ function normalizedEntry(item) {
196
233
  }
197
234
 
198
235
  async function buildNextSnapshot(options) {
199
- const { apiBase, apiPath, dataDir, manifest } = options;
236
+ const { apiBase, apiPath, dataDir, manifest: configuredManifest } = options;
200
237
  const parentDir = path.dirname(dataDir);
201
238
  const nextDir = path.join(parentDir, `.gaming-infos-cache-next-${process.pid}-${Date.now()}`);
202
239
  await fs.promises.rm(nextDir, { recursive: true, force: true });
203
240
  await fs.promises.mkdir(nextDir, { recursive: true });
204
241
 
205
- const rootData = await requestJson(apiUrl(apiBase, apiPath), options);
206
- if (!Array.isArray(rootData) || !rootData.length) {
207
- throw new Error('Gaming-infos root endpoint returned no games; refusing to replace the fallback cache');
208
- }
209
-
210
- const counts = {};
211
- for (const [game, types] of Object.entries(manifest)) {
212
- const meta = await requestJson(apiUrl(apiBase, apiPath, `/${encodeURIComponent(game)}/meta/${encodeURIComponent(game)}`), options);
213
- if (!meta || Array.isArray(meta) || typeof meta !== 'object') throw new Error(`Invalid meta payload for ${game}`);
214
- await writeJson(path.join(nextDir, game, 'meta.json'), meta);
215
- counts[game] = { meta: 1 };
216
-
217
- for (const type of types) {
218
- const list = await requestJson(apiUrl(apiBase, apiPath, `/${encodeURIComponent(game)}/${encodeURIComponent(type)}`), options);
219
- if (!Array.isArray(list)) throw new Error(`Expected an array for ${game}/${type}`);
220
-
221
- const seen = new Set();
222
- counts[game][type] = list.length;
223
- for (const rawItem of list) {
224
- const item = normalizedEntry(rawItem);
225
- if (seen.has(item.slug)) throw new Error(`Duplicate slug ${item.slug} in ${game}/${type}`);
226
- seen.add(item.slug);
227
- await writeJson(path.join(nextDir, game, type, `${item.slug}.json`), item);
242
+ try {
243
+ const rootEnvelope = await requestJson(apiUrl(apiBase, apiPath), { ...options, rawEnvelope: true });
244
+ validateRootSyncState(rootEnvelope);
245
+ const rootData = rootEnvelope.data !== undefined ? rootEnvelope.data : rootEnvelope;
246
+ if (!Array.isArray(rootData) || !rootData.length) {
247
+ throw new Error('Gaming-infos root endpoint returned no games; refusing to replace the fallback cache');
248
+ }
249
+ const manifest = mergeDiscoveredManifest(rootData, configuredManifest);
250
+
251
+ const counts = {};
252
+ for (const [game, types] of Object.entries(manifest)) {
253
+ const meta = await requestJson(apiUrl(apiBase, apiPath, `/${encodeURIComponent(game)}/meta/${encodeURIComponent(game)}`), options);
254
+ if (!meta || Array.isArray(meta) || typeof meta !== 'object') throw new Error(`Invalid meta payload for ${game}`);
255
+ await writeJson(path.join(nextDir, game, 'meta.json'), meta);
256
+ counts[game] = { meta: 1 };
257
+
258
+ for (const type of types) {
259
+ const list = await requestJson(apiUrl(apiBase, apiPath, `/${encodeURIComponent(game)}/${encodeURIComponent(type)}`), options);
260
+ if (!Array.isArray(list)) throw new Error(`Expected an array for ${game}/${type}`);
261
+
262
+ const seen = new Set();
263
+ counts[game][type] = list.length;
264
+ for (const rawItem of list) {
265
+ const item = normalizedEntry(rawItem);
266
+ if (seen.has(item.slug)) throw new Error(`Duplicate slug ${item.slug} in ${game}/${type}`);
267
+ seen.add(item.slug);
268
+ await writeJson(path.join(nextDir, game, type, `${item.slug}.json`), item);
269
+ }
228
270
  }
229
271
  }
230
- }
231
272
 
232
- return { nextDir, rootGameCount: rootData.length, counts };
273
+ return { nextDir, rootGameCount: rootData.length, counts, manifest };
274
+ } catch (error) {
275
+ await fs.promises.rm(nextDir, { recursive: true, force: true }).catch(() => {});
276
+ throw error;
277
+ }
233
278
  }
234
279
 
235
280
  async function syncApiCache(options = {}) {
@@ -251,6 +296,7 @@ async function syncApiCache(options = {}) {
251
296
  try {
252
297
  const built = await buildNextSnapshot(requestOptions);
253
298
  nextDir = built.nextDir;
299
+ const effectiveManifest = built.manifest;
254
300
  const oldFiles = await walkFiles(dataDir);
255
301
  const newFiles = await walkFiles(nextDir);
256
302
  const oldHash = hashFiles(oldFiles);
@@ -264,19 +310,20 @@ async function syncApiCache(options = {}) {
264
310
  changed: false,
265
311
  contentHash: newHash,
266
312
  source: apiUrl(apiBase, apiPath),
267
- games: Object.keys(manifest),
313
+ games: Object.keys(effectiveManifest),
268
314
  counts: built.counts,
269
315
  stats
270
316
  };
271
317
  }
272
318
 
273
319
  const cacheManifest = {
274
- schemaVersion: 1,
320
+ schemaVersion: 2,
275
321
  source: apiUrl(apiBase, apiPath),
276
322
  syncedAt: new Date().toISOString(),
277
323
  contentHash: newHash,
278
324
  rootGameCount: built.rootGameCount,
279
- games: Object.fromEntries(Object.entries(manifest).map(([game, types]) => [game, { types, counts: built.counts[game] }])),
325
+ autoDiscoveredTypes: true,
326
+ games: Object.fromEntries(Object.entries(effectiveManifest).map(([game, types]) => [game, { types, counts: built.counts[game] }])),
280
327
  stats
281
328
  };
282
329
  await writeJson(path.join(nextDir, '.cache-manifest.json'), cacheManifest);
@@ -288,7 +335,7 @@ async function syncApiCache(options = {}) {
288
335
  changed: true,
289
336
  contentHash: newHash,
290
337
  source: cacheManifest.source,
291
- games: Object.keys(manifest),
338
+ games: Object.keys(effectiveManifest),
292
339
  counts: built.counts,
293
340
  stats
294
341
  };
@@ -313,6 +360,8 @@ if (require.main === module) {
313
360
  module.exports = {
314
361
  DEFAULT_MANIFEST,
315
362
  loadManifest,
363
+ mergeDiscoveredManifest,
364
+ validateRootSyncState,
316
365
  normalizedEntry,
317
366
  snapshotHash,
318
367
  syncApiCache
@@ -145,16 +145,51 @@ function validateCodeList(game, payload) {
145
145
  return payload;
146
146
  }
147
147
 
148
+ function stableDirectorySnapshot(directory) {
149
+ const stable = JSON.parse(JSON.stringify(directory));
150
+ const updatedTimes = (stable.games || [])
151
+ .map((game) => new Date(game?.lastUpdatedAt || 0).getTime())
152
+ .filter((value) => Number.isFinite(value) && value > 0);
153
+
154
+ // APINODE's root `updatedAt` describes when the HTTP directory response was
155
+ // generated, so it changes every request. Keep a meaningful stable timestamp
156
+ // based on actual per-game data updates instead.
157
+ if (updatedTimes.length) stable.updatedAt = new Date(Math.max(...updatedTimes)).toISOString();
158
+ else delete stable.updatedAt;
159
+ return stable;
160
+ }
161
+
162
+ function stableCodeRow(row) {
163
+ const stable = JSON.parse(JSON.stringify(row));
164
+ if (stable.expiry && typeof stable.expiry === 'object' && !Array.isArray(stable.expiry)) {
165
+ // These values are calculated from Date.now() on every API request. The
166
+ // fallback retains expiresAt/local/expired, while consumers can calculate
167
+ // a live countdown themselves when needed.
168
+ delete stable.expiry.expiresInSeconds;
169
+ delete stable.expiry.countdown;
170
+ }
171
+ return stable;
172
+ }
173
+
174
+ function stableCodePayload(payload) {
175
+ const stable = {};
176
+ for (const bucket of ['Active', 'Expired', 'Unknown']) {
177
+ if (Array.isArray(payload[bucket])) stable[bucket] = payload[bucket].map(stableCodeRow);
178
+ }
179
+ return stable;
180
+ }
181
+
148
182
  async function buildNextSnapshot(options) {
149
183
  const { apiBase, apiPath, dataDir } = options;
150
184
  const nextDir = path.join(path.dirname(dataDir), `.game-codes-cache-next-${process.pid}-${Date.now()}`);
151
185
  await fs.promises.rm(nextDir, { recursive: true, force: true });
152
186
  await fs.promises.mkdir(nextDir, { recursive: true });
153
187
 
154
- const directory = await requestJson(apiUrl(apiBase, apiPath), options);
155
- if (!Array.isArray(directory.games) || !directory.games.length) {
188
+ const rawDirectory = await requestJson(apiUrl(apiBase, apiPath), options);
189
+ if (!Array.isArray(rawDirectory.games) || !rawDirectory.games.length) {
156
190
  throw new Error('Game code directory returned no games; refusing to replace fallback cache');
157
191
  }
192
+ const directory = stableDirectorySnapshot(rawDirectory);
158
193
  await writeJson(path.join(nextDir, 'directory.json'), directory);
159
194
 
160
195
  // /status is cache-only on APINODE and does not trigger source website requests.
@@ -167,10 +202,11 @@ async function buildNextSnapshot(options) {
167
202
  const delayMs = numberOption(options.delayMs ?? process.env.GAME_CODES_CACHE_GAME_DELAY_MS, 150, 0, 10000);
168
203
  for (let i = 0; i < directory.games.length; i += 1) {
169
204
  const game = gameSlug(directory.games[i]);
170
- const payload = validateCodeList(
205
+ const rawPayload = validateCodeList(
171
206
  game,
172
207
  await requestJson(apiUrl(apiBase, apiPath, `/${encodeURIComponent(game)}`, { includeUnknown: 'true' }), options)
173
208
  );
209
+ const payload = stableCodePayload(rawPayload);
174
210
  await writeJson(path.join(nextDir, 'games', `${game}.json`), payload);
175
211
  counts[game] = {
176
212
  active: payload.Active.length,
@@ -214,6 +250,7 @@ async function syncGameCodeCache(options = {}) {
214
250
  gameCount: built.directory.games.length,
215
251
  counts: built.counts,
216
252
  statusIsCacheOnly: true,
253
+ volatileFieldsRemoved: ['directory.updatedAt(request-time)', 'expiry.expiresInSeconds', 'expiry.countdown'],
217
254
  stats
218
255
  };
219
256
  await writeJson(path.join(nextDir, '.cache-manifest.json'), manifest);
@@ -239,6 +276,8 @@ if (require.main === module) {
239
276
  }
240
277
 
241
278
  module.exports = {
279
+ stableDirectorySnapshot,
280
+ stableCodePayload,
242
281
  syncGameCodeCache,
243
282
  validateCodeList
244
283
  };
File without changes
File without changes