From 091afc2b5c8b79bdc3c7f04dfbeb48097b55fa25 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Wed, 29 Jul 2026 20:52:11 +0900 Subject: [PATCH] asdfg --- .achievement_data/state.json | 1 + .gitignore | 9 - .htaccess | 15 +- .vscode/settings.json | 3 + ACHIEVEMENT_AUDIT.md | 268 -- AI_MAP.md | 112 - BUILD_NOTE.txt | 1 - FILES.json | 191 -- GAME_FEATURES.md | 93 - README.md | 95 +- achievement_api.php | 1564 ++++++--- app_manifest.json | 3 +- assets/ui/pwa-icon-192.png | Bin 0 -> 26366 bytes assets/ui/pwa-icon-512.png | Bin 0 -> 119075 bytes css/mobile.css | 20 + index.html | 373 +-- js/achievement_catalog.js | 724 ++++ js/achievements.js | 1975 +++++++---- js/circuit_board_system.js | 93 +- js/domain_ids.js | 4 +- js/game_dialogs.js | 13 +- js/item_environment_hazard_system.js | 2 +- js/item_lifecycle_step_frame.js | 2 +- js/mechanical_system.js | 4 +- js/placement_preview_system.js | 8 +- js/save_codec.js | 158 +- js/save_schema.js | 4 +- js/save_system.js | 29 +- js/signal_system.js | 60 +- js/snapshot_system.js | 239 +- js/structures.js | 1 + js/tarinai_direct_feeding_system.js | 21 +- js/tarinai_update_policy.js | 34 +- js/ui_bind.js | 12 +- js/ui_ground.js | 2 +- js/ui_layout_dialogs.js | 84 +- js/version.js | 4 +- js/world.js | 10 +- js/world_event_effects.js | 6 +- js/world_placement_log.js | 55 +- manifest.webmanifest | 27 + router.php | 24 + ...ievement_audio_tooltip_regression_audit.js | 22 - scripts/achievement_audit.js | 709 ---- scripts/achievement_integration_audit.py | 128 - scripts/achievement_save_v6_audit.js | 47 - scripts/achievement_save_v8_audit.js | 47 - scripts/achievement_server_state_audit.py | 341 -- scripts/ai_inventory.ps1 | 124 - scripts/ai_inventory.py | 212 -- scripts/colony_crisis_regression_audit.js | 44 - scripts/effect_runtime_regression_audit.js | 18 - scripts/generate_app_files.py | 52 - scripts/generate_item_icons.py | 146 - scripts/history_binary_regression_audit.js | 46 - .../long_session_runtime_regression_audit.js | 84 - ...on_tarinai_performance_regression_audit.js | 21 - .../object_render_cursor_regression_audit.js | 25 - ...een_motion_performance_regression_audit.js | 16 - scripts/pathfinding_regression_audit.js | 58 - scripts/performance_5to7_regression_audit.js | 58 - ...rformance_architecture_regression_audit.js | 28 - .../performance_cleanup_regression_audit.js | 77 - scripts/playstyle_achievement_audit.js | 232 -- .../population_recovery_regression_audit.js | 194 -- scripts/regression_check.py | 2944 ----------------- scripts/render_stack_regression_audit.js | 16 - ...oduction_memory_sprite_regression_audit.js | 26 - scripts/reproduction_regression_audit.js | 27 - .../secondary_achievement_regression_audit.js | 119 - scripts/self_zunchi_regression_audit.js | 129 - .../social_interaction_regression_audit.js | 23 - ...ltip_nail_colony_focus_regression_audit.js | 23 - scripts/ui_grass_limits_regression_audit.py | 85 - service-worker.js | 169 +- start_server.bat | 6 + start_server.sh | 5 + 77 files changed, 4413 insertions(+), 8231 deletions(-) create mode 100644 .achievement_data/state.json delete mode 100644 .gitignore create mode 100644 .vscode/settings.json delete mode 100644 ACHIEVEMENT_AUDIT.md delete mode 100644 AI_MAP.md delete mode 100644 BUILD_NOTE.txt delete mode 100644 FILES.json delete mode 100644 GAME_FEATURES.md create mode 100644 assets/ui/pwa-icon-192.png create mode 100644 assets/ui/pwa-icon-512.png create mode 100644 js/achievement_catalog.js create mode 100644 manifest.webmanifest create mode 100644 router.php delete mode 100644 scripts/achievement_audio_tooltip_regression_audit.js delete mode 100644 scripts/achievement_audit.js delete mode 100644 scripts/achievement_integration_audit.py delete mode 100644 scripts/achievement_save_v6_audit.js delete mode 100644 scripts/achievement_save_v8_audit.js delete mode 100644 scripts/achievement_server_state_audit.py delete mode 100644 scripts/ai_inventory.ps1 delete mode 100644 scripts/ai_inventory.py delete mode 100644 scripts/colony_crisis_regression_audit.js delete mode 100644 scripts/effect_runtime_regression_audit.js delete mode 100644 scripts/generate_app_files.py delete mode 100644 scripts/generate_item_icons.py delete mode 100644 scripts/history_binary_regression_audit.js delete mode 100644 scripts/long_session_runtime_regression_audit.js delete mode 100644 scripts/non_tarinai_performance_regression_audit.js delete mode 100644 scripts/object_render_cursor_regression_audit.js delete mode 100644 scripts/offscreen_motion_performance_regression_audit.js delete mode 100644 scripts/pathfinding_regression_audit.js delete mode 100644 scripts/performance_5to7_regression_audit.js delete mode 100644 scripts/performance_architecture_regression_audit.js delete mode 100644 scripts/performance_cleanup_regression_audit.js delete mode 100644 scripts/playstyle_achievement_audit.js delete mode 100644 scripts/population_recovery_regression_audit.js delete mode 100644 scripts/regression_check.py delete mode 100644 scripts/render_stack_regression_audit.js delete mode 100644 scripts/reproduction_memory_sprite_regression_audit.js delete mode 100644 scripts/reproduction_regression_audit.js delete mode 100644 scripts/secondary_achievement_regression_audit.js delete mode 100644 scripts/self_zunchi_regression_audit.js delete mode 100644 scripts/social_interaction_regression_audit.js delete mode 100644 scripts/tooltip_nail_colony_focus_regression_audit.js delete mode 100644 scripts/ui_grass_limits_regression_audit.py create mode 100644 start_server.bat create mode 100644 start_server.sh diff --git a/.achievement_data/state.json b/.achievement_data/state.json new file mode 100644 index 0000000..6939f24 --- /dev/null +++ b/.achievement_data/state.json @@ -0,0 +1 @@ +[4,1784091728,["39.16.75","39.16.105","39.16.123","39.16.60","39.16.74","39.16.137","39.16.104"],["first_birth","natural_zunchi_slave","natural_tarinai_king","self_zunchi_death","zunchi_collision_death","natural_zunchi_slave_5_generations","natural_tarinai_king_3_generations","death_50_in_10_seconds","birth_50_in_60_seconds","great_mother_1000_births","lifespan_completed","soccer_ball_death","fight_pair_danger_kill","all_non_sleep_diseased_25","colony_happy","direct_feed_33","ignite_during_birth_ritual","laxative_starvation","idle_observer_5_minutes","placed_objects_100","mechanized_industry","safe_colony_25_5_minutes","ants_alive_25","ants_killed_100","secret_collection_9_slots","pause_spam_4_in_1_second","continuous_play_1_hour","below_absolute_zero_item","undo_mass_revival","robot_cleaner_100","held_30_seconds","minimalist_happy","overprotective","self_sufficient","unplanned_city_30","sauna_cold_plunge","rain_shelter_all","medicine_ledger_all","mercury_lifespan","enemy_enemy_friend","revolution","fuel_to_fire","undo_20","redo_20","eternal_history_generation_10","king_full_satisfaction","slave_zero_satisfaction","town_doctor_50","poke_plushie_fling","chaos_seeker_666_fights","clean_freak_robot_only","true_tarinai_observer","sniper_333_shots","megalopolis","fertility_seeker_721_love_births","low_fps_single_digit","continuous_play_24_hours","sandbox_five_toilets","park_ground_changed","ground_change_4_in_1_second","ant_nest_without_tarinai","sticky_bomb_15_passes","daily_play_7_days","wire_shock_7_tarinai","information_industry","strength_in_numbers","elite_few","zunchi_overflow","comfortable_beds","stone_pillow","across_seasons","favorite_one","statistician","well_informed","lively_making","memento_mori","mad_scientist"],[["AT7UX0vQQHSjDSsx7ntrcA",115523,0,4],["AXHGXDetQkWuJ2vrhLVrsw",191206,599],["AZOq9iLdRLiVs_J1HNcdNg",26632,311131,1],["AgioecJAT9-DyKM9rBWwFg",354351,233,1],["An-kM9_1Rkuiu0eIHnJROw",24687,416999,5],["A9UhC-axRtWKy4Vw8NRLjA",225077,15],["BmyezTENRtm5ekkH3z99qg",289868,146256,5],["BsqXI2J_RTCLzl9rK-WTVQ",201094,16],["B5myaZslQwSnpkvoGpknsA",37613,1800,3],["C0tajnOTT1K0FgIfEGVsGA",256959,2973],["C4GRUxmdRbWZzHJ0V9JpsQ",17432,357474,2],["C8XqqJ5rTHa6qw-z8Y88jA",365330,744,1],["DFKBeF9-RpKfjhRhQx2ZRg",174267,14974],["DGPMjLtqRaCtHIptLtkjdw",221295,2546],["DyaOro6TQ5G_TeHnltFK0g",67643,146960],["EGSebCuwSk6FKdPePBeDOg",118168,1199,4],["EgkHNUQJQPat3FM4ZIU59w",156003,0],["EyToTt3-RX26J-6eJ-xpPw",191733,6600],["E0HXsU6-RYCO91MK9M07bw",9762,104722,4],["E2ldSdcxQN-zZ9xTBzDE1g",368929,299,2],["Gr93GoiFQkGMHKs2Mw4AaQ",97655,9130,3],["G2o3yC0MTIKMLhDk2o6YDA",245190,7797],["HYkDqmCsS8aiMrUThI4fcQ",201617,13],["Hns48Q0MQJ2H-lFEmwxfQw",23523,603,3],["HwT0Sk-XSGahzteQS7FPzQ",287737,92777,2],["H5ZDYEFvQ0e-3JC0xPTx-Q",27702,421,3],["ITRYJzo5QYaSkjLYaaJ6Yg",40636,46882,3],["ItNN0heHRqiNHc-OoGoSMA",297825,1298,1],["JuwFpfb1QJeYfGvWpzy3IA",63369,126598],["J7uf1v88T3mSFs1ptKO9RQ",120239,25],["KSZ59taVRJmYmVYVhe99hg",77159,1800,3],["KwJlifxVSh-GUDpUNABzrw",369833,308,2],["LDAWke6kRbutWe3r9dfLdQ",318649,776,1],["Li2NtIyOQpGHzFaEovEoYg",100406,0,4],["MEMut_ufQsmx1rPi45G81Q",36627,395276,2],["Mpv6WJHGTw6YwQQ70mk7BA",13479,80,3],["Nf-0jB-JQ0-UZnRWPHS98A",300640,4006,1],["Nhx8JhVfTOWAYRt1vRuQow",113724,74645],["NjK9LbETSzOX-LpfqL0nxg",255226,103273,1],["Nj7jeXEWTXa6Sm5VPRlmEg",206684,123],["NkwzEL98Rzi4GwdSd5A3ng",20301,0,3],["NuaRm_fYR7Sz8nVs4P4yCg",114763,34,4],["Nvzk_2oQQbawaZca4Pww2Q",291531,73544,1],["NxW5_hQ1SoO5iSuv66RCfg",270726,299,1],["Nx2nfW9iRpeJXcFw7IOdIA",283823,976,1],["QD38IUY8RJeCcN0xwmTyTQ",269572,9325,1],["QQx3jG6FR2-TJx80sIlCUA",241410,16],["QoCQg7HPQYKYtzZY6V5tTQ",375227,2400,2],["Q-YwoP68T6anCkkhU6gLAg",234541,3300],["RB-Tlne5T4OxgjAQO0_ysg",381794,740,2],["RB_DGMVqRV-QbX2uFDTRpA",365383,1322,1],["RVu0RBgqRbipwYNU50e9qQ",343860,0,1],["RrNxNSRsSiKVxmeMQo6lPg",433513,222,2],["R6bORyS0QYGxKejqyoB10A",303512,14,1],["SG0ktBVwR5Os_MDzYJoawQ",39040,599,3],["SfZYmhpyRZCwrqBiw9kqUA",132898,357],["SjYuiyoHTeivYcMyKoRGNg",337249,104061,5],["S5DpVCRaThahOqTSMlWPDA",89164,337690,2],["TUKrrmrET2ypgp5DaU4tlA",18426,0,3],["T9GkyS2HSqKbKoqMiC7WXg",47094,16,3],["URXE_032StGQlEl3V17HXQ",374574,2899,2],["UdLsI4uyQ_S-XN7a4N5CvQ",34688,392712,2],["UqaJ9nvhQl-eT-Bey1m8Rw",16731,420407,5],["U2UehYHxQ5248yI8fy7_iw",145756,39],["VRG1ggLhStGW9MXAb65Btw",31668,17,3],["WQ3Ku6CJTWaCLabPhA951w",363389,625,1],["W_G-e_6AT4qICRL-Hz05sA",119138,14,4],["XNVK7HGDTMiNp8dsEPgM2A",52741,13,3],["XdDGg7MrTG2ZNpPIVpk3VA",119422,14,4],["YIXXT1hTSrS0ESGYH59www",422182,1798,2],["YKJXT2URTSydN9aUmkJOKA",179869,1221],["YYaufcmaQ4GiD9Q_MZuMWw",299654,335,1],["Yads3JcpTMymeKumAQcF6Q",362480,16,1],["YkBYIYwHRYOwkX0fmeqT4w",249642,299],["ZBBiN426T3WEYxVBXituGQ",90719,15103,4],["ZHedzwwERgmbHhpZuIEoeg",110056,94,4],["ZO4ZHBKxRyu5IlOdEiaMjQ",173877,0],["Z2KTakqhSTuWysEEAKyGXA",195554,109463,1],["a7UWcDOyToOQycGTu33MeQ",214800,148558,1],["bFoT0vGfRSapP6saKj_L6g",423544,3600,2],["bP42-AJoR4CDwWnCKWHocQ",232663,300],["bcxw7HujQFi5Y1YOMwQOfA",121952,23511],["b-Q9bhMZTHGa_blAGuZseQ",341953,599,1],["cnupw5mWSfS4hwLnV1L2XA",248428,106847,1],["csNqBKPtRNe25Pr757SKaA",203517,9051],["dQ3Y_1tPRZ6oPIRWPKo4aQ",194363,0],["dqOnFoBXTlqwDr8C2mv5yw",25033,404829,2],["eKNNK69YS6-iu0qhfQ6p9A",125751,14],["eifTn_kTRgSMWj6KYbalIg",100380,14,4],["fGJd8BQqRkai1XTM6B7nEw",13459,279970,1],["fJLe4FHgS2O9b-hFtG9Ldw",344682,594,1],["gAlvRzRXTLOxqgdwcRxLQw",421603,301,2],["gMC_86VbT7uIH8B0fdKxlA",14550,0,3],["gVlK1ilNRPeMIPUAA-JRtQ",221160,13],["gcRLtucSTrGWq7WYznfTmQ",290969,899,1],["i8Ff_VXpTFORsOhh4jZHbQ",280665,51,1],["jAw6RpvST3OEMAvbeH77LA",240387,221],["j-mNs4bdSI2XqAhGb4p3Hw",88836,20,3],["kBFvt4RRSai3DmGuyfgf6w",19817,2574,3],["kUEPr6JJTz6mCwWJDLv-aQ",115199,2400,4],["mlBIJs6mT52AX2Ahpgh7sw",196651,3934],["mswKVgE5Qxqta4NAHvVkoQ",437951,14,5],["nTWtsnADSv648AxbFz4Lcg",212355,9300],["nVUbi4OmT6-J9byg8-q97g",32001,1123,3],["nxblVyMLTrSZAkfPf2FWwg",383336,26486,2],["n-SY48K2QeqzCkb7yTZPpQ",259675,1200],["n_tcf3j7S6yBYt2Y7yM85A",293624,169,1],["odJ_h7oUS1yB_7p6LB4w7w",112982,342,4],["oe139e_bR4WtnCcqSe2jBQ",332048,0,1],["o4yHwSx2SX24OE9qPkJHsw",432488,825,2],["pQFs21PfTXuXeaf1tEVdMQ",129750,4545],["qTwJtmQESIC3jAtmz_8sTg",423097,13,2],["qnK6RnesT2igW_HcZssIdA",97976,334829,2],["qs3rnavAT4acYZvaEeI5oQ",129913,1199],["qxGFhkHZTduV9YYYWyDSUg",147480,273846,2],["r75L6GPkR4uVqBqO_1aSyA",355137,2246,1],["sSCrwsqiSyqBVgz7Matzww",299588,101812,2],["s3O6OZTBRz6O0TUBQC64QA",23489,245,3],["s_uTFGOTQdKTmWoJ9Vbzpg",24710,357292,2],["tMDzFziXTJ2CYoBLWq9H9Q",8579,433201,5],["tWF1yvWnTfOaaaBAbXwtPA",29003,17,3],["taU6vA6PTjWhKC6V43YplA",24383,411287,2],["t2UqLSrEREKy0Ai8JSYKGQ",203662,113],["t7tpQzqNRV6-tLSbunsDfg",302098,133,1],["t_SyOC8gTi218xAe4vOGgw",382434,7684,2],["uAtfx_GBRvmJCP6E9UWmzg",100421,15175,4],["uU2jxY3bRBe7AtfHRFGSIA",34810,299,3],["uqFbOGx4S_6ZvjcQrWE2Bw",26634,346405,2],["v1K6sNSCRdWXvmk8Jl91sQ",20955,298322,1],["wBeR6TKiQ1SnWlxO6HjxUg",208309,1199],["wF4aj-hSTl-BSuCbVRywOA",429094,2999,2],["wK3oJUoHT5Kb0jeLIzFJJQ",184790,16],["wM8XiYo0R9SEcCkDjO2bxQ",390457,1499,2],["wQwjJN_pSSOryn69Y8lqyA",16767,418804,2],["when_J1OTpScd-vaA2i6xw",341088,309,1],["xhjNabCzQHGEWRw9eEFAYQ",101690,275766,2],["xnEBJGKVSSC7--M0K_e2JA",364391,1764,1],["yYHnh-RxQaOcASVbQj6gOg",297672,2435,1],["yp2M01VvTle5xgXu7ZNzqg",204919,14],["ys4oahR7TDm9WW5zOKyShA",30073,1310,3],["zJtKDIeiTM2oegMGMt3nvg",343271,0,1],["zLFzVsWhSTOYU6rJsMsi1Q",20602,1472,3],["zOC7w26xQYiGnLQy3x504A",57982,44,3],["zmEzLb-1Q6m08t7dLiqFdw",11424,599,3],["zn1ecYMwRNKrJukPfPFoXQ",103760,282772,2],["zsuxu35fRq6bIZCvcC3iag",106311,285,4],["0Ekuj3W8Qpm_EwKpSXYdXw",377873,19,2],["0KpzjsoqQYaN_S2X8ySd7w",22236,352889,2],["0ia10T1pSZmAKukutrEaIA",31969,87661,4],["0lcgISlzQEej3cvhI-e-Ww",289089,82410,2],["0y4SH2FXRYGK1d0FCkOUjw",378025,6741,2],["00Z1M7PFTqq9iwnnCxiZTA",269597,166,1],["1dKF4NRHRfaJtfQr4lafCg",40381,1201,3],["2Bdkm5NaSY2lY1VMj_1Mqw",69602,16,3],["2jtrVkYNQtGpp7Y2wSOwZQ",304589,74,1],["2rseIRu9RAuvUuNOsSO_-Q",26626,18,3],["28TEqRx1QuK-TmIYSLzL1Q",34802,0,3],["3E-4jv0QSPq33RbRg4B3Aw",423829,7130,2],["3XJWUCjdRgSxmd5Z1hfDcw",16826,420725,5],["3cXtHb7yRQSBcQK7yx-_zw",232365,465],["3mSl6ER2RtSj9a0p-sNDMA",430880,299,2],["4JrymWEKThuc99N1_G1_rQ",11020,430720,5],["4XM-EZ63SiSRpbvcIqdZyw",133745,183813,1],["4u-CXB7uTwuNWoFuQ2ZO9g",196903,14],["5aQrgazaTe2oNgyFnFVOFA",25453,8280,3],["5c9NjVEiSOGVeFiQ9tdHYg",217415,3322],["5woLhtEbSPuhPazSSQoluA",197842,0],["6kdvBVYWSuuyehSCCPGLYw",11195,237,3],["6ryKJq12TO-2vFdLKAIPXw",100122,163,4],["7587mqPLR3iJGXGBXjcDIQ",194177,56975],["8M_zxblPSkGw-_mE0gJUXQ",364739,2101,1],["9OZ_7xz9SkS56dk0cpJsLA",279830,659,1],["9UnXYYzZSYiFRDOzfNeCvw",264541,6248,1],["9gX-Lr9aTTKqMIA4Mb5MGg",28665,254163,1],["9pgiN8b5R8CzMX_G28g_Xg",388466,3300,2],["9uEITVHeQXegRVD6lXnz8w",297708,5964,1],["90eBgHt_S0Gai4x7q3f8eA",10519,253386],["92hhaxj2R1mlus6Ck9XBPQ",268366,15,6],["-Haa3Cb8RzO-mee5e4tggA",147060,1287],["-Rrwi9qrTv2pEbUgjIPivw",142099,14],["-3NMp6qyR9q05KN8ZX48Dg",33123,33796,3],["_MZl_4kLSDGUuDuNMl1-6g",369100,2265,2],["_QIygAxARMO5xTL3TO8SKg",90452,14,3],["_66f6qhySiOVSH5VcwD-kQ",293656,137,1],["_7nqDabvQF2tbBmkp4eb6Q",386660,0,2]],[[0,1,191271,2,26643,3,354359,4,24701,5,225088,6,289885,7,201106,8,37620,9,256968,10,17452,11,365338,12,174277,13,221305,14,67654,15,118185,17,191745,18,9776,19,369026,20,97667,21,245351,22,201626,23,23539,24,287749,25,27715,26,40652,28,63416,29,120261,30,77482,31,369844,32,318670,34,36639,35,13497,36,301783,37,113723,38,358493,39,206694,41,114776,42,291542,43,270735,45,269660,46,241420,47,375235,48,234562,49,381817,50,365395,52,433544,53,303521,54,39051,55,132912,56,337258,57,89223,59,47106,60,374592,61,34699,62,16742,63,145791,64,31682,65,363398,66,119148,67,52752,68,119433,69,422209,70,179877,72,362490,73,249681,74,90750,75,110092,77,195567,78,214810,79,423559,80,232673,81,121976,82,341963,83,248473,84,209959,86,25044,87,125762,88,100391,89,13473,90,344691,91,421843,93,221169,94,290981,95,280684,96,240401,97,88854,98,20420,99,115231,100,196666,101,437964,102,212360,103,32024,104,383388,105,259692,106,293635,107,112992,109,432574,111,423110,112,97987,113,129925,114,147489,115,355186,116,299605,117,23525,118,24722,119,20,120,29018,121,24421,122,203672,123,302108,124,382456,125,100417,126,34821,127,26661,128,20967,129,208332,130,429106,131,184801,132,390470,133,16796,134,341114,135,101701,136,364515,137,297724,138,204929,139,30097,141,20613,142,58021,143,11514,144,103781,145,106348,146,377892,147,22247,148,31993,149,289102,150,379181,151,269607,152,40388,153,69615,154,304597,155,26641,157,424098,158,16839,159,232374,160,430897,161,11034,162,192807,163,196695,164,25878,165,217502,167,11258,168,100136,169,194187,170,364747,171,279840,172,266280,173,28677,174,388528,175,297712,176,10531,177,268378,178,147110,179,142110,180,33135,181,369117,182,90463,183,293665],[1,1,191403,2,26877,4,25011,6,290244,8,38012,10,135101,11,365564,12,174521,13,221461,14,70866,15,118434,17,194565,18,10436,19,369151,21,245755,25,27750,26,40966,28,63838,30,78953,31,369987,34,271072,36,302091,42,292625,45,269918,47,375662,48,234981,49,382343,50,365969,56,337683,61,35564,62,17369,65,363850,70,180343,73,249848,74,93618,77,281773,78,214997,81,145234,83,248720,84,210906,86,25873,89,13762,90,344778,98,21616,99,115359,100,196869,102,214588,103,33124,106,293788,109,432827,113,130388,114,419890,115,355381,116,300912,118,24984,119,25211,124,385030,126,35004,128,21266,129,208665,130,429479,135,377244,139,30472,141,20849,144,290828,147,374711,149,371387,150,380038,157,425942,158,17350,159,232638,161,11255,169,251142,172,266492,173,200146,174,388714,175,298220,176,116422,180,33438,181,370575],[2,1,191761,2,26936,4,25652,6,290128,10,135093,11,365556,12,174506,13,222661,14,204836,15,119256,17,194663,18,112037,21,245876,26,41195,28,63729,36,302040,42,292786,45,269974,47,375721,48,234981,50,366237,56,337503,61,35520,62,17526,65,363886,70,180169,74,96379,77,282412,81,145315,83,248754,84,211047,86,25686,89,13752,90,344804,99,115637,100,197049,102,216835,113,130997,114,419917,115,355380,118,25605,119,25371,124,383289,128,21230,130,429474,135,377269,139,30496,141,20917,144,385343,150,379732,158,17595,159,232666,161,11354,171,280197,172,266472,173,200160,174,389034,175,298714,176,116572,181,370591],[5,74,101510,89,120349,128,22252,173,200261],[6,2,27153,12,174657,14,207834,89,13796,128,21666,173,200632],[7,1,191426,4,26737,6,290782,8,38242,9,259927,14,83478,23,24000,25,28120,30,78409,32,319011,34,38178,37,188014,43,270914,47,376773,49,382530,54,39411,61,35165,62,18098,74,91322,77,283158,81,122210,82,342359,83,249109,89,15076,102,212507,104,408595,105,260129,115,356759,121,25152,124,382604,127,27380,128,55608,129,209435,133,108888,135,147785,136,365613,137,299395,141,21485,149,289208,152,41570,157,424893,158,17870,161,280009,162,235865,170,364915,172,268642,173,113688,175,297802,176,10914,181,369796],[8,1,191304,2,27344,4,24911,6,290372,8,37848,9,257230,11,365879,12,174714,14,67718,17,197179,18,113098,21,245454,23,23612,25,27763,26,41186,30,78269,32,318871,34,36704,37,187874,42,294092,47,375996,48,235366,49,382263,54,39237,57,89275,61,34845,62,16821,74,90789,77,195597,78,294671,82,342519,83,248882,86,25562,89,14016,90,345085,100,198235,102,213131,104,408208,105,259742,112,98505,115,357048,119,267855,121,24568,124,382481,127,26894,128,22228,129,209359,132,391324,133,108196,136,365559,137,298989,141,21239,148,114644,149,290977,150,379324,157,424728,158,17014,161,11623,162,235545,164,27402,165,217591,167,11431,172,266558,173,28743,174,390487,175,297945,176,10852,181,369155,183,293764],[9,4,24947,14,124595,21,251727,34,254051,89,204437,121,24568,133,108196,158,20008,172,268084],[10,10,374646,11,365856,18,29044,21,246093,36,302622,42,293318,45,270168,61,35664,70,181083,74,94416,77,282919,84,211479,89,14055,99,116043,102,218148,115,355704,116,303469,124,384432,128,55963,130,431869,157,430786,158,24354,172,265911,173,282338,174,390520],[11,4,26736,17,192182,34,120809,36,304372,47,376752,48,237261,49,382325,61,426657,62,17177,74,92754,77,283423,83,249108,100,197742,102,213730,112,98720,114,420746,124,385160,128,318806,136,365447,137,299353,144,289095,149,290481,158,20965,160,431153,161,280037,172,264711,173,114102,175,300912],[12,1,191425,4,24722,6,290787,8,38527,10,374316,14,121713,17,196675,18,107315,20,97778,21,252728,23,24000,25,28120,30,78592,32,319011,34,108438,36,303508,37,113790,42,291912,47,376820,49,382397,50,365551,54,39407,56,337608,57,90476,61,206402,62,16919,74,91648,77,281947,78,294736,81,122083,82,342093,83,353761,84,211929,89,43353,94,291137,96,240608,100,197078,102,212515,103,32514,105,260264,107,113267,113,130585,114,419848,116,300962,124,382645,127,27423,128,55609,132,391032,133,108887,135,147784,136,365614,137,299469,143,11706,144,104301,145,106594,147,374763,149,289257,150,384351,151,269757,152,41479,157,425607,158,18012,161,110764,162,235879,168,100284,172,270247,173,113699,175,297801,176,10593,178,147214,181,370718],[13,4,24758,14,71230,34,242398,48,235358,61,35471,74,98685,133,108887,158,78115],[14,45,273422,78,215806,84,204504,110,132675,116,401030,124,383690,130,431406,136,365236,158,74280,175,298584],[16,4,26004,8,38458,14,210165,17,196587,18,10744,20,98085,25,27833,42,291833,47,376752,48,236419,54,39421,57,303665,62,18071,74,91316,77,301630,78,294675,81,122023,83,355044,94,291093,96,240572,102,213445,104,383590,115,357109,116,398245,118,380402,124,384746,128,56573,135,376985,137,299172,148,32382,157,425513,158,17750,161,11805,175,297873,181,369640],[17,3,354469,4,25296,6,290190,12,175855,14,103020,17,194491,28,64429,30,78286,31,369999,34,182984,54,39586,55,133085,57,302318,61,207064,62,179559,74,92764,77,282434,78,286253,82,342039,83,248781,89,42902,100,197667,102,212649,109,432657,112,98249,113,130070,114,420504,116,300601,118,25397,121,26268,124,383302,126,35081,130,431627,132,391584,136,365374,137,299921,143,11632,157,429460,158,21084,161,11375,173,113883,175,300479],[18,4,25332,10,135105,11,365623,12,175107,14,71139,17,196976,18,112078,21,245597,36,304490,42,292808,45,272914,48,234998,56,346890,61,206011,70,180164,73,249874,74,99353,77,284595,78,215864,81,145286,86,429778,89,13788,90,344982,98,21613,99,115373,100,198070,102,212467,105,259888,115,355780,119,25912,121,26268,124,388453,128,21255,130,429684,141,20897,144,384877,149,291763,150,384742,158,24245,162,288310,165,217575,172,267542,175,299709],[19,1,191505,4,25153,6,290163,8,37859,9,258622,13,222651,14,124567,17,195544,18,107317,21,251485,23,23627,25,27916,28,188921,30,78400,32,319421,34,36844,37,187526,42,292712,47,376156,48,236882,49,382399,50,365450,57,302393,61,34956,62,17794,74,91304,77,281875,78,363130,81,122066,82,342083,83,249043,84,208552,89,43348,96,240575,102,212853,104,408403,105,260336,109,432658,113,130646,114,419593,116,300144,118,25369,119,267176,121,24824,124,382580,127,26983,128,57118,129,209100,130,431893,132,391901,133,108754,136,365494,137,298902,139,30309,143,11760,144,288492,145,106522,147,374823,148,116020,149,289201,152,40754,157,425760,158,17343,161,279984,170,366547,172,266672,173,113460,174,390196,175,297850,176,116964,181,369630],[20,4,25189,121,24824,124,385693,133,108754],[21,2,26996,61,427115,89,195346,157,428408,158,263423,162,287847,172,267224,173,281557],[22,14,123728,17,197312,18,107323,21,252670,47,377566,62,17096,74,91943,77,281723,83,354448,100,198850,102,213072,107,113311,116,304590,124,382583,127,372792,132,391115,136,365580,137,300102,150,384682,152,41478,158,22009,172,269722,173,201023,175,297768],[23,74,92072,77,302595,102,213092,124,384683,158,106247,172,269734,175,300252],[25,1,191404,14,210559,74,103507,77,282901,78,214910,82,342291,83,249655,102,212392,124,384532,127,112599,133,108407,149,289120,175,298129,181,369126],[26,12,185477,14,109479,17,195329,21,248783,42,295126,45,273163,56,346839,74,96996,77,208550,84,207107,89,110513,102,215942,110,134283,116,304075,119,110526,121,114968,124,386037,128,60621,133,108407,144,386532,148,117825,150,381627,157,429459,158,25016,175,303068],[27,61,36271,121,114968,124,383305,133,434195,139,30359,149,290550,175,302397],[28,102,216662,124,382614,175,297813],[29,1,191381,2,26831,4,25093,6,290187,8,38238,10,296441,11,365547,12,174667,13,223097,14,71264,15,118514,17,194670,18,10718,19,369203,20,98030,21,245525,23,24040,25,27853,26,41154,28,63954,30,77655,32,319016,34,36880,36,302068,42,291863,43,270947,45,269762,47,375812,48,235044,50,365896,54,39410,55,133254,56,337622,57,89600,61,35319,62,16966,65,363856,69,423612,70,180530,73,249892,74,91305,77,281807,81,122103,83,248967,84,211240,86,26562,90,344834,94,291135,96,240590,98,22085,99,115666,100,197074,102,212540,103,32485,104,383811,107,113262,109,432692,112,98804,113,130302,114,419910,115,355419,116,303556,118,25022,119,526,121,25203,124,382636,126,35057,127,26920,128,21320,129,208926,130,429477,132,391434,133,109367,135,377050,136,365057,137,298037,139,30386,141,20839,143,11694,144,103933,148,114947,149,289297,150,379630,152,41539,157,424904,158,17243,159,232762,161,11410,162,287928,164,28962,165,217992,170,365437,171,280205,172,265906,173,28954,174,389148,175,297884,176,10921,181,369297],[30,4,25129,121,25203,133,109367,161,369733],[31,10,374525,12,176382,13,221895,14,126836,18,10417,21,246095,25,28086,30,78065,32,319287,37,188289,42,292307,45,270035,47,377041,50,366703,56,338039,57,90141,61,207037,70,180845,74,93795,78,215776,83,249727,84,204362,89,107481,99,115724,100,199494,102,214010,110,130215,114,420164,115,356298,116,301335,119,0,124,383030,127,27602,130,429954,136,364827,141,21112,157,427514,158,72072,170,366834,171,280483,172,265218,175,300421,176,263900],[33,1,191776,10,296506,11,365621,34,38079,36,302337,42,293115,47,375844,62,59047,65,364009,77,282254,81,145459,115,355837,118,122093,124,387234,128,21571,130,429714,158,19455,173,200446,174,389493],[34,8,38404,25,28065,57,302660,61,34870,74,92335,83,250320,102,213816,105,260325,116,300730,124,386317,144,289116,158,22018],[35,4,25269,6,290038,8,38275,13,222496,17,195112,18,10633,25,27809,28,64123,30,77500,34,254918,36,303374,42,360305,45,276602,48,235550,57,303544,61,36191,74,92915,83,248885,94,291731,99,115630,100,199950,102,219571,113,130782,116,401290,118,25594,121,432761,124,383293,127,27750,128,318631,132,391465,137,298306,139,30513,141,21378,148,118915,157,424086,172,270124,174,389200,175,299106],[36,4,25305,121,432761],[37,57,302377,74,92225,102,213565,119,261,124,382877,158,22256,172,266074,175,297740],[38,45,272277,74,94416,84,211479,102,218148,124,384432,158,110089,172,265911],[39,74,97963,77,281739,102,213091,124,382613,136,365592,158,173209,172,269542,173,281466,175,299583],[40,74,105068],[41,4,25170,6,289906,12,174728,14,67955,17,195667,18,113027,25,27722,50,365707,54,39168,57,90303,62,17228,69,423455,74,93501,77,301283,89,15351,91,421743,95,280710,99,116177,102,215121,103,32636,109,432665,113,129984,115,356679,116,400156,121,25726,124,384916,132,390794,135,377118,136,365993,139,31379,143,11571,145,106322,147,22462,158,17208,159,232523,161,284364,162,235523,173,113389,175,299541,176,116451],[42,4,25206,74,94427,102,216172,121,25726,124,384484,175,297822],[43,124,384494],[44,2,27065,4,24998,8,37896,9,258677,10,296542,11,365657,12,174379,14,67808,17,195704,18,112952,21,245429,23,23733,25,27777,26,41035,34,36980,36,302570,37,187908,42,292948,45,269903,47,375938,48,235017,54,39380,57,304103,61,35232,62,16865,74,90988,77,282572,80,232782,83,249042,86,25894,89,13885,90,344901,100,197788,102,214872,105,259819,112,98560,114,421048,115,355470,119,268540,121,24838,124,388670,127,26880,128,21985,129,208847,130,430084,141,21325,144,385312,148,116181,150,381354,157,425400,158,17056,161,11575,162,235759,164,28303,165,218031,172,266620,173,28827,174,389351,175,299217,176,263299],[45,4,25034,50,365924,74,105201,83,355113,99,115884,110,130804,112,98230,121,24838,137,298117,158,22765,172,265327],[46,137,299323,158,22601],[47,4,24994,13,223105,15,119001,28,189088,32,319329,48,235877,50,366471,56,337483,61,426414,74,90941,78,285331,83,249081,86,25500,89,43893,90,345028,102,213712,115,355381,116,400930,121,284262,124,386720,128,56235,129,209262,137,297767,144,385207,157,426525,158,21129,161,284250,172,265124,180,33620],[48,4,25030,49,381894,60,374700,69,423497,74,97521,75,110145,80,232716,102,213657,118,121940,119,30130,121,284262,141,22071,161,369609,169,250908,171,279948],[49,14,204027,102,221572,158,17815,175,299731],[52,42,291797,61,36074,74,97029,83,354951,102,213336,127,373039,128,318977,144,380529,173,281527,175,298219],[53,74,93052,83,353710,129,209051,158,255175,172,269548],[54,14,119904,21,248936,34,108044,61,207416,74,95588,89,14524,128,58042,157,427025,158,18504,172,268247,175,301597],[55,1,191428,2,337581,4,441091,6,290458,9,257426,10,374408,12,174844,14,118370,15,118168,17,192183,19,369078,21,245638,31,370134,32,319005,34,101422,36,301967,37,188018,42,291882,45,270154,47,376752,50,365565,60,374729,61,206787,71,299651,74,103186,77,283094,78,294682,81,122391,82,342350,84,203514,86,429769,89,192464,90,345160,99,116281,100,196668,102,212862,104,383348,105,259936,114,420753,115,355426,118,380896,121,111372,124,382565,127,372529,128,317960,129,209107,132,390869,133,107924,136,365542,137,298946,144,384220,148,114234,149,289184,150,379157,157,424868,158,106762,161,280010,162,235590,165,217561,172,266631,173,200315,174,388467,175,297857,181,369188,183,293788],[56,121,111372,133,107924],[57,6,289972,114,420957,119,266958,121,111565,127,372077,144,288507,158,110162,175,302496],[58,17,197366,21,245231,74,104124,78,215230,102,213041,121,111565,124,384598,144,287805,158,173028,165,217445,172,269648,175,297908],[59,34,391437,61,114991,65,363441,74,105537,78,215231,89,106995,90,344794,102,212810,124,384583,136,365953,158,196543,175,297902],[60,14,123957,18,107350,21,252949,36,303740,49,382413,56,346880,74,102675,77,301840,78,215467,82,342336,84,212308,94,291699,100,198904,102,215489,107,113320,114,420022,124,384639,127,372776,136,365686,150,384747,158,106330,173,201096,175,299916,176,116916],[61,1,191420,13,221449,17,197118,18,113400,32,318997,34,194687,42,291861,48,236805,57,302135,61,206823,62,179418,69,422282,77,281930,78,363173,81,122369,83,250206,100,199717,102,212500,113,130376,114,419958,116,302518,118,381733,124,382618,133,108810,135,376961,144,103912,145,106400,147,374707,149,289246,154,304637,157,424849,158,353670,161,279995,172,269968,173,113543,175,297846,181,369686],[62,133,108810],[63,1,191363,14,127000,24,288037,28,189890,34,365376,48,236230,56,441217,57,302789,74,105340,78,284879,96,240556,102,213420,104,384238,124,385272,135,377004,137,298932,148,118495,158,107956,161,110547,170,366328,173,113261,175,303255],[65,2,337650,4,441098,6,290343,32,318924,34,372242,42,294133,47,375872,49,382321,61,427163,77,281660,78,294667,83,353649,89,282739,90,345104,104,408130,115,357049,119,267398,121,432547,124,382456,127,372515,128,317930,132,391156,133,430633,136,365533,137,298941,149,289151,150,379391,157,424757,158,272882,161,279952,162,287535,172,266559,173,281659,174,390786,175,297783,181,369141,183,293771],[66,36,304038,42,292535,45,270412,50,366698,56,338296,57,302887,69,423947,78,285653,114,421210,115,356053,116,302761,121,431688,124,383151,128,318191,130,431526,133,430633,136,364820,144,385684,157,429858,158,273730,172,265210,175,298518],[67,3,354577,4,440718,6,290151,10,374475,11,365914,31,369952,32,319012,34,373125,36,302051,42,291840,43,270912,45,269845,47,375384,49,382019,50,365552,57,304793,60,374837,61,427278,69,422650,77,281802,78,294678,82,342332,83,353757,86,428257,94,291103,104,384609,106,293733,109,432623,114,419903,115,355827,116,299864,118,380748,119,267188,121,284856,124,382605,127,372765,128,318014,130,430679,132,390903,134,341283,136,365614,137,297904,144,289098,147,374717,149,289252,157,424893,158,273017,161,279922,170,364913,171,280210,172,268962,175,297805,181,369750],[68,6,290183,10,374344,19,368988,27,299118,32,319041,34,271673,36,303683,42,291901,43,270914,44,284640,45,269651,47,376863,49,382406,50,365594,56,337726,57,302995,62,436784,71,299742,77,282733,78,285959,82,342332,83,353823,86,329345,90,344859,91,421617,94,291406,104,383983,114,419928,115,355154,116,299868,121,283727,124,382755,127,292292,128,318045,130,430698,132,391737,134,341392,136,364394,137,297941,144,289066,147,374832,149,289418,150,379152,154,304658,157,424484,158,273073,160,430905,161,283747,170,364972,171,280265,172,264794,173,282819,174,388508,175,299623],[69,10,296402,11,365566,34,365234,36,302688,43,270772,47,375632,49,381976,57,302134,61,426392,65,363478,78,294665,83,354920,89,282426,94,291007,106,293677,115,355666,116,300288,118,380568,121,432498,123,302226,124,382445,130,429727,132,390515,136,365522,137,299237,147,374645,149,290945,157,424893,158,273019,161,279942,162,287388,170,365279,171,280084,172,267250,173,282165,174,390615,175,297916,181,369369],[70,10,374721,11,366069,36,303579,42,292886,45,270377,57,304625,77,284531,86,429861,89,284239,115,356053,116,302978,121,432498,124,384000,128,318581,130,431496,136,365520,144,385719,157,430156,158,276314,172,268248,175,300409],[71,118,380826,124,386975,158,283967],[73,45,271543,89,282703],[74,6,290345,34,380603,61,427400,77,282057,104,408098,115,357053,124,384549,127,372524,132,391057,133,430683,136,365533,137,298936,144,383554,149,289152,158,272891,161,279950,172,267329,175,297791,181,369145],[75,133,430683,149,292107,158,349944],[76,132,391517]]] \ No newline at end of file diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 7c69165..0000000 --- a/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -.tmp-edge-profile/ -__pycache__/ -*.pyc - -# Local browser smoke/probe artifacts. -*probe*.html -*probe*.png -*smoke*.html -*smoke*.png diff --git a/.htaccess b/.htaccess index 1a8ccd5..fedc16b 100644 --- a/.htaccess +++ b/.htaccess @@ -1,3 +1,5 @@ +AddType application/manifest+json .webmanifest + ExpiresActive On ExpiresByType text/html "access plus 0 seconds" @@ -21,15 +23,22 @@ Header set Pragma "no-cache" Header set Expires "0" + + Header set Cache-Control "public, max-age=2592000, immutable" + Header set Cache-Control "no-cache, no-store, must-revalidate" Header set Pragma "no-cache" Header set Expires "0" - - Header set Cache-Control "public, max-age=2592000, immutable" - AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json text/plain + + +# Never serve hidden server-data paths. + + RewriteEngine On + RewriteRule (^|/)\. - [R=404,L] + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6f3a291 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5501 +} \ No newline at end of file diff --git a/ACHIEVEMENT_AUDIT.md b/ACHIEVEMENT_AUDIT.md deleted file mode 100644 index 5f3a086..0000000 --- a/ACHIEVEMENT_AUDIT.md +++ /dev/null @@ -1,268 +0,0 @@ -# 実績システム再監査レポート v39.16.78 - -対象: 全72実績、解除イベント、判定順序、進捗リセット、保存・復元、Undo/Redo、サーバー同期 -方針: 前回重点対象だった「自分のずんち」「敵の敵は味方」だけでなく、これまで大きく取り上げていなかった実績を再度コード経路から監査した。明確な不具合は修正し、ゲームデザイン判断が必要な条件は変更していない。 - -## 1. 今回新たに確定・修正した不具合 - -### 1.1 「ミニマリスト」: プレイヤー設置物情報が保存されなくなる - -`ミニマリスト` は「現在存在するプレイヤー設置物が10個以下」を判定するため、各アイテムの `_achievementPlayerPlaced` を必要とする。 - -旧実装では、この情報を保存する条件が実質的に `無計画都市` の未解除状態へ依存していた。そのため、`無計画都市` を先に解除した後にセーブ・ロードすると、既存アイテムのプレイヤー設置情報が失われ、`ミニマリスト` の物数判定が過少になる可能性があった。 - -**修正:** `無計画都市` と `ミニマリスト` のどちらかが未解除なら、プレイヤー設置情報を保存する。30秒削除用の設置時刻は従来どおり `無計画都市` に必要な場合だけ保存する。 - ---- - -### 1.2 「サウナ水風呂」: 高温開始時刻が更新され続ける - -条件は「暑すぎる状態になった個体を15秒以内に寒すぎる状態へ移す」だが、旧実装は高温状態を評価するたびに開始時刻を現在時刻へ更新していた。 - -このため、長時間高温に置いた後でも、寒冷へ移した直前の評価時刻との差だけで15秒以内と判定され得た。 - -**修正:** 快適域外の高温状態へ**入った瞬間**だけ開始時刻を記録し、連続して高温にいる間は更新しない。高温状態を離れてから再び入った場合は新しい試行として時刻を記録する。 - ---- - -### 1.3 「抵抗器じゃない」: 致死感電した7体目がカウントされない - -旧実装では感電ダメージを与えた後に実績カウントを行っていた。感電が致死になると対象は死亡済みになり、`recordWireShock` の生存チェックで除外されるため、7体目がその感電で死亡すると解除できない場合があった。 - -**修正:** 有効な感電を確認した時点で、ダメージ適用より先に実績カウントを行う。致死・非致死のどちらでも、その感電自体は1体分として扱う。 - ---- - -### 1.4 実績リセット後に隠れた途中進捗が残る - -全実績リセット後も、World・たりない・アイテム本体に直接保持された一部の途中状態が残っていた。そのため、リセット直後に少ない追加操作だけで再解除できる実績があった。 - -**修正したリセット対象:** - -- 同一個体への直接給餌回数・直接治療回数 -- サウナ開始時刻・直前高温状態 -- たりない保持開始時刻 -- 喧嘩餅から喧嘩開始までの一時情報 -- 粘着爆弾の受け渡し回数 -- `無計画都市` 用の配置時刻 -- 自然奴隷・自然王の世代進捗 - -`ミニマリスト` が現在の配置物を正しく識別するため、既存アイテムの「プレイヤーが配置した物か」という事実自体はリセットで消していない。 - ---- - -### 1.5 「監視カメラ」: Undo/Redo・地面変更が介入になっていない - -配置・削除・給餌・つつく・射撃などは観察タイマーをリセットしていた一方、Undo、Redo、有効な地面変更は世界状態を変更する操作なのに観察タイマーをリセットしていなかった。 - -**修正:** 成功したUndo/Redo、および実際に地面種別が変わった操作を介入として記録する。 - ---- - -## 2. 前回重点対象の修正状態 - -### 「あぁ^~ たまらねぇぜ」 - -現在の条件は次の経路で成立する。 - -`おしり鋲でずんち蓄積 → おしり鋲を抜いて排出 → 排出ずんちが本人から一度離れる → バウンス柵等で反射・加速 → 本人へ帰還して致死衝突` - -- 生成直後の本人との重なりは衝突扱いしない。 -- 本人から一度離れた後に帰還した衝突だけを自己帰還として扱う。 -- 非致死では解除しない。 -- バウンス柵による反射・加速は資格を失わせない。 -- 停止済み・プレイヤー保持済みの排出ずんちは対象外。 -- 致死処理で本人の生存IDが解放されても、ダメージ前に所有者一致を保持するため解除可能。 - -### 「敵の敵は味方」 - -30体以上の状態で稼いだ連続数は、生存たりないが30体未満になった時点で0へ戻る。危険カテゴリのアイテムによるたりないへのダメージでも従来どおりリセットする。 - -前回ユーザー確認済みの「無傷」の範囲、戦闘ペア危険死の期限、機械化産業の履歴AND条件は現行仕様のまま変更していない。 - ---- - -## 3. これまで大きく取り上げていなかった実績の再監査結果 - -### 生態・繁殖 - -- `はじめての繁殖`、`ベビーブーム`、`大いなる母` は実際の繁殖成功イベントから接続されている。 -- `天寿`、`水銀で天寿` は最終確定した死亡理由を受けた後に判定される。 -- `25体全員が病気` は睡眠病だけを除外し、ずんち病・爆発病・喧嘩病を対象としている。 -- `管理された楽園` は6日目以降から連続1ゲーム日を計測する。 -- `安全第一` は25体以上を300ゲーム秒維持し、天寿以外の死亡で再計測する。天寿後に25体を下回れば人数条件によって停止する。 -- `自給自足` は20体以上・複製機なし・直接給餌なしを600ゲーム秒維持するフィールド内条件。 -- `雨宿り完了` は現行の雨天である小雨時に、パラソル・巣箱・土管の保護判定を使う。 -- `町のお医者さん` は応急手当を使った回数ではなく、実際にHPが1以上回復した回数だけを累計する。 -- `豊穣ヲ希求スル者` はラブ餅の影響下で成立した繁殖だけを累計する。 - -**追加の到達不能・イベント未接続は確認されなかった。** - -### 実験・危険行動 - -- `6番目の大量絶滅` は同一Worldの10ゲーム秒ローリング区間で死亡50件。 -- `豆野、殺ッカーやろうぜ!` は通常ボールの速度条件を満たした致死衝突から直接接続される。 -- `ヘコヘコ中に着火` は着火処理が繁殖儀式状態を消す前に判定される。 -- `下剤中の餓死` は死亡フレームの下剤状態を保持して判定する。 -- `物理法則を下側に超越` はアイテム位置の実温度計算を走査しており、温度効果の重畳で到達可能。 -- `おくすり手帳全埋め` は現行仕様どおり11種類を対象とし、鎖・王冠は除外する。 -- `革命` は王の喧嘩死と、直前0.1ゲーム秒以内の奴隷による喧嘩ダメージを照合する。 -- `火に油` は喧嘩餅を受けた個体が30ゲーム秒以内に参加する喧嘩開始を判定する。 -- `五体満足` / `零体満足` は状態効果付与時と自然ステータス変化時の双方から再評価される。 -- `混沌ヲ希求スル者` は喧嘩餅影響下の喧嘩開始だけを永続累計する。 -- `命のバトン` は同じ生存中の粘着爆弾の受け渡し回数を保存・復元する。 -- `情報化産業` は別々の2基盤を電線類で直接つなぎ、信号が実際に伝達された経路から解除される。 - -**追加の確定不具合は「抵抗器じゃない」の致死感電順序だけで、修正済み。** - -### 建築・環境 - -- `物でいっぱい` はプレイヤー配置イベントを永続累計し、リンク類の配置も配置として数える。 -- `清掃業者` はロボット掃除機による有効な清掃200回のフィールド内進捗で、現行ドキュメントと一致する。 -- `潔癖症` は生存たりない0、生存アリ0、生存アイテムがロボット掃除機のみ、かつ条件を満たす稼働掃除機が存在する場合に成立する。死亡たりないは死亡直後に非表示となり定期圧縮されるため、画面上の死体残留との矛盾は確認されなかった。 -- `メガロポリス` は100体・複製機10・草ベッド/巣箱/土管合計15の境界値で評価される。 -- `砂場` は生存中のトイレ5個で成立する。 -- `そこ公共空間だよ?` は公園フィールドで実際に地面変更が成立した時だけ解除する。 -- `破格の工事費用` は壁時計1秒以内の有効な地面変更4回を数える。 - -**追加の確定不具合は「ミニマリスト」の保存メタデータだけで、修正済み。** - -### 操作・継続 - -- `秘密のコレクション` は9スロットすべてに実セーブハッシュがあることを確認する。 -- `何やってるの?` は空フィールドへのクリックだけを壁時計1秒内に4回数える。 -- `見なかったことにしよう` は1回のUndo前後で死亡個体が10体以上減った場合に成立する。 -- `やっぱなし` / `やっぱあり` は成功したUndo/Redoだけを永続累計する。 -- `割とふわふわ` は共通の「つつく」対象判定からぬいぐるみを飛ばした場合に成立する。 -- `狙撃手` は命中ではなく発砲333回を永続累計する。 -- `スペックがたりない` は有効な計測FPSが10未満になった場合で、非表示タブ由来の異常なフレーム間隔を除外する。 -- `毎日たりない観察` はローカル暦の日付単位の7日連続起動。 -- `寝ろ` / `めっちゃたりない観察` は壁時計の連続経過で、現行仕様どおりタブ非表示時間も継続として扱う。 -- `真・たりない観察` は自身以外の71実績が解除済みかを判定する。 - -**追加の確定不具合は「監視カメラ」の介入漏れと、全実績リセット後の隠れ進捗残留で、修正済み。** - ---- - -## 4. 仕様確認が必要な曖昧点 — 現在は未変更 - -以下は実装が壊れているとは断定できず、表示文から複数の解釈が成立するため、回答を受けるまで条件変更を行っていない。 - -### Q1. 「ずんちどれい」「たりない王」の「出現」範囲 - -現行は**喧嘩戦績による自然変身だけ**で解除する。 - -- 鎖を付けて奴隷化 → 対象外 -- 王冠で王化 → 対象外 -- 奴隷同士から生まれた生来の奴隷 → 対象外 - -候補: - -- **A: 現行どおり** — 喧嘩戦績による変身だけ -- **B: 人工付与以外** — 喧嘩戦績変身 + 生来の奴隷を対象。鎖・王冠は除外 -- **C: 文字どおり全出現** — 鎖・王冠を含め、状態になった時点で解除 - -### Q2. 「末代までの恥」「華麗なる王族」の「○代連続」 - -現行は**同じ家系でなくても、自然変身した個体の世代番号が連番なら成立**する。例えば無関係な家系の2代・3代・4代・5代・6代に自然奴隷が1体ずついれば5代連続になる。 - -候補: - -- **A: 現行どおり** — 世界内の世代番号の連続 -- **B: 直系家系限定** — 親子関係を辿って同じ系譜で連続して状態が続く必要がある - -Q1で生来の奴隷を対象にする場合、Q2の奴隷5代判定にもその出生を含めるかを合わせて決める必要がある。 - -### Q3. 「過保護」の「薬で治療」 - -現行は、薬カテゴリの直接使用なら効果の善悪を問わず治療回数に加算する。対象には応急手当だけでなく、睡眠薬、下剤、プロテイン、ニテロプ、弾薬、水銀、巨大化薬、小型化薬なども含まれる。 - -候補: - -- **A: 現行どおり** — 薬カテゴリの直接使用3回 -- **B: 治療系アイテムだけ** — 対象薬を明示的に限定 -- **C: 実際に有益な回復が発生した使用だけ** - -### Q4. 「無計画都市」の「削除する操作を累計30回」 - -現行は**操作回数ではなく、30秒以内に削除された対象物1個につき1回**進む。範囲削除1回で対象物を10個消した場合、最大10進む。 - -候補: - -- **A: 現行どおり** — 削除した対象物数を数える -- **B: 削除コマンド単位** — クリック/範囲削除の1実行を1回として数える -- **C: 連続削除ジェスチャ単位** — 押している間の連続削除を1回として数える - -### Q5. 「監視カメラ」の5分と「見るだけ」の意味 - -現行は**300ゲーム秒**。高速化すれば実時間5分より早く解除できる。今回、世界を変更するUndo/Redo・地面変更も介入として追加したが、UI操作、セーブ、速度変更、ポーズ、空クリックなど世界を直接変更しない操作はタイマーを止めない。 - -時間基準: - -- **A: 現行どおり300ゲーム秒** -- **B: 実時間5分** - -「見るだけ」の入力基準: - -- **1: 現行どおり世界への介入だけでリセット** -- **2: ほぼすべてのユーザー操作でリセット** - -例: `Q5 = A1` のように指定可能。 - -### Q6. 「抵抗器じゃない」のセーブ/Undo跨ぎ - -現行の「同じ1本」はランタイム中の同一電線オブジェクト。セーブロードやUndo/RedoでWorldが再構築されると、1~6体までの途中人数は失われる。 - -候補: - -- **A: 現行どおり** — 7体すべてを同じランタイム中に感電させる -- **B: 同じ保存上の電線なら継続** — セーブロード後も電線ごとの途中人数を保存する - -### Q7. 「害虫駆除」の「アリを倒す」主体 - -現行は**アリが死亡した事実をすべて累計**する。プレイヤーの危険アイテムだけでなく、たりないとの戦闘、設備・環境など別要因で死亡しても1体として数える。 - -候補: - -- **A: 現行どおり** — 死亡したアリをすべて数える -- **B: プレイヤー由来の撃破だけ** -- **C: プレイヤーまたはたりない陣営が倒した場合だけ** - ---- - -## 5. 前回ユーザー確認済みの仕様 — 変更なし - -以下は前回の確認により現行仕様を維持している。 - -- 戦闘ペア危険死: 第1死亡から第2死亡まで同一World内で明示的な時間制限なし。 -- 機械化産業: ロープ・棒・ばねの配置履歴と信号作動履歴のAND。現在同時に存在する必要なし。 -- 敵の敵は味方: 「無傷」は危険カテゴリ由来のたりないへのダメージをリセット対象とし、あらゆるダメージを禁止する意味ではない。 - ---- - -## 6. 検証結果 - -v39.16.78 に対して以下を実行し、すべて成功した。 - -- JavaScript全ファイル・監査スクリプトの `node --check` -- 全72実績の定義・カテゴリUI・同期監査 -- プレイスタイル実績の境界値・リセット範囲・永続化・フック監査 -- 「あぁ^~ たまらねぇぜ」専用の生成直後除外 / 離脱 / 帰還 / 致死・非致死回帰テスト -- サウナ開始時刻、実績リセット、監視介入、ミニマリスト保存情報、致死感電の追加回帰テスト -- 実績イベント統合監査 -- サーバー状態・旧形式移行・並行同期監査 -- 全体回帰監査 `scripts/regression_check.py` -- Python監査スクリプトのコンパイル確認 -- `achievement_api.php` のPHP構文確認 - -結果はすべて `[OK]`。今回の再監査で新たに確認した**確定不具合は5系統で、すべて修正済み**。上記Q1~Q7は仕様回答待ちのため、v39.16.78では現行挙動を維持している。 - - -## Confirmed specification decisions (39.16.78) -- ずんちどれい / たりない王: birth-status appearances also qualify. -- 末代までの恥 / 華麗なる王族: only a direct qualifying parent-child lineage counts. -- 過保護: a treatment counts only when the direct use actually improves the health need. -- 無計画都市: one deletion operation counts once, even if area deletion removes multiple objects. -- 監視カメラ: requires 3 uninterrupted game days; gameplay operations are interventions. -- 抵抗器じゃない: partial wire progress remains nonpersistent across save/load and Undo. -- 害虫駆除: only player-caused ant deaths count. diff --git a/AI_MAP.md b/AI_MAP.md deleted file mode 100644 index db0401f..0000000 --- a/AI_MAP.md +++ /dev/null @@ -1,112 +0,0 @@ -# tarinai_ AI map - -Purpose: static Japanese browser simulation/game. Optimize context by reading this file only after `README.md`, then open the relevant subsystem files. For exact file expansion, use `scripts/ai_inventory.ps1` or `scripts/ai_inventory.py`. - -## Architecture - -- Entrypoint: `index.html` defines the app shell, canvas, dialogs, right panels, and exact script order. -- Runtime style: plain browser JS, global IIFEs, exports on `window`, no bundler/module loader. -- Main loop: `js/main.js` initializes assets/audio/world/UI/save, then drives update/render. -- Performance/display settings: `js/perf_profiler.js` owns manual visual settings and profiler buckets; display tiers affect rendering/visuals, not simulation-quality branches. -- Update order: `js/system_order.js` calls phase facades from `js/simulation_systems.js`. -- Command path: UI/input emits command objects; `js/command_dispatcher.js` routes tool, pointer, selection, save, reset, and UI intents. -- Persistence: save files are schema/codec/storage/coordinator modules; snapshot/history modules keep world restoration and stats consistent. - -## Subsystem Map - -- Core/bootstrap: `js/version.js`, `js/event_bus.js`, registries, `js/data.js`, `js/ground_types.js`, `js/health.js`, `js/main.js`, `js/debug_tools.js`, `js/perf_profiler.js`. -- Rendering/assets/audio: `js/assets.js`, `js/audio.js`, `js/render.js`, `js/tarinai_render.js`, `js/item_render_runtime.js`, `js/sound_pack.js`. -- Simulation phases: `js/sim_core.js`, `js/simulation*.js`, `js/world_update*.js`, `js/system_order.js`. -- World domain: `js/world*.js`, `js/weather_system.js`; covers state, camera/view, environment, combat/effects, placement/history, tools, family/social, ants, spatial budgets. -- Creature domain: `js/tarinai*.js`; covers state, identity, needs, actions, forced behavior, social life, item effects, disease/nests, movement/update pipeline, rendering. -- Item/structure domain: `js/item*.js`, `js/items.js`, `js/structures.js`, `js/structure_lifecycle.js`; covers definitions, spawning defaults, lifecycle, dynamic behavior, rendering. -- Physics/mechanics: `js/physics*.js`, `js/mechanical_system.js`, `js/constraint_system.js`, `js/collision_footprint_system.js`. -- UI/input: `js/ui*.js`, `js/text_catalog.js`, `js/command_dispatcher.js`; covers panels, tools, selected creature, logs, charts, family tree, mouse/touch, dialogs, labels. -- Family tree: `js/family_graph.js`, `js/ui_family_*.js`; data transform, layout, paths, async rendering, validation. -- Save/history: `js/save*.js`, `js/snapshot_system.js`, `js/restore_coordinator.js`, `js/history_system.js`. -- Scripts: `scripts/generate_app_files.py`, `scripts/generate_item_icons.py`, `scripts/regression_check.py`, `scripts/ai_inventory.py`, `scripts/ai_inventory.ps1`. - -## File Pattern Semantics - -- `*_pipeline.js`: ordered runner/facade for update or lifecycle work. -- `*_step_*.js`: narrow pipeline step wrapper. -- `*_system.js`: domain service, phase service, or compatibility facade. -- `*_runtime.js`: stable runtime-facing facade around split modules. -- `*_registry.js`: definition registry and lookup/normalization helpers. -- `world_*`: world-owned behavior; `tarinai_*`: creature-owned behavior; `item_*`: item-owned behavior; `ui_*`: DOM/input/panel behavior. -- `assets/sprites/tarinai_*.webp`: creature visual state sprites; numeric prefix is stable asset ID/order. -- `assets/ui/tool_*`: tool palette icons; `assets/ui/favicon*`, `apple-touch-icon`, `ecology_*` are app/help icons. -- `assets/objects/*`: field object sprites for ants, zunchi, pushpin, oshibyo, genkotsu, plushie. -- `assets/sounds/voice_*`: creature voice samples; `shoot_*`, `major_damage_*`, `firecracker_*` are SFX. - -## Task-Oriented Read Sets - -- UI/layout bug: `index.html`, relevant `css/*.css`, `js/ui_bind.js`, `js/ui_layout_dialogs.js`, matching `js/ui_*.js`. -- Tool behavior: `js/command_dispatcher.js`, `js/world_tool_actions.js`, `js/item_registry.js`, relevant `js/item_*` or `js/tarinai_item_*`. -- Creature behavior: `js/tarinai.js`, `js/tarinai_update_pipeline.js`, relevant `js/tarinai_*`, `js/system_order.js`. -- World/environment/combat: matching `js/world_*`, `js/simulation_*`, `js/system_order.js`. -- Fire/burning behavior: `js/fire_runtime_system.js`, `js/item_dynamic_tool_system.js`, relevant item definitions, `js/world_combat_effects.js`, and nearby environment helpers. -- Performance/crowding: `js/perf_profiler.js`, `js/item_update_scheduler.js`, `js/tarinai_update_policy.js`, `js/constraint_system.js`, `js/world_spatial_budget.js`, and render/detail code. -- Save/load: `js/save_schema.js`, `js/save_codec.js`, `js/save_system.js`, `js/snapshot_system.js`, `js/restore_coordinator.js`. -- Family tree: `js/world_family_social.js`, `js/family_graph.js`, `js/ui_family_*.js`. -- Static app/cache/version: `app_manifest.json`, `scripts/generate_app_files.py`, `service-worker.js`, `js/version.js`, `index.html`. - -## Invariants - -- Preserve `index.html` script order unless all dependent manifests/generated files are updated together. -- Keep `app_manifest.json`, `js/version.js`, `service-worker.js`, and cache query versions synchronized. -- Keep intentional global exports for compatibility with regression checks and browser runtime; remove public facades only after repo-wide caller checks. -- Prefer focused edits in the owning subsystem; split modules are intentional compatibility boundaries. -- Avoid reintroducing `simulationQualityTier` branches unless a real simulation-quality setting is restored; current quality controls are visual/detail controls. -- `High` display quality may include quality-preserving optimizations, but should not reduce simulation cadence, object limits, or visible fidelity in a noticeable way. -- Save format compatibility with old text saves is not required when intentionally shortening/replacing save hash text; keep current save/load internally consistent instead. -- Run `node --check js/*.js` and `python scripts/regression_check.py` for behavior changes. - -## Current Maintenance Notes - -This section records current boundaries only. Version-by-version cleanup history and completed work logs are intentionally omitted. - -### Live Compatibility Surfaces - -- Ordered global-IIFE loading is still part of the app contract; check repo-wide callers before removing bare globals or facade objects. -- Keep public event names with active listeners: `audio:play`, `selection:changed`, `tool:selected`, `world:phase`, `log:entry`, and `tool:placed`. Telemetry-only emit calls should stay removed. -- Save compatibility with old exported text is not required, but the current save/load round-trip must remain internally consistent. -- Tool icon ownership is `TOOL_DEFINITIONS.icon`; UI code converts asset paths for CSS variables. Avoid restoring CSS-local `url(...)` definitions. -- Shared geometry/display/domain helpers should stay in their owning helper modules unless a dedicated namespace migration is being done. - -### Open Architecture Candidates - -- Full namespace migration for behavior/action/needs/tool helpers. This requires rewriting many bare global call sites and updating regression contracts. -- Effect-specific spatial indexing, fuller delta-history storage, and high-speed collision partitioning. These need new indexes or semantics beyond cleanup. -- UI/text-side meaning consolidation, including labels, wrapping/measurement helpers, and duplicated scalar helpers such as `num`, `finiteOr`, `isObject`, and `clonePlain`. -- Further `World.prototype` reduction is possible, but only for helpers proven not to be runtime-facing canvas/UI/tool APIs. - -## Current Feature Routing - -- Achievement UI: `index.html` achievement dialog, `css/components.css` achievement rows/toast styles, and `js/achievements.js` DOM rendering. -- Achievement spell payload: `TarinaiAchievements.exportSpellState()`, `snapshot.g.s`, `save_system.js` spell export inclusion, and `save_codec.js` achievement metadata readers/writers. -- Logic board: `js/circuit_board_system.js`, `js/signal_system.js`, signal-related item definitions/metadata, and the generated circuit editor markup/styles. -- Shared play: `js/seesaw_system.js`, tarinai action/need modules, placement preview, and item/structure definitions. -- Reset presets: `js/world_reset_presets.js`; UI reset selection is in `js/ui_layout_dialogs.js`; social archive reset/fallback routing touches `js/world_family_social.js`. -- Physics tool icons and previews: `js/ui_tools.js`, `js/render.js`, `js/placement_preview_system.js`, `js/physics_shape_editor_system.js`, and `js/item_tool_metadata.js`. - -## Cleanup Candidate Audit - rechecked 2026-07-18 - -Do not delete these yet. This is a current worktree audit of unused, duplicated, stale, or low-meaning processes that should be reviewed before any removal or refactor. - -### Stale Current-State Docs And Labels - -- `BUILD_NOTE.txt` header still says schema 51 / payload v6, while `js/save_schema.js`, `js/achievements.js`, and `scripts/achievement_save_v6_audit.js` indicate schema 53 / payload v8. - -### Audit Script Candidates - -- `scripts/achievement_save_v6_audit.js` is named for v6 but asserts current schema 53 and achievement v8. Rename or fold into `scripts/achievement_audit.js` / `scripts/regression_check.py`. -- The many single-purpose `*_regression_audit.js` scripts appear to overlap the broad `scripts/regression_check.py` and achievement audits. Review for consolidation before deleting: `achievement_audio_tooltip_regression_audit.js`, `colony_crisis_regression_audit.js`, `effect_runtime_regression_audit.js`, `history_binary_regression_audit.js`, `long_session_runtime_regression_audit.js`, `non_tarinai_performance_regression_audit.js`, `object_render_cursor_regression_audit.js`, `offscreen_motion_performance_regression_audit.js`, `pathfinding_regression_audit.js`, `performance_5to7_regression_audit.js`, `performance_architecture_regression_audit.js`, `performance_cleanup_regression_audit.js`, `population_recovery_regression_audit.js`, `render_stack_regression_audit.js`, `reproduction_memory_sprite_regression_audit.js`, `reproduction_regression_audit.js`, `self_zunchi_regression_audit.js`, `social_interaction_regression_audit.js`, `tooltip_nail_colony_focus_regression_audit.js`, and `ui_grass_limits_regression_audit.py`. -- The invariant `node --check js/*.js` is weak on this Windows/PowerShell workspace because wildcard expansion does not behave like POSIX shells. Prefer a script-backed check or explicit file enumeration. - -### Duplicated Helper/Glue Processes - -- `const num = global.TarinaiCoreHelpers?.finiteOr || ...` is repeated in `js/geometry_helpers.js`, `js/mechanical_system.js`, `js/physics_projection_system.js`, `js/physics_world_system.js`, `js/placement_preview_system.js`, `js/seesaw_system.js`, and `js/signal_system.js`. Consider a load-order-safe local import pattern or just use `TarinaiCoreHelpers.finiteOr` directly where safe. -- `const clamp = global.TarinaiCoreHelpers?.clampNumber || ...` is repeated in `js/circuit_board_system.js`, `js/physics_world_system.js`, `js/seesaw_system.js`, and `js/signal_system.js`. -- Dialog close/backdrop wiring is partly consolidated in `js/world_placement_log.js`, but `js/world_placement_log.js` signal editor and `js/circuit_board_system.js` still manually wire cancel/close/backdrop handlers. Candidate for a small shared modal-close helper after checking editor-specific behavior. -- `finiteOrNull` in `js/snapshot_system.js` and `finiteNumberOrNull` in `js/save_codec.js` are nearly identical null-preserving numeric helpers. They are persistence-local today, but can be centralized if the save/snapshot boundary remains clear. diff --git a/BUILD_NOTE.txt b/BUILD_NOTE.txt deleted file mode 100644 index 06df610..0000000 --- a/BUILD_NOTE.txt +++ /dev/null @@ -1 +0,0 @@ -Current build metadata is defined in js/version.js and app_manifest.json. diff --git a/FILES.json b/FILES.json deleted file mode 100644 index 08ffb02..0000000 --- a/FILES.json +++ /dev/null @@ -1,191 +0,0 @@ -{ - "purpose": "Token-efficient machine-readable routing for tarinai_. Use scripts/ai_inventory.py for exact current file expansion.", - "version": "39.16.137", - "entrypoints": { - "app": "index.html", - "runtime": "js/main.js", - "update_order": "js/system_order.js", - "manifest": "app_manifest.json", - "service_worker": "service-worker.js" - }, - "read_strategy": { - "default": [ - "README.md" - ], - "architecture": [ - "README.md", - "AI_MAP.md" - ], - "exact_inventory": [ - "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All", - "python scripts/ai_inventory.py --all" - ], - "machine_routing": [ - "FILES.json" - ] - }, - "groups": [ - { - "id": "root", - "patterns": [ - ".gitignore", - ".htaccess", - "app_manifest.json", - "favicon.ico", - "index.html", - "README.md", - "AI_MAP.md", - "GAME_FEATURES.md", - "FILES.json", - "service-worker.js", - "achievement_api.php", - ".achievement_data/.htaccess" - ], - "meaning": "Repo/app shell metadata, server/static cache config, docs, manifest, entrypoint." - }, - { - "id": "css", - "patterns": [ - "css/*.css" - ], - "meaning": "Base, layout, panel, component, and mobile responsive styling." - }, - { - "id": "scripts", - "patterns": [ - "scripts/*.js", - "scripts/*.py", - "scripts/*.ps1" - ], - "meaning": "Generators, regression checks, version-specific audits, and token-aware AI inventory tooling." - }, - { - "id": "core", - "patterns": [ - "js/version.js", - "js/event_bus.js", - "js/domain_ids.js", - "js/registry_base.js", - "js/disease_registry.js", - "js/sound_pack.js", - "js/deterministic_helpers.js", - "js/item_tool_metadata.js", - "js/item_tool_definitions.js", - "js/item_visual_definitions.js", - "js/item_food_definitions.js", - "js/item_effect_definitions.js", - "js/item_registry.js", - "js/data.js", - "js/ground_types.js", - "js/health.js", - "js/input_mode_manager.js", - "js/main.js", - "js/math.js", - "js/perf_profiler.js", - "js/debug_tools.js", - "js/update_step_pipeline_runner.js", - "js/burn_motion_util.js" - ], - "meaning": "Bootstrap, shared constants/helpers, registries, config, profiling/debug." - }, - { - "id": "assets_runtime", - "patterns": [ - "js/assets.js", - "js/audio.js", - "js/render.js", - "js/tarinai_render.js", - "js/item_render_runtime.js" - ], - "meaning": "Image/audio loading and canvas rendering." - }, - { - "id": "simulation", - "patterns": [ - "js/sim_core.js", - "js/simulation*.js", - "js/system_order.js" - ], - "meaning": "Simulation primitives, concrete systems, phase ordering." - }, - { - "id": "world", - "patterns": [ - "js/world*.js", - "js/weather_system.js" - ], - "meaning": "World state/view/update/tools/environment/combat/family/social/ants." - }, - { - "id": "physics", - "patterns": [ - "js/physics*.js", - "js/mechanical_system.js", - "js/constraint_system.js", - "js/collision_footprint_system.js" - ], - "meaning": "Geometry, collisions, constraints, mechanical item behavior." - }, - { - "id": "items", - "patterns": [ - "js/item*.js", - "js/items.js", - "js/structures.js", - "js/structure_lifecycle.js", - "js/circuit_board_system.js" - ], - "meaning": "Item/structure registry, lifecycle, dynamic behavior, rendering." - }, - { - "id": "tarinai", - "patterns": [ - "js/tarinai*.js", - "js/ants.js", - "js/seesaw_system.js" - ], - "meaning": "Creature state/actions/needs/social life/item effects/disease/rendering plus ant actor model." - }, - { - "id": "save", - "patterns": [ - "js/save*.js", - "js/snapshot_system.js", - "js/restore_coordinator.js", - "js/history_system.js" - ], - "meaning": "Snapshot schema/codec/storage/restore/history." - }, - { - "id": "ui", - "patterns": [ - "js/ui*.js", - "js/text_catalog.js", - "js/command_dispatcher.js", - "js/family_graph.js", - "js/achievements.js" - ], - "meaning": "DOM UI, input, tools, selected panel, logs, charts, family tree, text labels, command routing." - }, - { - "id": "assets", - "patterns": [ - "assets/sprites/tarinai_*.webp", - "assets/ui/tool_*", - "assets/ui/favicon*", - "assets/ui/apple-touch-icon.png", - "assets/ui/ecology_*.webp", - "assets/objects/*", - "assets/sounds/*" - ], - "meaning": "Creature sprites, UI/tool icons, field object images, voice/SFX samples." - } - ], - "conventions": { - "*_pipeline.js": "ordered runner/facade", - "*_step_*.js": "pipeline stage wrapper", - "*_system.js": "domain service or compatibility facade", - "*_runtime.js": "stable runtime-facing facade", - "*_registry.js": "definition lookup/normalization" - } -} diff --git a/GAME_FEATURES.md b/GAME_FEATURES.md deleted file mode 100644 index 0b440d3..0000000 --- a/GAME_FEATURES.md +++ /dev/null @@ -1,93 +0,0 @@ -# tarinai_ Game Features - -`tarinai_` is an observation sandbox about small creatures called tarinai. The player shapes a field with food, medicine, furniture, hazards, machines, weather, and ground choices, then watches how the colony reacts. The game is about cause and effect: small changes in the environment can alter movement, stress, disease, relationships, reproduction, accidents, cleanup, and survival. - -## Core Play - -The player starts by choosing a field and ground, then places objects or uses direct tools on the colony. Tarinai continue acting on their own while the player observes, edits the environment, removes problems, or creates new ones. The colony panel, event log, selected-tarinai data, and family tree help explain what is happening without taking control away from the simulation. - -The game supports quiet observation and chaotic experiments. A player can build a comfortable habitat with beds, food, grass, and cleaning tools, or create a dangerous machine field with rotators, fences, stones, pins, explosions, ants, and poison. - -## Tarinai - -Tarinai are autonomous creatures with HP, needs, stress, personality, age, family links, diseases, item effects, and behavior states. They choose actions from their current condition and surroundings instead of following a fixed script. Hunger can make them seek grass or food, sleepiness can make them find a bed, fear can make them flee, and social state can make them follow, fight, avoid, or approach another tarinai. - -Each tarinai has individual tendencies. Aggressive tarinai fight more easily, social tarinai seek others more often, nervous tarinai react strongly to danger, and inherited traits can make children resemble their parents. Selecting a tarinai shows health, needs, stress, effects, relations, inventory, family information, and the reason for its current behavior. - -Tarinai can be damaged by hazards, pushed by physics objects, carried by ants, attached to links, blocked by fences, affected by medicine, frightened by accidents, comforted by habitat items, or preserved when outside the active field. Some objects can become emotionally important, so losing them may cause panic. - -## Food And Consumables - -Grass is both food and habitat material. Tarinai can eat it, feel comfort near it, gather it for simple beds, and interact with it through ground and weather systems. Water can be drunk, can appear from rain, and can be generated or copied by tools. - -Sweet food helps satisfy hunger and can support recovery in some cases. Love mochi makes reproduction behavior more likely for a while, while fight mochi pushes the colony toward conflict. Zunda juice clears item effects, making it useful after medicine or special consumables have changed a tarinai too much. - -Medicine changes the simulation more aggressively than normal food. Sleep drugs push tarinai toward sleep or sleep disease, laxatives create repeated zunchi and reduce food benefit, protein makes a stronger permanent mode with lower disease risk, and niteropu makes a weaker permanent mode with higher disease risk. Ammo creates temporary speed and impact danger. Mystery drugs choose from eligible effects unpredictably. Mercury changes lifespan, while giant and dwarf drugs reshape size, movement, power, and ongoing damage risk. - -## Habitat And Cleanup - -Zunchi is waste, but it is also part of the ecology. It affects hygiene and disease risk and can fertilize grass. Beds and nest boxes give tarinai safer or better sleep options, reduce stress around resting areas, and help recovery. Balls give tarinai something to play with and can also become part of physics experiments. - -Robot cleaners are mobile cleanup items. They scan for selected target types, pursue matching waste or corpses, and remove them on contact. The water hose gives the player direct cleanup control by washing dirt, zunchi, corpses, stains, and similar field problems. Signboards are editable objects that can be inspected after placement, useful for labeling areas or experiments. - -## Physics And Machines - -Physics items turn the field into a mechanical sandbox. Ropes connect two targets with a flexible link that can go slack but should not stretch freely. Rods create a rigid connection. Springs connect targets with elastic motion, allowing linked objects to pull back and oscillate. - -Fences block movement and can be rotated for layout control. Gates can be opened or closed after placement. One-way fences allow passage from one direction while blocking the opposite direction. Bounce fences launch colliding targets more strongly than normal fences, which makes them useful for traps or kinetic experiments. - -Rotators are editable mechanical shapes that spin and push nearby targets. Their shape and speed can create conveyors, crushers, traps, or chaotic collision fields. Reciprocators move back and forth along a line, so they can shove objects repeatedly. Poison blocks are custom hazard shapes that damage on contact. - -## Electrical And Automation - -Detectors, wires, insulated wires, and circuit boards are grouped in the Electrical tool category. Normal energized wire can hurt living targets on contact, while insulated wire is safer for incidental contact. Detectors can sense target ranges or time windows and send signals; clicking either dual-range track moves the nearer thumb directly to the clicked value. Circuit-board external input and output terminals are placed directly on the board edge, can be moved around all four sides, and appear dim in the editor until a world wire is actually connected. - -## Danger Tools - -Danger tools create immediate or ongoing pressure. Stones are heavy objects that become dangerous when dropped, thrown, or moved by machines. Genkotsu creates a large impact on the ground. Firecrackers cause explosions, with flame variants adding fire while respecting active-fire limits. - -Pushpins can roll, lodge in tarinai, cause panic, and deal continuous damage. Oshibyo blocks zunchi while lodged and releases stored zunchi when removed. Shooting is a direct attack tool that can hit tarinai, ants, items, pins, balloons, or placed objects depending on the target under the aim point. - -Ant nests introduce an enemy system. Ants search for tarinai, carry captured targets back to the nest, and can be shot or killed. Worker ants and queen ants have separate HP values, and ant counts are shown through colony statistics. - -## Operation Tools - -The observe tool selects tarinai or compatible objects and opens their data. The add tool introduces new tarinai. Poke applies a small interaction or push, while pinch lets the player drag tarinai and some objects. Copy duplicates placed items with their settings so setups can be repeated quickly. The operation palette also provides a dedicated snap control shared by placement and pinch movement; it cycles through Free, 20px, 40px, and 60px. - -Undo and redo reverse or reapply placement and edit actions. Delete removes a specific object or stain, while area delete removes many nearby targets at once. These tools let the player repair a field after experiments without resetting the whole colony. - -## Grounds And Environment - -Fields define the playable space and boundary style. Grounds change the character of the colony: sand, dirt, concrete, blanket, foot massage, ice, and laboratory-style surfaces can affect movement, stress, comfort, and grass behavior. Ground choice matters because tarinai react not only to objects, but also to the surface they live on. - -Weather changes the field over time. Rain can create water and affect comfort, shade can protect from weather or sun, and temperature changes how tarinai experience the environment. Grass, zunchi, splats, corpses, ant corpses, rain drops, and stains all feed into hygiene, disease, cleanup, rendering, and observation. - -## Colony Statistics - -The colony panel summarizes population, sick tarinai, juveniles, elders, deaths, mood, temperature, population limits, and object limits. Graph tabs show population, needs, and environment. Total ants are included in the environment statistics so active ant pressure can be read alongside other field signals. - -The event panel records important simulation events such as birth, death, fights, accidents, weather, grass changes, and relation changes. The family tree shows parent-child structure, generations, inherited stats, personality tags, and family links. - -## Save, Display, And Performance - -Manual save and restore use browser storage. Reset returns to field selection and starts a new state. Service worker caching can keep the static app shell available after the first successful load when the browser supports it. - -Display settings control effects, shadows, drawing detail, and quality choices for crowded scenes. Crowded fields are an intended part of the sandbox, so the game uses spatial lookup, scheduling, cached drawing, and incremental maintenance systems to keep large colonies and many objects playable. - - -## Achievement system -- Seventy-six achievements track reproduction, natural status changes, generation runs, rapid population events, special deaths, colony conditions, individual care, autonomous ecology, history usage, cleanup, rapid deletion, temperature transitions, weather shelter, medicine-category completion, combat reversals, circuit-board signaling, and ant-related play styles. -- Locked rows reveal only the configured condition allowlist; all others show `???`. There are no achievement tooltips. 母なる大地, めっちゃたりない観察, 敵の敵は味方, 混沌ヲ希求スル者, and 豊穣ヲ希求スル者 include local progress percentages, and unlocked achievements are sorted first inside each category. -- Each achievement row behaves as a horizontal bar chart. Its background fill visualizes the optional global completion percentage from 0–100%, and the global achiever count is displayed beside it. -- `物でいっぱい` requires 100 cumulative player placements across field resets. `害虫駆除` requires 100 cumulative ant kills across field resets. -- `餌やりじいさん` requires 33 cumulative direct feedings; field resets do not reset this progress. -- `機械化産業` replaces the former detector-automation and link-craftsman achievements and unlocks only when both requirements are met. -- Local unlocks and persistent totals live in browser storage. Direct-feeding, Undo, and Redo totals persist across field resets. Cleaner, self-sufficiency, enemy-ant, generation, observation, safety, and per-entity/item progress remain field-scoped where their conditions require it. Nest-box and pipe occupants block rain/weather effects and count as sheltered for 雨宿り完了. -- Achievement evaluation is primarily event-driven; periodic world scans skip completed or irrelevant conditions. The server writes compact v3 achievement aggregate JSON while migrating older aggregate formats on read. - -### Achievement spell persistence -- Exported spells compactly include non-debug achievement unlocks, unlock times, and persistent progress. -- Loading a spell merges achievements and keeps the greater progress instead of erasing local achievement data. -- Restored unlocks are queued for the existing server recovery synchronization. Save slots remain world-only. - -- Spell achievement metadata uses sparse binary encoding for unlock bits, persistent progress, minute-delta unlock timestamps, and sparse per-entity state. The current save and achievement payload formats are defined by `js/save_schema.js` and `js/achievements.js`. diff --git a/README.md b/README.md index c9ab1b4..c719566 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,60 @@ -# tarinai_ +# たりない観察 v39.18.3 -AI-first entrypoint for a static browser simulation/game. Keep this file small; read deeper docs only when needed. +## 実績システム -## Read Strategy +実績定義は `js/achievement_catalog.js` を唯一のカタログとして管理します。表示名、説明、カテゴリ、未解除時の公開設定、同期可能な進捗条件、累積上限は、クライアントと `achievement_api.php` が同じカタログから読み取ります。 -- Start here for architecture, script order constraints, and where to look next. -- For game mechanics, UI, items, and tarinai behavior, read `GAME_FEATURES.md`. -- For subsystem work, read `AI_MAP.md` plus only the matching files. -- For exact file inventory, run `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All` or narrow it: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 ui`. Python equivalent: `python scripts/ai_inventory.py --all`. -- For machine-readable routing, read `FILES.json`. -- Avoid loading all assets into context; asset filenames are mostly self-describing and grouped by glob. +累積進捗の正規データは端末・タブ別の増加台帳です。表示用 `progress` に同じ累積値を重複保存しません。旧形式の重複カウンターと `synced` 一覧は読込時だけ移行し、現行形式では保存しません。 -## Runtime Shape +「敵の敵は味方」は実績画面の「生態」カテゴリに掲載されます。 -- Mutable runtime lives on `window`; primary entities are `World`, `Tarinai`, items, structures, ants, family graph, weather, and save snapshots. -- Update loop: `main.js` creates the world/UI loop; `World.update` delegates to `TarinaiSystemOrder.update`; `system_order.js` runs named phases. -- UI loop: pointer/tool/UI intents go through `command_dispatcher.js`; canvas drawing is in render modules; side panels are `ui_*.js`. -- Generated/static app shell: `app_manifest.json`, `service-worker.js`, `FILES.json`, and `js/version.js` must stay version-aligned. +## ローカル保存 -## File Routing +実績状態とサーバー資格情報は、通常保存、予備保存、同一タブ保存、IndexedDBへ世代付きで保存します。起動時は同じ世代の状態を追加方向に統合し、明示的な実績リセット時だけ世代を進めます。 -- `index.html`: DOM shell, CSS/JS load order, panels/dialogs/canvas. -- `css/*.css`: base/layout/panel/components/mobile styling. -- `js/domain_ids.js`, `js/item_type_catalog.js`, `js/data.js`, `js/ground_types.js`: stable domain ids, current item-type save catalog, core config, field/ground definitions. -- `js/world*.js`: world state, view, update phases, tools, placement, combat, environment, family/social, ants. -- `js/tarinai*.js`: creature state, needs, actions, behavior, social life, item effects, update pipeline, rendering. -- `js/item*.js`, `js/structures*.js`: item registry/runtime/lifecycle/dynamic behavior/rendering plus structures. -- `js/simulation*.js`, `js/system_order.js`, `js/sim_core.js`: concrete update systems and phase ordering. -- `js/ui*.js`, `js/text_catalog.js`, `js/command_dispatcher.js`: DOM UI, input, selected panel, logs, charts, family tree, tools, labels. -- `js/save*.js`, `js/snapshot_system.js`, `js/restore_coordinator.js`, `js/history_system.js`: persistence and history. -- `js/physics*.js`, `js/mechanical_system.js`, `js/constraint_system.js`, `js/collision_footprint_system.js`: geometry, mechanics, constraints, collisions. -- `assets/sprites/tarinai_*.webp`: creature state sprites. -- `assets/ui/tool_*`: tool palette icons; other `assets/ui/*` are app/help icons. -- `assets/objects/*`: field object images. -- `assets/sounds/*`: voice and SFX samples. -- `scripts/*`: generators, regression checks, and AI inventory. +v39.18.3は、v39.18.0以前の実績保存、旧ワールド実績メタデータ、実績スペルv9・v10、旧バイナリセーブを読み込めます。旧データは現行形式へ正規化され、新規保存には廃止済みフィールドを書きません。 -## Conventions +## サーバー同期 -- `*_pipeline` = ordered runner/facade. -- `*_step_*` = pipeline stage wrapper. -- `*_system` = domain service or compatibility facade. -- `*_runtime` = stable runtime-facing facade. -- `*_registry` = definition lookup/normalization. -- Many split files preserve regression contracts; do not collapse them casually. +同期プロトコルはv5です。 -## Checks +- 解除状態と途中進捗を追加型で統合 +- 同じ端末台帳の再送を重複加算しない +- リセット要求の再送を無害化 +- 古いサーバーバックアップから新しいローカル世代を復旧 +- 古い端末からリセット前の状態を復活させない +- プレイヤーIDと所有秘密鍵を耐久保存から復旧 -- Syntax: `node --check js/*.js` -- Regression: `python scripts/regression_check.py` -- Inventory sanity: `powershell -NoProfile -ExecutionPolicy Bypass -File scripts/ai_inventory.ps1 -All` +共有達成率は、サーバーがゲームシミュレーションを再実行した検証値ではなく、クライアント端末の報告値です。UIとAPIでは「端末報告ベース」と明示します。 + +## 旧サーバーデータの自動移行 + +データディレクトリ、またはアプリ直下の `.achievement_data` に旧コンパクト形式の `state.json`(スキーマ4)がある場合、最初のAPI要求時に現行形式へ自動移行します。環境変数で新保存先を指定した場合や、v39.18.1が空の現行保存先を先に作成済みの場合も検出します。 + +- 原本は `legacy-backup/` へSHA-256付きで保存 +- 旧プレイヤーIDから決定的なUUIDを生成 +- プレイヤー数、解除実績、解除時刻、実績別集計を移行 +- 旧データに存在しない途中進捗は0から開始 +- `legacy-v4-migration.json` に完了件数を記録 +- 中断時は次のAPI要求で冪等に再開 +- 切替中にコピー元の旧 `state.json` が更新された場合も、内容ハッシュを再確認して追加方向で再取込 +- ファイルサイズとmtimeが同じ更新もSHA-256比較で検出 + +旧ブラウザーに22文字のプレイヤーIDが残っている場合、クライアントはUUIDへ上書きする前に専用領域へ退避し、初回セッションで一度だけ新しい所有秘密鍵へ引き継ぎます。引継ぎ済みの旧IDを別の秘密鍵で再取得することはできません。 + +旧版から直接v39.18.3へ更新したブラウザーは自動引継ぎできます。v39.18.1を既に起動して旧22文字IDがブラウザーから失われている場合、サーバーデータだけから端末との本人対応を再構成することはできません。解除履歴と共有集計自体は正常に移行されます。 + +## 実績APIの保存先 + +本番環境では、PHP実行ユーザーが書き込める非公開ディレクトリを用意し、環境変数 `TARINAI_ACHIEVEMENT_DATA_DIR` に絶対パスを設定してください。未設定時は、HTTP公開ディレクトリ外の安定したユーザーデータ領域を使用します。 -### Shared achievement percentage deployment -- Upload `achievement_api.php` and the hidden `.achievement_data` directory with the game. -- PHP must be enabled, and `.achievement_data` must be writable by the PHP process. -- On a static-only host, local unlocks still work; the global percentage is shown as unavailable. -- Set `window.TARINAI_ACHIEVEMENT_API` before `achievements.js` to use a different same-origin endpoint. -- The current release tracks 76 achievement IDs, supports per-player reset, and accepts additive bulk synchronization for automatic recovery after aggregate-data loss. A separate backup remains necessary to recover players who never return. -- `python3 scripts/achievement_server_state_audit.py` creates deterministic randomized achievement histories at runtime; it does not require or modify a production `state.json`. +## PHP組み込みサーバーでの起動 + +PHP組み込みサーバーは `.htaccess` を解釈しません。旧 `.achievement_data/state.json` には引継ぎ用IDが含まれるため、必ず同梱のルーターを使用してください。 + +```sh +./start_server.sh 8000 +``` + +Windowsでは `start_server.bat 8000` を使用します。本番環境では引き続き `TARINAI_ACHIEVEMENT_DATA_DIR` をHTTP公開ディレクトリ外へ設定してください。 diff --git a/achievement_api.php b/achievement_api.php index cedb961..22fbb63 100644 --- a/achievement_api.php +++ b/achievement_api.php @@ -4,91 +4,63 @@ declare(strict_types=1); header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('X-Content-Type-Options: nosniff'); +header('Referrer-Policy: no-referrer'); +header('X-Frame-Options: DENY'); + +function load_achievement_catalog(): array { + $path = __DIR__ . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . 'achievement_catalog.js'; + $raw = @file_get_contents($path); + if (!is_string($raw)) throw new RuntimeException('achievement_catalog_unavailable'); + $prefix = 'window.TARINAI_ACHIEVEMENT_CATALOG = '; + $trimmed = trim($raw); + if (!str_starts_with($trimmed, $prefix) || !str_ends_with($trimmed, ';')) { + throw new RuntimeException('achievement_catalog_invalid'); + } + $json = substr($trimmed, strlen($prefix), -1); + try { $catalog = json_decode($json, true, 512, JSON_THROW_ON_ERROR); } + catch (JsonException) { throw new RuntimeException('achievement_catalog_invalid'); } + if (!is_array($catalog) || !is_array($catalog['definitions'] ?? null)) { + throw new RuntimeException('achievement_catalog_invalid'); + } + return $catalog; +} + +$achievementCatalog = load_achievement_catalog(); +$achievementDefinitions = []; +$achievementIds = []; +$progressRules = []; +foreach ($achievementCatalog['definitions'] as $definition) { + if (!is_array($definition)) continue; + $id = (string)($definition['id'] ?? ''); + if ($id === '' || isset($achievementDefinitions[$id])) throw new RuntimeException('achievement_catalog_invalid'); + $achievementDefinitions[$id] = $definition; + $achievementIds[] = $id; + if (is_array($definition['progress'] ?? null)) $progressRules[$id] = $definition['progress']; +} +define('ACHIEVEMENT_IDS', $achievementIds); +define('ACHIEVEMENT_DEFINITIONS', $achievementDefinitions); +define('PROGRESS_RULES', $progressRules); +define('COMPLETIONIST_ID', (string)($achievementCatalog['completionistId'] ?? 'true_tarinai_observer')); +define('CATALOG_REVISION', (string)($achievementCatalog['revision'] ?? 'unknown')); +define('ADDITIVE_PROGRESS_CAPS', is_array($achievementCatalog['additiveProgressCaps'] ?? null) ? $achievementCatalog['additiveProgressCaps'] : []); +define('MEDICINE_LEDGER_TYPES', is_array($achievementCatalog['medicineLedgerTypes'] ?? null) ? array_values($achievementCatalog['medicineLedgerTypes']) : []); +define('LINK_PROGRESS_TYPES', is_array($achievementCatalog['linkProgressTypes'] ?? null) ? array_values($achievementCatalog['linkProgressTypes']) : []); +define('MAD_SCIENTIST_TARGET', max(1, (int)($progressRules['mad_scientist']['target'] ?? 10))); -const ACHIEVEMENT_IDS = [ - 'first_birth', - 'natural_zunchi_slave', - 'natural_tarinai_king', - 'self_zunchi_death', - 'zunchi_collision_death', - 'natural_zunchi_slave_5_generations', - 'natural_tarinai_king_3_generations', - 'death_50_in_10_seconds', - 'birth_50_in_60_seconds', - 'great_mother_1000_births', - 'lifespan_completed', - 'soccer_ball_death', - 'fight_pair_danger_kill', - 'all_non_sleep_diseased_25', - 'colony_happy', - 'direct_feed_33', - 'ignite_during_birth_ritual', - 'laxative_starvation', - 'idle_observer_5_minutes', - 'placed_objects_100', - 'mechanized_industry', - 'safe_colony_25_5_minutes', - 'ants_alive_25', - 'ants_killed_100', - 'secret_collection_9_slots', - 'pause_spam_4_in_1_second', - 'continuous_play_1_hour', - 'below_absolute_zero_item', - 'undo_mass_revival', - 'robot_cleaner_100', - 'held_30_seconds', - 'minimalist_happy', - 'overprotective', - 'self_sufficient', - 'unplanned_city_30', - 'sauna_cold_plunge', - 'rain_shelter_all', - 'medicine_ledger_all', - 'mercury_lifespan', - 'enemy_enemy_friend', - 'revolution', - 'fuel_to_fire', - 'undo_20', - 'redo_20', - 'eternal_history_generation_10', - 'king_full_satisfaction', - 'slave_zero_satisfaction', - 'town_doctor_50', - 'poke_plushie_fling', - 'chaos_seeker_666_fights', - 'clean_freak_robot_only', - 'true_tarinai_observer', - 'sniper_333_shots', - 'megalopolis', - 'fertility_seeker_721_love_births', - 'low_fps_single_digit', - 'continuous_play_24_hours', - 'sandbox_five_toilets', - 'park_ground_changed', - 'ground_change_4_in_1_second', - 'ant_nest_without_tarinai', - 'sticky_bomb_15_passes', - 'daily_play_7_days', - 'wire_shock_7_tarinai', - 'information_industry', - 'strength_in_numbers', - 'elite_few', - 'zunchi_overflow', - 'comfortable_beds', - 'stone_pillow', - 'across_seasons', - 'favorite_one', - 'statistician', - 'well_informed', - 'lively_making', - 'memento_mori', - 'mad_scientist', -]; -const DATA_DIR_NAME = '.achievement_data'; -const DATA_FILE_NAME = 'state.json'; -const LOCK_FILE_NAME = 'state.lock'; -const STATE_SCHEMA_VERSION = 3; const MAX_REQUEST_BYTES = 131072; +const RATE_WINDOW_SECONDS = 60; +const RATE_READ_LIMIT = 240; +const RATE_SESSION_IP_LIMIT = 120; +const RATE_WRITE_IP_LIMIT = 600; +const RATE_PLAYER_WRITE_LIMIT = 120; +const QUALIFY_MIN_ACTIVITY = 2; +const QUALIFY_MIN_AGE_SECONDS = 30; +const SESSION_COOKIE_NAME = 'tarinai_achievement_session_v2'; +const SESSION_COOKIE_MAX_AGE = 31536000; +const IDENTITY_ISSUE_WINDOW_SECONDS = 86400; +const IDENTITY_ISSUE_LIMIT_PER_IP = 512; +const SYNC_PROTOCOL_VERSION = 5; + function respond(array $payload, int $status = 200): never { http_response_code($status); @@ -97,266 +69,962 @@ function respond(array $payload, int $status = 200): never { } function valid_player_id(string $value): bool { - return preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $value) === 1; + return preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value) === 1; } -function empty_state(): array { - return ['players' => [], 'unlocks' => []]; +function valid_player_secret(string $value): bool { + return preg_match('/^[0-9a-f]{64}$/i', $value) === 1; } -function compact_player_id(string $value): string { - $hex = str_replace('-', '', strtolower($value)); - if (strlen($hex) !== 32 || !ctype_xdigit($hex)) return ''; - $binary = hex2bin($hex); - if ($binary === false) return ''; - return rtrim(strtr(base64_encode($binary), '+/', '-_'), '='); +function client_ip(): string { + return substr((string)($_SERVER['REMOTE_ADDR'] ?? 'unknown'), 0, 128); } -function expand_player_id(string $value): string { - if (strlen($value) !== 22 || preg_match('/^[A-Za-z0-9_-]{22}$/', $value) !== 1) return ''; - $binary = base64_decode(strtr($value, '-_', '+/') . '==', true); - if ($binary === false || strlen($binary) !== 16) return ''; - $hex = bin2hex($binary); - return substr($hex, 0, 8) . '-' . substr($hex, 8, 4) . '-' . substr($hex, 12, 4) . '-' . substr($hex, 16, 4) . '-' . substr($hex, 20, 12); +function directory_has_achievement_data(string $path): bool { + return is_file($path . DIRECTORY_SEPARATOR . 'state.json') + || is_file($path . DIRECTORY_SEPARATOR . 'summary.json') + || is_file($path . DIRECTORY_SEPARATOR . 'session-key.json') + || is_dir($path . DIRECTORY_SEPARATOR . 'players'); } -function detect_state_format(mixed $value): string { - if (!is_array($value)) return 'invalid'; - if ($value === []) return 'empty'; - if (array_is_list($value)) { - return (($value[0] ?? null) === STATE_SCHEMA_VERSION - && isset($value[1], $value[2], $value[3], $value[4]) - && is_array($value[2]) && is_array($value[3]) && is_array($value[4])) - ? 'v3' : 'invalid'; +function copy_directory_tree(string $source, string $target): void { + ensure_directory($target); + $items = @scandir($source); + if (!is_array($items)) throw new RuntimeException('storage_unavailable'); + foreach ($items as $name) { + if ($name === '.' || $name === '..') continue; + $from = $source . DIRECTORY_SEPARATOR . $name; + $to = $target . DIRECTORY_SEPARATOR . $name; + if (is_dir($from)) copy_directory_tree($from, $to); + elseif (is_file($from) && !is_file($to) && !@copy($from, $to)) throw new RuntimeException('storage_write_failed'); } - if (($value['v'] ?? null) === 2 && isset($value['p'], $value['u']) && is_array($value['p']) && is_array($value['u'])) { - return 'v2'; - } - if (array_key_exists('players', $value) || array_key_exists('unlocks', $value)) return 'verbose'; - return 'invalid'; } -function normalize_state(mixed $value): array { - if (!is_array($value)) return empty_state(); - $players = []; - $unlocks = []; - - // v3 positional form: [version, baseTime, versions, players, unlocks]. - if (($value[0] ?? null) === STATE_SCHEMA_VERSION && isset($value[1], $value[2], $value[3], $value[4]) - && is_array($value[2]) && is_array($value[3]) && is_array($value[4])) { - $baseTime = max(1, (int)$value[1]); - $versions = array_map(static fn($entry): string => substr((string)$entry, 0, 32), $value[2]); - $playerIds = []; - foreach ($value[3] as $row) { - if (!is_array($row) || !isset($row[0]) || !is_string($row[0])) continue; - $id = expand_player_id($row[0]); - if (!valid_player_id($id)) continue; - $firstSeen = $baseTime + max(0, (int)($row[1] ?? 0)); - $lastSeen = $firstSeen + max(0, (int)($row[2] ?? 0)); - $versionIndex = max(0, (int)($row[3] ?? 0)); - $playerIds[] = $id; - $players[$id] = [ - 'firstSeen' => max(1, $firstSeen), - 'lastSeen' => max(1, $lastSeen), - 'gameVersion' => $versions[$versionIndex] ?? '', - ]; - } - foreach ($value[4] as $row) { - if (!is_array($row) || count($row) < 3) continue; - $achievementIndex = (int)$row[0]; - $achievementId = ACHIEVEMENT_IDS[$achievementIndex] ?? ''; - if ($achievementId === '') continue; - $records = []; - $count = count($row); - for ($i = 1; $i + 1 < $count; $i += 2) { - $playerIndex = (int)$row[$i]; - if (!isset($playerIds[$playerIndex])) continue; - $records[$playerIds[$playerIndex]] = $baseTime + max(0, (int)$row[$i + 1]); - } - if ($records) $unlocks[$achievementId] = $records; - } - // v2 is an on-read migration source and is always rewritten as v3. - } elseif (($value['v'] ?? null) === 2 && isset($value['p'], $value['u']) && is_array($value['p']) && is_array($value['u'])) { - $playerIds = []; - foreach ($value['p'] as $row) { - if (!is_array($row) || !isset($row[0]) || !is_string($row[0]) || !valid_player_id($row[0])) continue; - $id = strtolower($row[0]); - $playerIds[] = $id; - $players[$id] = [ - 'firstSeen' => max(1, (int)($row[1] ?? 1)), - 'lastSeen' => max(1, (int)($row[2] ?? ($row[1] ?? 1))), - 'gameVersion' => substr((string)($row[3] ?? ''), 0, 32), - ]; - } - foreach ($value['u'] as $achievementId => $flat) { - if (!is_string($achievementId) || !in_array($achievementId, ACHIEVEMENT_IDS, true) || !is_array($flat)) continue; - $records = []; - $count = count($flat); - for ($i = 0; $i + 1 < $count; $i += 2) { - $playerIndex = (int)$flat[$i]; - if (!isset($playerIds[$playerIndex])) continue; - $records[$playerIds[$playerIndex]] = max(1, (int)$flat[$i + 1]); - } - if ($records) $unlocks[$achievementId] = $records; - } - } else { - $rawPlayers = isset($value['players']) && is_array($value['players']) ? $value['players'] : []; - foreach ($rawPlayers as $id => $player) { - if (!is_string($id) || !valid_player_id($id) || !is_array($player)) continue; - $players[strtolower($id)] = [ - 'firstSeen' => max(1, (int)($player['firstSeen'] ?? 1)), - 'lastSeen' => max(1, (int)($player['lastSeen'] ?? ($player['firstSeen'] ?? 1))), - 'gameVersion' => substr((string)($player['gameVersion'] ?? ''), 0, 32), - ]; - } - $rawUnlocks = isset($value['unlocks']) && is_array($value['unlocks']) ? $value['unlocks'] : []; - foreach ($rawUnlocks as $achievementId => $records) { - if (!is_string($achievementId) || !in_array($achievementId, ACHIEVEMENT_IDS, true) || !is_array($records)) continue; - foreach ($records as $id => $timestamp) { - $normalizedId = strtolower((string)$id); - if (!isset($players[$normalizedId])) continue; - $unlocks[$achievementId][$normalizedId] = max(1, (int)$timestamp); - } - } - } - - - return ['players' => $players, 'unlocks' => $unlocks]; +function legacy_state_snapshot(string $path): ?array { + if (!is_file($path)) return null; + $bytes = @file_get_contents($path); + if (!is_string($bytes) || trim($bytes) === '') throw new RuntimeException('legacy_state_unreadable'); + return [ + 'path' => $path, + 'bytes' => $bytes, + 'hash' => hash('sha256', $bytes), + 'size' => strlen($bytes), + 'mtime' => max(0, (int)@filemtime($path)), + ]; } -function compact_state(array $state): array { - $playerIds = array_keys($state['players']); - sort($playerIds, SORT_STRING); - $playerIndex = []; - $timestamps = []; - $versionCounts = []; - foreach ($playerIds as $id) { - $player = is_array($state['players'][$id] ?? null) ? $state['players'][$id] : []; - $timestamps[] = max(1, (int)($player['firstSeen'] ?? 1)); - $timestamps[] = max(1, (int)($player['lastSeen'] ?? ($player['firstSeen'] ?? 1))); - $version = substr((string)($player['gameVersion'] ?? ''), 0, 32); - $versionCounts[$version] = ($versionCounts[$version] ?? 0) + 1; +function remember_legacy_state_source(string $target, array $source): void { + atomic_write_json($target . DIRECTORY_SEPARATOR . 'legacy-v4-source.json', [ + 'schema' => 1, + 'sourcePath' => (string)$source['path'], + 'sourceHash' => (string)$source['hash'], + 'sourceSize' => (int)$source['size'], + 'sourceMtime' => (int)$source['mtime'], + 'copiedAt' => time(), + ]); +} + +function copy_legacy_state_snapshot(string $target, array $source): void { + atomic_write_bytes($target . DIRECTORY_SEPARATOR . 'state.json', (string)$source['bytes']); + remember_legacy_state_source($target, $source); +} + +function migrate_legacy_data_root(string $target, array $legacyRoots): void { + $targetCurrent = is_file($target . DIRECTORY_SEPARATOR . 'summary.json') + || is_file($target . DIRECTORY_SEPARATOR . 'session-key.json') + || is_dir($target . DIRECTORY_SEPARATOR . 'players'); + $targetStatePath = $target . DIRECTORY_SEPARATOR . 'state.json'; + $targetState = legacy_state_snapshot($targetStatePath); + + $candidates = []; + $stateCandidates = []; + foreach ($legacyRoots as $legacy) { + $path = rtrim((string)$legacy, DIRECTORY_SEPARATOR); + if ($path === '' || $path === $target || !is_dir($path) || !directory_has_achievement_data($path)) continue; + $directoryMtime = max( + (int)@filemtime($path), + (int)@filemtime($path . DIRECTORY_SEPARATOR . 'state.json'), + (int)@filemtime($path . DIRECTORY_SEPARATOR . 'summary.json') + ); + $candidates[$path] = $directoryMtime; + $snapshot = legacy_state_snapshot($path . DIRECTORY_SEPARATOR . 'state.json'); + if ($snapshot !== null) $stateCandidates[$path] = $snapshot; } - foreach ($state['unlocks'] as $achievementId => $records) { - if (!is_string($achievementId) || !in_array($achievementId, ACHIEVEMENT_IDS, true) || !is_array($records)) continue; - foreach ($records as $timestamp) $timestamps[] = max(1, (int)$timestamp); - } - $baseTime = $timestamps ? min($timestamps) : 1; - uksort($versionCounts, static function (string $a, string $b) use ($versionCounts): int { - $countOrder = ($versionCounts[$b] ?? 0) <=> ($versionCounts[$a] ?? 0); - return $countOrder !== 0 ? $countOrder : strcmp($a, $b); + arsort($candidates, SORT_NUMERIC); + uasort($stateCandidates, static function (array $a, array $b): int { + $mtime = ((int)$b['mtime']) <=> ((int)$a['mtime']); + if ($mtime !== 0) return $mtime; + return ((int)$b['size']) <=> ((int)$a['size']); }); - $versions = array_keys($versionCounts); - if (!$versions) $versions = ['']; - $versionIndex = array_flip($versions); - $players = []; - foreach ($playerIds as $index => $id) { - $playerIndex[$id] = $index; - $player = is_array($state['players'][$id] ?? null) ? $state['players'][$id] : []; - $firstSeen = max(1, (int)($player['firstSeen'] ?? 1)); - $lastSeen = max($firstSeen, (int)($player['lastSeen'] ?? $firstSeen)); - $version = substr((string)($player['gameVersion'] ?? ''), 0, 32); - $row = [compact_player_id($id), $firstSeen - $baseTime, $lastSeen - $firstSeen]; - $indexValue = (int)($versionIndex[$version] ?? 0); - if ($indexValue !== 0) $row[] = $indexValue; - $players[] = $row; + // When a compact source was copied into a private current root, keep + // comparing the original source on later requests. This captures final + // writes made by the legacy server during cut-over instead of freezing the + // first copied snapshot forever. + if ($targetState !== null && $stateCandidates) { + $sourceMarker = read_json_file($target . DIRECTORY_SEPARATOR . 'legacy-v4-source.json', []); + $migrationMarker = read_json_file($target . DIRECTORY_SEPARATOR . 'legacy-v4-migration.json', []); + $preferredPath = (string)($sourceMarker['sourcePath'] ?? ''); + $source = $preferredPath !== '' && isset($stateCandidates[dirname($preferredPath)]) + ? $stateCandidates[dirname($preferredPath)] + : reset($stateCandidates); + if (is_array($source) && !hash_equals((string)$targetState['hash'], (string)$source['hash'])) { + $trackedSource = $preferredPath !== '' && (string)$source['path'] === $preferredPath; + $targetIsLastImportedSnapshot = hash_equals( + strtolower((string)($migrationMarker['sourceHash'] ?? '')), + strtolower((string)$targetState['hash']) + ); + $sourceIsNotOlder = (int)$source['mtime'] >= (int)$targetState['mtime']; + if ($trackedSource || $targetIsLastImportedSnapshot || $sourceIsNotOlder) { + copy_legacy_state_snapshot($target, $source); + $targetState = $source; + } + } + return; } - $unlocks = []; - foreach (ACHIEVEMENT_IDS as $achievementIndex => $achievementId) { - $records = $state['unlocks'][$achievementId] ?? null; - if (!is_array($records) || !$records) continue; - $pairs = []; - foreach ($records as $id => $timestamp) { - if (!isset($playerIndex[$id])) continue; - $pairs[] = [$playerIndex[$id], max(1, (int)$timestamp) - $baseTime]; - } - usort($pairs, static fn(array $a, array $b): int => $a[0] <=> $b[0]); - if (!$pairs) continue; - $row = [$achievementIndex]; - foreach ($pairs as [$indexValue, $timestampDelta]) { - $row[] = $indexValue; - $row[] = max(0, $timestampDelta); - } - $unlocks[] = $row; + if (!$candidates) return; + + // A prior current build may already have created an empty summary/player + // tree. Copy the newest compact source into it so the importer can merge it + // without discarding current records. + if ($targetCurrent) { + $source = $stateCandidates ? reset($stateCandidates) : null; + if (is_array($source)) copy_legacy_state_snapshot($target, $source); + return; } - return [STATE_SCHEMA_VERSION, $baseTime, $versions, $players, $unlocks]; + + $sourcePath = (string)array_key_first($candidates); + ensure_directory(dirname($target)); + if (isset($stateCandidates[$sourcePath])) { + copy_directory_tree($sourcePath, $target); + remember_legacy_state_source($target, $stateCandidates[$sourcePath]); + return; + } + if (!file_exists($target) && @rename($sourcePath, $target)) { + // A separate compact source may coexist with a current-format legacy + // directory. Merge the compact snapshot as well. + $stateSource = $stateCandidates ? reset($stateCandidates) : null; + if (is_array($stateSource)) copy_legacy_state_snapshot($target, $stateSource); + return; + } + copy_directory_tree($sourcePath, $target); + $stateSource = $stateCandidates ? reset($stateCandidates) : null; + if (is_array($stateSource) && !is_file($targetStatePath)) copy_legacy_state_snapshot($target, $stateSource); } -function encode_compact_state(array $state): string { +function data_root(): string { + $configured = trim((string)(getenv('TARINAI_ACHIEVEMENT_DATA_DIR') ?: '')); + if ($configured !== '') { + $target = rtrim($configured, DIRECTORY_SEPARATOR); + migrate_legacy_data_root($target, [ + __DIR__ . DIRECTORY_SEPARATOR . '.achievement_data', + dirname(__DIR__) . DIRECTORY_SEPARATOR . '.achievement_data', + ]); + return $target; + } + + $documentRoot = realpath((string)($_SERVER['DOCUMENT_ROOT'] ?? '')); + $publicRoot = is_string($documentRoot) && $documentRoot !== '' ? $documentRoot : __DIR__; + $installPath = realpath(__DIR__) ?: __DIR__; + $installKey = substr(hash('sha256', $installPath), 0, 16); + $legacyParent = dirname($publicRoot); + $legacyRoots = [ + $legacyParent . DIRECTORY_SEPARATOR . '.tarinai-achievements-' . $installKey, + __DIR__ . DIRECTORY_SEPARATOR . '.achievement_data', + $publicRoot . DIRECTORY_SEPARATOR . '.achievement_data', + ]; + $globbed = glob($legacyParent . DIRECTORY_SEPARATOR . '.tarinai-achievements-*', GLOB_ONLYDIR); + if (is_array($globbed)) $legacyRoots = array_merge($legacyRoots, $globbed); + + // Keep local-server data independent of the ZIP extraction directory and + // listening port. This makes upgrades and side-by-side extracted versions + // share one persistent achievement identity and server record. + $xdg = trim((string)(getenv('XDG_DATA_HOME') ?: '')); + $home = trim((string)(getenv('HOME') ?: getenv('USERPROFILE') ?: '')); + if ($xdg !== '') $target = rtrim($xdg, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'tarinai-observer' . DIRECTORY_SEPARATOR . 'achievements'; + elseif ($home !== '') $target = rtrim($home, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.tarinai-observer' . DIRECTORY_SEPARATOR . 'achievements'; + else $target = $legacyParent . DIRECTORY_SEPARATOR . '.tarinai-achievements'; + try { - return json_encode(compact_state($state), JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR); - } catch (JsonException $error) { - throw new RuntimeException('storage_encode_failed', 0, $error); + migrate_legacy_data_root($target, $legacyRoots); + return $target; + } catch (RuntimeException) { + // Hosted environments may not permit writes under HOME. The fallback + // remains outside the document root and is stable across sibling builds. + $fallback = $legacyParent . DIRECTORY_SEPARATOR . '.tarinai-achievements'; + migrate_legacy_data_root($fallback, $legacyRoots); + return $fallback; } } -function decode_stored_state(string $contents): array { - if (trim($contents) === '') return [empty_state(), 'empty']; +function ensure_directory(string $path): void { + if (!is_dir($path) && !mkdir($path, 0770, true) && !is_dir($path)) throw new RuntimeException('storage_unavailable'); +} + + +function protect_data_root(string $path): void { + ensure_directory($path); + $rules = "Options -Indexes\n\n Require all denied\n\n\n Deny from all\n\n"; + $htaccess = $path . DIRECTORY_SEPARATOR . '.htaccess'; + if (!is_file($htaccess)) @file_put_contents($htaccess, $rules, LOCK_EX); + $index = $path . DIRECTORY_SEPARATOR . 'index.html'; + if (!is_file($index)) @file_put_contents($index, '', LOCK_EX); +} + +function read_json_file(string $path, array $fallback): array { + if (!is_file($path)) return $fallback; + $raw = file_get_contents($path); + if (!is_string($raw)) throw new RuntimeException('storage_unavailable'); + if (trim($raw) === '') return $fallback; try { - $decoded = json_decode($contents, true, 512, JSON_THROW_ON_ERROR); + $value = json_decode($raw, true, 128, JSON_THROW_ON_ERROR); } catch (JsonException $error) { throw new RuntimeException('storage_corrupt', 0, $error); } - $format = detect_state_format($decoded); - if ($format === 'invalid') throw new RuntimeException('storage_corrupt'); - return [normalize_state($decoded), $format]; + return is_array($value) ? $value : $fallback; } -function atomic_write_state(string $dataPath, string $encoded): void { - $dataDir = dirname($dataPath); - $tempPath = tempnam($dataDir, DATA_FILE_NAME . '.tmp.'); - if ($tempPath === false) throw new RuntimeException('storage_unavailable'); - $handle = @fopen($tempPath, 'wb'); - if ($handle === false) { - @unlink($tempPath); - throw new RuntimeException('storage_unavailable'); - } +function atomic_write_json(string $path, array $value): void { + ensure_directory(dirname($path)); + try { + $encoded = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_THROW_ON_ERROR); + } catch (JsonException $error) { + throw new RuntimeException('storage_encode_failed', 0, $error); + } + $temp = tempnam(dirname($path), basename($path) . '.tmp.'); + if ($temp === false) throw new RuntimeException('storage_unavailable'); + $handle = @fopen($temp, 'wb'); + if ($handle === false) { @unlink($temp); throw new RuntimeException('storage_unavailable'); } try { - $length = strlen($encoded); $offset = 0; + $length = strlen($encoded); while ($offset < $length) { $written = fwrite($handle, substr($encoded, $offset)); if ($written === false || $written === 0) throw new RuntimeException('storage_write_failed'); $offset += $written; } if (!fflush($handle)) throw new RuntimeException('storage_write_failed'); - if (function_exists('fsync') && !fsync($handle)) throw new RuntimeException('storage_write_failed'); + if (function_exists('fsync')) @fsync($handle); } catch (Throwable $error) { - fclose($handle); - @unlink($tempPath); - throw $error; + fclose($handle); @unlink($temp); throw $error; } fclose($handle); - @chmod($tempPath, 0664); - if (!@rename($tempPath, $dataPath)) { - @unlink($tempPath); - throw new RuntimeException('storage_write_failed'); - } + @chmod($temp, 0660); + if (!@rename($temp, $path)) { @unlink($temp); throw new RuntimeException('storage_write_failed'); } } -function build_summary(array $state): array { - $total = count($state['players']); - $achievements = []; - foreach (ACHIEVEMENT_IDS as $id) { - $unlocked = isset($state['unlocks'][$id]) && is_array($state['unlocks'][$id]) - ? count($state['unlocks'][$id]) - : 0; - $percentage = $total > 0 ? round(($unlocked / $total) * 100, 1) : 0.0; - $achievements[$id] = [ - 'unlockedPlayers' => $unlocked, - 'percentage' => $percentage, - ]; + +function atomic_write_bytes(string $path, string $bytes): void { + ensure_directory(dirname($path)); + $temp = tempnam(dirname($path), basename($path) . '.tmp.'); + if ($temp === false) throw new RuntimeException('storage_unavailable'); + $handle = @fopen($temp, 'wb'); + if ($handle === false) { @unlink($temp); throw new RuntimeException('storage_unavailable'); } + try { + $offset = 0; + $length = strlen($bytes); + while ($offset < $length) { + $written = fwrite($handle, substr($bytes, $offset)); + if ($written === false || $written === 0) throw new RuntimeException('storage_write_failed'); + $offset += $written; + } + if (!fflush($handle)) throw new RuntimeException('storage_write_failed'); + if (function_exists('fsync')) @fsync($handle); + } catch (Throwable $error) { + fclose($handle); @unlink($temp); throw $error; } + fclose($handle); + @chmod($temp, 0660); + if (!@rename($temp, $path)) { @unlink($temp); throw new RuntimeException('storage_write_failed'); } +} + +function valid_legacy_player_id(string $value): bool { + return preg_match('/^[A-Za-z0-9_-]{22}$/', $value) === 1; +} + +function legacy_player_uuid(string $legacyId): string { + if (!valid_legacy_player_id($legacyId)) throw new RuntimeException('legacy_state_invalid'); + $decoded = base64_decode(strtr($legacyId, '-_', '+/') . '==', true); + if (!is_string($decoded) || strlen($decoded) !== 16) throw new RuntimeException('legacy_state_invalid'); + $bytes = $decoded; + $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40); + $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); + $hex = bin2hex($bytes); + return substr($hex, 0, 8) . '-' . substr($hex, 8, 4) . '-' . substr($hex, 12, 4) . '-' . substr($hex, 16, 4) . '-' . substr($hex, 20, 12); +} + +/** + * Imports the compact schema-4 state.json used by the pre-credential server. + * The source remains untouched and is copied byte-for-byte into legacy-backup. + * Player writes are deterministic and idempotent, so a process interruption is + * safely resumed on the next request before the completion marker is written. + */ +function migrate_legacy_compact_state_v4(string $root, string $summaryPath): ?array { + $sourcePath = $root . DIRECTORY_SEPARATOR . 'state.json'; + if (!is_file($sourcePath)) return null; + $markerPath = $root . DIRECTORY_SEPARATOR . 'legacy-v4-migration.json'; + $marker = read_json_file($markerPath, []); + $sourceSize = max(0, (int)@filesize($sourcePath)); + $sourceMtime = max(0, (int)@filemtime($sourcePath)); + $sourceRaw = @file_get_contents($sourcePath); + if (!is_string($sourceRaw) || trim($sourceRaw) === '') throw new RuntimeException('legacy_state_unreadable'); + $sourceHash = hash('sha256', $sourceRaw); + if (($marker['status'] ?? '') === 'complete' + && hash_equals((string)($marker['sourceHash'] ?? ''), $sourceHash)) { + $marker['sourceSize'] = $sourceSize; + $marker['sourceMtime'] = $sourceMtime; + atomic_write_json($markerPath, $marker); + return $marker; + } + // If the legacy server wrote a final update during cut-over, import the + // changed snapshot again. The merge is grow-only and deterministic. + + try { + $data = json_decode($sourceRaw, true, 256, JSON_THROW_ON_ERROR); + } catch (JsonException $error) { + throw new RuntimeException('legacy_state_invalid', 0, $error); + } + if (!is_array($data) || count($data) !== 6 || ($data[0] ?? null) !== 4) throw new RuntimeException('legacy_state_invalid'); + $baseTimestamp = $data[1] ?? null; + $versions = $data[2] ?? null; + $catalogIds = $data[3] ?? null; + $playerRows = $data[4] ?? null; + $unlockRows = $data[5] ?? null; + if (!is_int($baseTimestamp) || $baseTimestamp < 946684800 || !is_array($versions) || !$versions + || !is_array($catalogIds) || !is_array($playerRows) || !is_array($unlockRows)) { + throw new RuntimeException('legacy_state_invalid'); + } + foreach ($versions as $version) { + if (!is_string($version) || $version === '' || strlen($version) > 32) throw new RuntimeException('legacy_state_invalid'); + } + if (array_values($catalogIds) !== array_values(ACHIEVEMENT_IDS)) throw new RuntimeException('legacy_catalog_mismatch'); + + $perPlayerUnlocks = array_fill(0, count($playerRows), []); + $legacyCounts = array_fill_keys(ACHIEVEMENT_IDS, 0); + $seenAchievements = []; + $unlockAssignments = 0; + foreach ($unlockRows as $unlockRow) { + if (!is_array($unlockRow) || count($unlockRow) < 3 || count($unlockRow) % 2 !== 1) throw new RuntimeException('legacy_state_invalid'); + $achievementIndex = $unlockRow[0] ?? null; + if (!is_int($achievementIndex) || $achievementIndex < 0 || $achievementIndex >= count(ACHIEVEMENT_IDS) + || isset($seenAchievements[$achievementIndex])) throw new RuntimeException('legacy_state_invalid'); + $seenAchievements[$achievementIndex] = true; + $achievementId = ACHIEVEMENT_IDS[$achievementIndex]; + $seenPlayers = []; + for ($offset = 1; $offset < count($unlockRow); $offset += 2) { + $playerIndex = $unlockRow[$offset] ?? null; + $timeOffset = $unlockRow[$offset + 1] ?? null; + if (!is_int($playerIndex) || $playerIndex < 0 || $playerIndex >= count($playerRows) + || isset($seenPlayers[$playerIndex]) || !is_int($timeOffset) || $timeOffset < 0) { + throw new RuntimeException('legacy_state_invalid'); + } + $timestamp = $baseTimestamp + $timeOffset; + if ($timestamp < 946684800) throw new RuntimeException('legacy_state_invalid'); + $seenPlayers[$playerIndex] = true; + $perPlayerUnlocks[$playerIndex][$achievementId] = $timestamp; + $legacyCounts[$achievementId]++; + $unlockAssignments++; + } + } + + $backupPath = $root . DIRECTORY_SEPARATOR . 'legacy-backup' . DIRECTORY_SEPARATOR . 'state-v4-' . $sourceHash . '.json'; + if (!is_file($backupPath)) atomic_write_bytes($backupPath, $sourceRaw); + $backupRaw = @file_get_contents($backupPath); + if (!is_string($backupRaw) || !hash_equals($sourceHash, hash('sha256', $backupRaw))) throw new RuntimeException('legacy_backup_failed'); + + atomic_write_json($markerPath, [ + 'schema' => 1, + 'status' => 'in_progress', + 'sourceSchema' => 4, + 'sourceHash' => $sourceHash, + 'sourceSize' => $sourceSize, + 'sourceMtime' => $sourceMtime, + 'startedAt' => time(), + 'expectedPlayers' => count($playerRows), + 'expectedUnlockAssignments' => $unlockAssignments, + 'backup' => basename(dirname($backupPath)) . '/' . basename($backupPath), + ]); + + $legacyIdsSeen = []; + $migratedPlayerIds = []; + foreach ($playerRows as $index => $row) { + if (!is_array($row) || count($row) < 3 || count($row) > 4) throw new RuntimeException('legacy_state_invalid'); + $legacyId = $row[0] ?? null; + $firstOffset = $row[1] ?? null; + $duration = $row[2] ?? null; + $versionIndex = $row[3] ?? 0; + if (!is_string($legacyId) || !valid_legacy_player_id($legacyId) || isset($legacyIdsSeen[$legacyId]) + || !is_int($firstOffset) || $firstOffset < 0 || !is_int($duration) || $duration < 0 + || !is_int($versionIndex) || $versionIndex < 0 || $versionIndex >= count($versions)) { + throw new RuntimeException('legacy_state_invalid'); + } + $legacyIdsSeen[$legacyId] = true; + $playerId = legacy_player_uuid($legacyId); + if (isset($migratedPlayerIds[$playerId])) throw new RuntimeException('legacy_player_collision'); + $migratedPlayerIds[$playerId] = true; + $claimHash = hash('sha256', $legacyId); + $path = player_path($root, $playerId); + if (is_file($path)) { + $existingRaw = read_json_file($path, []); + $existingClaimHash = strtolower((string)($existingRaw['legacyClaimHash'] ?? '')); + if ($existingClaimHash !== '' && !hash_equals($existingClaimHash, $claimHash)) throw new RuntimeException('legacy_player_collision'); + if ($existingClaimHash === '' && (string)($existingRaw['ownerHash'] ?? '') !== '') throw new RuntimeException('legacy_player_collision'); + $player = normalize_player($existingRaw, $playerId); + } else { + $player = default_player($playerId, '', $baseTimestamp + $firstOffset); + } + $player['schema'] = 5; + $player['legacyClaimHash'] = $claimHash; + $player['legacyClaimedAt'] = max(0, (int)($player['legacyClaimedAt'] ?? 0)); + $player['firstSeen'] = min((int)$player['firstSeen'], $baseTimestamp + $firstOffset); + $player['lastSeen'] = max((int)$player['lastSeen'], $baseTimestamp + $firstOffset + $duration); + $player['activityCount'] = max(2, (int)$player['activityCount']); + $player['qualified'] = true; + if ((string)$player['gameVersion'] === '') $player['gameVersion'] = $versions[$versionIndex]; + // A later explicit reset must remain authoritative if migration is + // resumed after the player has already advanced to a newer generation. + if ((int)$player['generation'] === 0) { + foreach ($perPlayerUnlocks[$index] as $achievementId => $timestamp) { + if (!isset($player['unlocked'][$achievementId])) $player['unlocked'][$achievementId] = $timestamp; + else $player['unlocked'][$achievementId] = min((int)$player['unlocked'][$achievementId], $timestamp); + } + } + reconcile_completionist($player['unlocked'], $player['lastSeen']); + atomic_write_json($path, $player); + } + + $verifiedAssignments = 0; + foreach ($playerRows as $index => $row) { + $legacyId = (string)$row[0]; + $playerId = legacy_player_uuid($legacyId); + $player = normalize_player(read_json_file(player_path($root, $playerId), []), $playerId); + if (!hash_equals(hash('sha256', $legacyId), (string)$player['legacyClaimHash'])) throw new RuntimeException('legacy_migration_verify_failed'); + if ((int)$player['generation'] === 0) { + foreach ($perPlayerUnlocks[$index] as $achievementId => $timestamp) { + if (!isset($player['unlocked'][$achievementId]) || (int)$player['unlocked'][$achievementId] > $timestamp) { + throw new RuntimeException('legacy_migration_verify_failed'); + } + $verifiedAssignments++; + } + } + } + if ($verifiedAssignments !== $unlockAssignments) throw new RuntimeException('legacy_migration_verify_failed'); + + $summary = repair_summary_cache($root, $summaryPath); + if ((int)$summary['totalPlayers'] < count($playerRows)) throw new RuntimeException('legacy_migration_verify_failed'); + foreach ($legacyCounts as $achievementId => $count) { + if ((int)($summary['counts'][$achievementId] ?? 0) < $count) throw new RuntimeException('legacy_migration_verify_failed'); + } + $complete = [ + 'schema' => 1, + 'status' => 'complete', + 'sourceSchema' => 4, + 'sourceHash' => $sourceHash, + 'sourceSize' => $sourceSize, + 'sourceMtime' => $sourceMtime, + 'completedAt' => time(), + 'migratedPlayers' => count($playerRows), + 'migratedUnlockAssignments' => $unlockAssignments, + 'summaryPlayers' => (int)$summary['totalPlayers'], + 'backup' => basename(dirname($backupPath)) . '/' . basename($backupPath), + ]; + atomic_write_json($markerPath, $complete); + return $complete; +} + +function with_lock(string $path, int $mode, callable $callback): mixed { + ensure_directory(dirname($path)); + $handle = @fopen($path, 'c'); + if ($handle === false || !flock($handle, $mode)) { + if (is_resource($handle)) fclose($handle); + throw new RuntimeException('storage_locked'); + } + try { return $callback(); } finally { flock($handle, LOCK_UN); fclose($handle); } +} + +function player_path(string $root, string $playerId): string { + $bucket = substr(hash('sha256', $playerId), 0, 2); + return $root . DIRECTORY_SEPARATOR . 'players' . DIRECTORY_SEPARATOR . $bucket . DIRECTORY_SEPARATOR . hash('sha256', $playerId) . '.json'; +} + +function default_summary(): array { return [ - 'ok' => true, - 'totalPlayers' => $total, - 'achievements' => $achievements, + 'schema' => 1, + 'totalPlayers' => 0, + 'counts' => array_fill_keys(ACHIEVEMENT_IDS, 0), ]; } -$method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'); +function normalize_summary(array $summary): array { + $out = default_summary(); + $out['totalPlayers'] = max(0, (int)($summary['totalPlayers'] ?? 0)); + $counts = is_array($summary['counts'] ?? null) ? $summary['counts'] : []; + foreach (ACHIEVEMENT_IDS as $id) $out['counts'][$id] = max(0, (int)($counts[$id] ?? 0)); + return $out; +} + +function default_progress_state(): array { + return [ + 'progress' => [ + 'linkTypes' => [], + 'signalActivated' => false, + 'medicineTypes' => [], + 'dailyPlayStreak' => 0, + 'dailyPlayLastDay' => 0, + 'mysteryDrugTarinaiIds' => [], + ], + 'ledger' => [ + 'base' => array_fill_keys(array_keys(ADDITIVE_PROGRESS_CAPS), 0), + 'shards' => array_fill_keys(array_keys(ADDITIVE_PROGRESS_CAPS), []), + ], + ]; +} + +function progress_counter_value(array $progressState, string $key): int { + $cap = (int)(ADDITIVE_PROGRESS_CAPS[$key] ?? 0); + if ($cap <= 0) return 0; + $base = bounded_int($progressState['ledger']['base'][$key] ?? 0, 0, $cap); + $shards = is_array($progressState['ledger']['shards'][$key] ?? null) ? $progressState['ledger']['shards'][$key] : []; + $sum = $base; + foreach ($shards as $value) $sum += bounded_int($value, 0, $cap); + return min($cap, $sum); +} + +function bounded_int(mixed $value, int $minimum, int $maximum): int { + if (!is_int($value) && !is_float($value) && !is_string($value)) return $minimum; + if (!is_numeric($value)) return $minimum; + $numeric = (float)$value; + if (!is_finite($numeric)) return $minimum; + return max($minimum, min($maximum, (int)floor($numeric))); +} + +function unique_allowed_strings(mixed $input, array $allowed): array { + if (!is_array($input)) return []; + $lookup = array_fill_keys($allowed, true); + $out = []; + foreach ($input as $value) { + $text = (string)$value; + if (isset($lookup[$text])) $out[$text] = true; + } + return array_keys($out); +} + +function normalize_progress_state(mixed $input): array { + $out = default_progress_state(); + if (!is_array($input)) return $out; + $progressSource = is_array($input['progress'] ?? null) ? $input['progress'] : []; + $ledgerSource = is_array($input['ledger'] ?? null) ? $input['ledger'] : []; + $baseSource = is_array($ledgerSource['base'] ?? null) ? $ledgerSource['base'] : []; + $shardsSource = is_array($ledgerSource['shards'] ?? null) ? $ledgerSource['shards'] : []; + + foreach (ADDITIVE_PROGRESS_CAPS as $key => $rawCap) { + $cap = max(0, (int)$rawCap); + $base = bounded_int($baseSource[$key] ?? 0, 0, $cap); + $shards = []; + $rawShards = is_array($shardsSource[$key] ?? null) ? $shardsSource[$key] : []; + foreach ($rawShards as $actorId => $rawValue) { + $id = substr((string)$actorId, 0, 160); + if ($id === '' || preg_match('/[\x00-\x1F\x7F]/', $id) === 1) continue; + $value = bounded_int($rawValue, 0, $cap); + if ($value > 0) $shards[$id] = max((int)($shards[$id] ?? 0), $value); + } + $total = min($cap, $base + array_sum($shards)); + // Protocol v4 and older duplicated additive counters in progress. Read + // them as a one-time migration floor, but never emit them again. + $legacyFloor = bounded_int($progressSource[$key] ?? 0, 0, $cap); + if ($legacyFloor > $total) { + $base = min($cap, $base + ($legacyFloor - $total)); + $total = $legacyFloor; + } + if ($total >= $cap) { + $base = $cap; + $shards = []; + } + $out['ledger']['base'][$key] = $base; + $out['ledger']['shards'][$key] = $shards; + } + + $out['progress']['linkTypes'] = unique_allowed_strings($progressSource['linkTypes'] ?? [], LINK_PROGRESS_TYPES); + $out['progress']['signalActivated'] = (bool)($progressSource['signalActivated'] ?? false); + $out['progress']['medicineTypes'] = unique_allowed_strings($progressSource['medicineTypes'] ?? [], MEDICINE_LEDGER_TYPES); + $out['progress']['dailyPlayStreak'] = bounded_int($progressSource['dailyPlayStreak'] ?? 0, 0, 7); + $out['progress']['dailyPlayLastDay'] = bounded_int($progressSource['dailyPlayLastDay'] ?? 0, 0, 1000000); + $ids = []; + foreach ((array)($progressSource['mysteryDrugTarinaiIds'] ?? []) as $rawId) { + $id = substr(trim((string)$rawId), 0, 160); + if ($id !== '' && preg_match('/[\x00-\x1F\x7F]/', $id) !== 1) $ids[$id] = true; + if (count($ids) >= MAD_SCIENTIST_TARGET) break; + } + $out['progress']['mysteryDrugTarinaiIds'] = array_keys($ids); + return $out; +} + +function merge_progress_state(mixed $targetInput, mixed $sourceInput): array { + $target = normalize_progress_state($targetInput); + $source = normalize_progress_state($sourceInput); + foreach (ADDITIVE_PROGRESS_CAPS as $key => $cap) { + $target['ledger']['base'][$key] = max((int)$target['ledger']['base'][$key], (int)$source['ledger']['base'][$key]); + foreach ($source['ledger']['shards'][$key] as $actorId => $value) { + $target['ledger']['shards'][$key][$actorId] = max((int)($target['ledger']['shards'][$key][$actorId] ?? 0), (int)$value); + } + } + $targetDay = (int)$target['progress']['dailyPlayLastDay']; + $sourceDay = (int)$source['progress']['dailyPlayLastDay']; + if ($sourceDay > $targetDay) { + $target['progress']['dailyPlayLastDay'] = $sourceDay; + $target['progress']['dailyPlayStreak'] = (int)$source['progress']['dailyPlayStreak']; + } elseif ($sourceDay === $targetDay) { + $target['progress']['dailyPlayStreak'] = max((int)$target['progress']['dailyPlayStreak'], (int)$source['progress']['dailyPlayStreak']); + } + $target['progress']['signalActivated'] = (bool)$target['progress']['signalActivated'] || (bool)$source['progress']['signalActivated']; + $target['progress']['linkTypes'] = array_values(array_unique(array_merge($target['progress']['linkTypes'], $source['progress']['linkTypes']))); + $target['progress']['medicineTypes'] = array_values(array_unique(array_merge($target['progress']['medicineTypes'], $source['progress']['medicineTypes']))); + $target['progress']['mysteryDrugTarinaiIds'] = array_slice(array_values(array_unique(array_merge( + $target['progress']['mysteryDrugTarinaiIds'], + $source['progress']['mysteryDrugTarinaiIds'] + ))), 0, MAD_SCIENTIST_TARGET); + return normalize_progress_state($target); +} + +function progress_unlock_satisfied(string $id, array $progressState): ?bool { + $rule = PROGRESS_RULES[$id] ?? null; + if (!is_array($rule)) return null; + $type = (string)($rule['type'] ?? ''); + $key = (string)($rule['key'] ?? ''); + $target = max(0, (int)($rule['target'] ?? 0)); + $progress = $progressState['progress']; + if ($type === 'counter') return progress_counter_value($progressState, $key) >= $target; + if ($type === 'value') return bounded_int($progress[$key] ?? 0, 0, max($target, 1000000)) >= $target; + if ($type === 'set') { + $required = (string)($rule['required'] ?? ''); + $requiredValues = $required === 'medicineLedgerTypes' ? MEDICINE_LEDGER_TYPES : []; + return count(array_intersect($requiredValues, (array)($progress[$key] ?? []))) === count($requiredValues); + } + if ($type === 'unique') return count((array)($progress[$key] ?? [])) >= $target; + if ($type === 'mechanized') { + return (bool)($progress['signalActivated'] ?? false) + && count(array_intersect(LINK_PROGRESS_TYPES, (array)($progress['linkTypes'] ?? []))) === count(LINK_PROGRESS_TYPES); + } + return null; +} + +function reconcile_progress_unlocks(array &$unlocked, array $progressState, int $now): void { + foreach (ACHIEVEMENT_IDS as $id) { + if ($id === COMPLETIONIST_ID || isset($unlocked[$id])) continue; + if (progress_unlock_satisfied($id, $progressState) === true) $unlocked[$id] = $now; + } +} + +function default_player(string $playerId, string $ownerHash, int $now): array { + return [ + 'schema' => 5, + 'playerId' => $playerId, + 'ownerHash' => $ownerHash, + 'firstSeen' => $now, + 'lastSeen' => $now, + 'activityCount' => 0, + 'qualified' => false, + 'gameVersion' => '', + 'generation' => 0, + 'unlocked' => [], + 'progressState' => default_progress_state(), + ]; +} + +function normalize_player(array $player, string $playerId): array { + $now = time(); + $unlocked = []; + foreach ((array)($player['unlocked'] ?? []) as $id => $timestamp) { + if (is_string($id) && in_array($id, ACHIEVEMENT_IDS, true)) $unlocked[$id] = max(1, (int)$timestamp); + } + return [ + 'schema' => 5, + 'playerId' => $playerId, + 'ownerHash' => (string)($player['ownerHash'] ?? ''), + 'firstSeen' => max(1, (int)($player['firstSeen'] ?? $now)), + 'lastSeen' => max(1, (int)($player['lastSeen'] ?? $now)), + 'activityCount' => max(0, (int)($player['activityCount'] ?? 0)), + 'qualified' => (bool)($player['qualified'] ?? false), + 'gameVersion' => substr((string)($player['gameVersion'] ?? ''), 0, 32), + 'generation' => max(0, (int)($player['generation'] ?? 0)), + 'unlocked' => $unlocked, + 'progressState' => normalize_progress_state($player['progressState'] ?? null), + 'legacyClaimHash' => preg_match('/^[0-9a-f]{64}$/', strtolower((string)($player['legacyClaimHash'] ?? ''))) === 1 + ? strtolower((string)$player['legacyClaimHash']) : '', + 'legacyClaimedAt' => max(0, (int)($player['legacyClaimedAt'] ?? 0)), + ]; +} + +/** + * The summary is a derived cache, never the source of truth. Rebuilding it from + * player records makes a corrupt/missing summary self-healing and repairs a + * process interruption between the player and summary renames. + */ +function rebuild_summary_from_players(string $root): array { + $summary = default_summary(); + $playersRoot = $root . DIRECTORY_SEPARATOR . 'players'; + if (!is_dir($playersRoot)) return $summary; + try { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($playersRoot, FilesystemIterator::SKIP_DOTS) + ); + foreach ($iterator as $file) { + if (!$file->isFile() || strtolower($file->getExtension()) !== 'json') continue; + try { + $raw = read_json_file($file->getPathname(), []); + $playerId = strtolower((string)($raw['playerId'] ?? '')); + if (!valid_player_id($playerId)) continue; + $player = normalize_player($raw, $playerId); + } catch (Throwable) { + // One damaged player record must not take down every player. + continue; + } + if (!(bool)$player['qualified']) continue; + $summary['totalPlayers']++; + foreach (array_keys($player['unlocked']) as $id) { + if (isset($summary['counts'][$id])) $summary['counts'][$id]++; + } + } + } catch (UnexpectedValueException) { + throw new RuntimeException('storage_unavailable'); + } + return normalize_summary($summary); +} + +function repair_summary_cache(string $root, string $summaryPath): array { + $summary = rebuild_summary_from_players($root); + atomic_write_json($summaryPath, $summary); + return $summary; +} + +function verify_owner(array &$player, string $secret): void { + $submitted = hash('sha256', strtolower($secret)); + $stored = (string)($player['ownerHash'] ?? ''); + if ($stored === '' || !hash_equals($stored, $submitted)) respond(['ok' => false, 'error' => 'invalid_owner'], 403); +} + +function reconcile_completionist(array &$unlocked, int $now): void { + $latestPrerequisite = 0; + foreach (ACHIEVEMENT_IDS as $id) { + if ($id === COMPLETIONIST_ID) continue; + if (!isset($unlocked[$id])) { unset($unlocked[COMPLETIONIST_ID]); return; } + $latestPrerequisite = max($latestPrerequisite, max(1, (int)$unlocked[$id])); + } + $completionTime = max(1, $latestPrerequisite ?: $now); + if (!isset($unlocked[COMPLETIONIST_ID])) $unlocked[COMPLETIONIST_ID] = $completionTime; + else $unlocked[COMPLETIONIST_ID] = max((int)$unlocked[COMPLETIONIST_ID], $completionTime); +} + +function normalize_client_unlock_time(mixed $value, int $now): int { + if (!is_int($value) && !is_float($value) && !is_string($value)) return $now; + if (!is_numeric($value)) return $now; + $numeric = (float)$value; + if (!is_finite($numeric) || $numeric <= 0) return $now; + // Browser timestamps are milliseconds; legacy/server timestamps are seconds. + if ($numeric >= 100000000000.0) $numeric /= 1000.0; + $seconds = (int)floor($numeric); + // Reject implausible/future values, but preserve valid offline unlock times. + if ($seconds < 946684800 || $seconds > $now + 300) return $now; + return $seconds; +} + +function apply_summary_delta(array &$summary, array $before, array $after): void { + foreach (ACHIEVEMENT_IDS as $id) { + $had = isset($before[$id]); + $has = isset($after[$id]); + if ($had === $has) continue; + $summary['counts'][$id] = max(0, (int)$summary['counts'][$id] + ($has ? 1 : -1)); + } +} + +function maybe_qualify(array &$player, array &$summary, int $now): bool { + if ($player['qualified']) return false; + if ((int)$player['activityCount'] < QUALIFY_MIN_ACTIVITY) return false; + if ($now - (int)$player['firstSeen'] < QUALIFY_MIN_AGE_SECONDS) return false; + $player['qualified'] = true; + $summary['totalPlayers'] = max(0, (int)$summary['totalPlayers'] + 1); + foreach (array_keys($player['unlocked']) as $id) $summary['counts'][$id] = max(0, (int)$summary['counts'][$id] + 1); + return true; +} + +function public_summary(array $summary, ?array $player = null): array { + $total = max(0, (int)$summary['totalPlayers']); + $achievements = []; + foreach (ACHIEVEMENT_IDS as $id) { + $count = max(0, min($total, (int)($summary['counts'][$id] ?? 0))); + $achievements[$id] = [ + 'unlockedPlayers' => $count, + 'percentage' => $total > 0 ? round($count * 100 / $total, 1) : 0.0, + ]; + } + $out = ['ok' => true, 'totalPlayers' => $total, 'achievements' => $achievements, 'statisticsTrust' => 'client-reported']; + if ($player !== null) { + $out['qualified'] = (bool)($player['qualified'] ?? false); + $out['playerGeneration'] = max(0, (int)($player['generation'] ?? 0)); + // Return the authenticated player's grow-only set. The client uploads + // its offline snapshot first, then merges this atomic union response. + $out['playerUnlocked'] = (object)(is_array($player['unlocked'] ?? null) ? $player['unlocked'] : []); + $out['playerProgress'] = normalize_progress_state($player['progressState'] ?? null); + } + return $out; +} + +function enforce_rate_limit(string $root, string $scope, int $limit): void { + $now = time(); + $hash = hash('sha256', $scope); + $path = $root . DIRECTORY_SEPARATOR . 'rate' . DIRECTORY_SEPARATOR . substr($hash, 0, 2) . DIRECTORY_SEPARATOR . $hash . '.json'; + ensure_directory(dirname($path)); + $lockPath = $path . '.lock'; + with_lock($lockPath, LOCK_EX, function () use ($path, $now, $limit): void { + $state = read_json_file($path, ['window' => $now, 'count' => 0]); + $window = (int)($state['window'] ?? $now); + $count = (int)($state['count'] ?? 0); + if ($now - $window >= RATE_WINDOW_SECONDS) { $window = $now; $count = 0; } + $count++; + if ($count > $limit) respond(['ok' => false, 'error' => 'rate_limited', 'retryAfter' => max(1, RATE_WINDOW_SECONDS - ($now - $window))], 429); + atomic_write_json($path, ['window' => $window, 'count' => $count]); + }); +} + +function claim_identity_issue_slot(string $root, string $ip): void { + $now = time(); + $hash = hash('sha256', 'identity:' . $ip); + $path = $root . DIRECTORY_SEPARATOR . 'identity-rate' . DIRECTORY_SEPARATOR . substr($hash, 0, 2) . DIRECTORY_SEPARATOR . $hash . '.json'; + with_lock($path . '.lock', LOCK_EX, function () use ($path, $now): void { + $state = read_json_file($path, ['window' => $now, 'count' => 0]); + $window = max(1, (int)($state['window'] ?? $now)); + $count = max(0, (int)($state['count'] ?? 0)); + if ($now - $window >= IDENTITY_ISSUE_WINDOW_SECONDS) { $window = $now; $count = 0; } + if ($count >= IDENTITY_ISSUE_LIMIT_PER_IP) { + respond([ + 'ok' => false, + 'error' => 'identity_creation_limited', + 'retryAfter' => max(1, IDENTITY_ISSUE_WINDOW_SECONDS - ($now - $window)), + ], 429); + } + atomic_write_json($path, ['window' => $window, 'count' => $count + 1]); + }); +} + +function random_uuid_v4(): string { + $bytes = random_bytes(16); + $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40); + $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); + $hex = bin2hex($bytes); + return substr($hex, 0, 8) . '-' . substr($hex, 8, 4) . '-' . substr($hex, 12, 4) . '-' . substr($hex, 16, 4) . '-' . substr($hex, 20, 12); +} + +function session_signing_key(string $root): string { + $path = $root . DIRECTORY_SEPARATOR . 'session-key.json'; + return with_lock($path . '.lock', LOCK_EX, function () use ($path): string { + $state = read_json_file($path, []); + $hex = strtolower((string)($state['key'] ?? '')); + if (preg_match('/^[0-9a-f]{64}$/', $hex) !== 1) { + $hex = bin2hex(random_bytes(32)); + atomic_write_json($path, ['key' => $hex]); + } + $key = hex2bin($hex); + if (!is_string($key) || strlen($key) !== 32) throw new RuntimeException('storage_corrupt'); + return $key; + }); +} + +function make_session_cookie(string $root, string $playerId, string $secret): string { + $payload = strtolower($playerId) . '.' . strtolower($secret); + return $payload . '.' . hash_hmac('sha256', $payload, session_signing_key($root)); +} + +function read_session_cookie(string $root): ?array { + $token = strtolower((string)($_COOKIE[SESSION_COOKIE_NAME] ?? '')); + $parts = explode('.', $token); + if (count($parts) !== 3) return null; + [$playerId, $secret, $mac] = $parts; + if (!valid_player_id($playerId) || !valid_player_secret($secret) || preg_match('/^[0-9a-f]{64}$/', $mac) !== 1) return null; + $payload = $playerId . '.' . $secret; + $expected = hash_hmac('sha256', $payload, session_signing_key($root)); + return hash_equals($expected, $mac) ? ['playerId' => $playerId, 'playerSecret' => $secret] : null; +} + +function set_session_cookie(string $root, string $playerId, string $secret): void { + $secure = (!empty($_SERVER['HTTPS']) && strtolower((string)$_SERVER['HTTPS']) !== 'off'); + setcookie(SESSION_COOKIE_NAME, make_session_cookie($root, $playerId, $secret), [ + 'expires' => time() + SESSION_COOKIE_MAX_AGE, + 'path' => '/', + 'secure' => $secure, + 'httponly' => true, + 'samesite' => 'Strict', + ]); +} + +function stored_owner_matches(string $root, string $playerId, string $secret): bool { + $path = player_path($root, $playerId); + if (!is_file($path)) return false; + try { + $player = normalize_player(read_json_file($path, []), $playerId); + } catch (RuntimeException) { + return false; + } + $stored = (string)($player['ownerHash'] ?? ''); + return $stored !== '' && hash_equals($stored, hash('sha256', strtolower($secret))); +} + +function resolve_session_credentials(string $root, string $submittedPlayerId, string $submittedSecret, string $legacyPlayerId): array { + if (valid_legacy_player_id($legacyPlayerId) && valid_player_secret($submittedSecret)) { + $migratedPlayerId = legacy_player_uuid($legacyPlayerId); + $path = player_path($root, $migratedPlayerId); + if (is_file($path)) { + $raw = read_json_file($path, []); + $claimHash = strtolower((string)($raw['legacyClaimHash'] ?? '')); + if ($claimHash !== '' && hash_equals($claimHash, hash('sha256', $legacyPlayerId))) { + $ownerHash = strtolower((string)($raw['ownerHash'] ?? '')); + $submittedOwnerHash = hash('sha256', strtolower($submittedSecret)); + if ($ownerHash !== '' && !hash_equals($ownerHash, $submittedOwnerHash)) { + // A valid durable identity or HttpOnly cookie must remain a + // recovery path even when stale legacy-ID storage survived + // but its generated secret did not. Otherwise the stale + // legacy value permanently blocks an already-authenticated + // browser before cookie recovery is considered. + if (valid_player_id($submittedPlayerId) && valid_player_secret($submittedSecret) + && stored_owner_matches($root, strtolower($submittedPlayerId), strtolower($submittedSecret))) { + return [ + 'playerId' => strtolower($submittedPlayerId), + 'playerSecret' => strtolower($submittedSecret), + 'created' => false, + ]; + } + $cookie = read_session_cookie($root); + if ($cookie !== null && stored_owner_matches($root, $cookie['playerId'], $cookie['playerSecret'])) { + return [ + 'playerId' => $cookie['playerId'], + 'playerSecret' => $cookie['playerSecret'], + 'created' => false, + ]; + } + respond(['ok' => false, 'error' => 'legacy_player_already_claimed'], 403); + } + return [ + 'playerId' => $migratedPlayerId, + 'playerSecret' => strtolower($submittedSecret), + 'created' => false, + 'legacyClaimId' => $legacyPlayerId, + ]; + } + } + } + if (valid_player_id($submittedPlayerId) && valid_player_secret($submittedSecret) + && stored_owner_matches($root, strtolower($submittedPlayerId), strtolower($submittedSecret))) { + return ['playerId' => strtolower($submittedPlayerId), 'playerSecret' => strtolower($submittedSecret), 'created' => false]; + } + $cookie = read_session_cookie($root); + if ($cookie !== null && stored_owner_matches($root, $cookie['playerId'], $cookie['playerSecret'])) { + return ['playerId' => $cookie['playerId'], 'playerSecret' => $cookie['playerSecret'], 'created' => false]; + } + claim_identity_issue_slot($root, client_ip()); + return ['playerId' => random_uuid_v4(), 'playerSecret' => bin2hex(random_bytes(32)), 'created' => true]; +} + +$method = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')); if (!in_array($method, ['GET', 'POST'], true)) respond(['ok' => false, 'error' => 'method_not_allowed'], 405); $payload = []; @@ -365,137 +1033,195 @@ if ($method === 'POST') { if ($declaredLength > MAX_REQUEST_BYTES) respond(['ok' => false, 'error' => 'request_too_large'], 413); $raw = file_get_contents('php://input', false, null, 0, MAX_REQUEST_BYTES + 1); if (!is_string($raw) || strlen($raw) > MAX_REQUEST_BYTES) respond(['ok' => false, 'error' => 'request_too_large'], 413); - try { - $decoded = json_decode($raw, true, 64, JSON_THROW_ON_ERROR); - } catch (JsonException) { - respond(['ok' => false, 'error' => 'invalid_json'], 400); - } + try { $decoded = json_decode($raw, true, 64, JSON_THROW_ON_ERROR); } + catch (JsonException) { respond(['ok' => false, 'error' => 'invalid_json'], 400); } if (!is_array($decoded)) respond(['ok' => false, 'error' => 'invalid_json'], 400); $payload = $decoded; } -$action = $method === 'POST' - ? (string)($payload['action'] ?? '') - : (string)($_GET['action'] ?? 'summary'); -$playerId = $method === 'POST' - ? (string)($payload['playerId'] ?? '') - : (string)($_GET['playerId'] ?? ''); +$action = $method === 'POST' ? (string)($payload['action'] ?? '') : (string)($_GET['action'] ?? 'summary'); +if (!in_array($action, ['session', 'sync', 'reset', 'summary'], true)) respond(['ok' => false, 'error' => 'invalid_action'], 400); +if ($method === 'GET' && $action !== 'summary') respond(['ok' => false, 'error' => 'method_not_allowed'], 405); -if (!in_array($action, ['session', 'unlock', 'sync', 'reset', 'summary'], true)) { - respond(['ok' => false, 'error' => 'invalid_action'], 400); -} -if ($method === 'GET' && $action !== 'summary') { - respond(['ok' => false, 'error' => 'method_not_allowed'], 405); -} -if (!valid_player_id($playerId)) { - respond(['ok' => false, 'error' => 'invalid_player'], 400); -} -$playerId = strtolower($playerId); -$syncUnlocks = []; -$completionistEligible = null; -if ($action === 'unlock') { - $achievementId = (string)($payload['achievementId'] ?? ''); - if (!in_array($achievementId, ACHIEVEMENT_IDS, true)) { - respond(['ok' => false, 'error' => 'invalid_achievement'], 400); - } -} elseif ($action === 'sync') { - $submittedUnlocks = $payload['unlocked'] ?? null; - if (!is_array($submittedUnlocks) || count($submittedUnlocks) > count(ACHIEVEMENT_IDS)) { - respond(['ok' => false, 'error' => 'invalid_unlocks'], 400); - } - if (array_key_exists('completionistEligible', $payload)) { - if (!is_bool($payload['completionistEligible'])) { - respond(['ok' => false, 'error' => 'invalid_completionist_state'], 400); - } - $completionistEligible = $payload['completionistEligible']; - } - foreach ($submittedUnlocks as $achievementId => $timestamp) { - if (!is_string($achievementId) || !in_array($achievementId, ACHIEVEMENT_IDS, true)) { - respond(['ok' => false, 'error' => 'invalid_achievement'], 400); - } - if (!is_int($timestamp) && !is_float($timestamp) && !is_string($timestamp)) { - respond(['ok' => false, 'error' => 'invalid_timestamp'], 400); - } - $numericTimestamp = filter_var($timestamp, FILTER_VALIDATE_FLOAT); - if ($numericTimestamp === false || !is_finite((float)$numericTimestamp) || $numericTimestamp <= 0) { - respond(['ok' => false, 'error' => 'invalid_timestamp'], 400); - } - $syncUnlocks[$achievementId] = (float)$numericTimestamp; - } - if ($completionistEligible === false) unset($syncUnlocks['true_tarinai_observer']); -} - -$dataDir = __DIR__ . DIRECTORY_SEPARATOR . DATA_DIR_NAME; -if (!is_dir($dataDir) && !mkdir($dataDir, 0775, true) && !is_dir($dataDir)) { - respond(['ok' => false, 'error' => 'storage_unavailable'], 503); -} -$dataPath = $dataDir . DIRECTORY_SEPARATOR . DATA_FILE_NAME; -$lockPath = $dataDir . DIRECTORY_SEPARATOR . LOCK_FILE_NAME; -$lockHandle = @fopen($lockPath, 'c'); -if ($lockHandle === false || !flock($lockHandle, LOCK_EX)) { - if (is_resource($lockHandle)) fclose($lockHandle); - respond(['ok' => false, 'error' => 'storage_locked'], 503); -} - -$summary = null; -$storageError = null; try { - $contents = is_file($dataPath) ? file_get_contents($dataPath) : ''; - if ($contents === false) throw new RuntimeException('storage_unavailable'); - [$state, $sourceFormat] = decode_stored_state($contents); - $now = time(); - $mutatesPlayer = $action !== 'summary'; - - if ($mutatesPlayer) { - $player = isset($state['players'][$playerId]) && is_array($state['players'][$playerId]) - ? $state['players'][$playerId] - : ['firstSeen' => $now]; - $player['lastSeen'] = $now; - $submittedGameVersion = substr((string)($payload['gameVersion'] ?? ''), 0, 32); - if ($submittedGameVersion !== '' || !isset($player['gameVersion'])) $player['gameVersion'] = $submittedGameVersion; - $state['players'][$playerId] = $player; - } - - if ($action === 'unlock') { - $achievementId = (string)$payload['achievementId']; - if (!isset($state['unlocks'][$achievementId]) || !is_array($state['unlocks'][$achievementId])) { - $state['unlocks'][$achievementId] = []; - } - if (!isset($state['unlocks'][$achievementId][$playerId])) { - $state['unlocks'][$achievementId][$playerId] = $now; - } - } elseif ($action === 'sync') { - if ($completionistEligible === false && isset($state['unlocks']['true_tarinai_observer'][$playerId])) { - unset($state['unlocks']['true_tarinai_observer'][$playerId]); - } - foreach ($syncUnlocks as $achievementId => $submittedTimestamp) { - if (!isset($state['unlocks'][$achievementId]) || !is_array($state['unlocks'][$achievementId])) { - $state['unlocks'][$achievementId] = []; - } - if (isset($state['unlocks'][$achievementId][$playerId])) continue; - $timestampSeconds = $submittedTimestamp >= 100000000000 - ? (int)floor($submittedTimestamp / 1000) - : (int)floor($submittedTimestamp); - $state['unlocks'][$achievementId][$playerId] = max(1, min($now, $timestampSeconds)); - } - } elseif ($action === 'reset') { - foreach ($state['unlocks'] as $achievementId => $records) { - if (is_array($records)) unset($state['unlocks'][$achievementId][$playerId]); - } - } - - $encoded = encode_compact_state($state); - if (trim($contents) !== $encoded || $sourceFormat !== 'v3' || $mutatesPlayer) { - atomic_write_state($dataPath, $encoded); - } - $summary = build_summary($state); + $root = data_root(); + protect_data_root($root); + $summaryPath = $root . DIRECTORY_SEPARATOR . 'summary.json'; + $globalLock = $root . DIRECTORY_SEPARATOR . 'summary.lock'; + with_lock($globalLock, LOCK_EX, fn() => migrate_legacy_compact_state_v4($root, $summaryPath)); } catch (RuntimeException $error) { - $known = ['storage_corrupt', 'storage_unavailable', 'storage_write_failed', 'storage_encode_failed']; - $storageError = in_array($error->getMessage(), $known, true) ? $error->getMessage() : 'storage_unavailable'; -} finally { - flock($lockHandle, LOCK_UN); - fclose($lockHandle); + $known = [ + 'storage_corrupt', 'storage_unavailable', 'storage_write_failed', 'storage_encode_failed', 'storage_locked', + 'legacy_state_unreadable', 'legacy_state_invalid', 'legacy_catalog_mismatch', 'legacy_backup_failed', + 'legacy_player_collision', 'legacy_migration_verify_failed', + ]; + $message = in_array($error->getMessage(), $known, true) ? $error->getMessage() : 'storage_unavailable'; + respond(['ok' => false, 'error' => $message], 503); +} +if ($action === 'summary') { + enforce_rate_limit($root, 'ip:' . client_ip() . ':read', RATE_READ_LIMIT); +} elseif ($action === 'session') { + enforce_rate_limit($root, 'ip:' . client_ip() . ':session', RATE_SESSION_IP_LIMIT); +} else { + // Authenticated writes are primarily player-scoped. The high IP ceiling is + // only an abuse fuse and does not make a classroom share a 30-request pool. + enforce_rate_limit($root, 'ip:' . client_ip() . ':write-abuse', RATE_WRITE_IP_LIMIT); +} +if ($action === 'summary') { + try { + $result = with_lock($globalLock, LOCK_EX, fn() => public_summary(repair_summary_cache($root, $summaryPath))); + respond($result); + } catch (RuntimeException $error) { respond(['ok' => false, 'error' => $error->getMessage()], 503); } } -if ($storageError !== null) respond(['ok' => false, 'error' => $storageError], 503); -respond(is_array($summary) ? $summary : ['ok' => false, 'error' => 'storage_unavailable'], is_array($summary) ? 200 : 503); +$submittedPlayerId = strtolower((string)($payload['playerId'] ?? '')); +$submittedSecret = strtolower((string)($payload['playerSecret'] ?? '')); +$submittedLegacyPlayerId = (string)($payload['legacyPlayerId'] ?? ''); +$sessionCredentials = null; +if ($action === 'session') { + try { $sessionCredentials = resolve_session_credentials($root, $submittedPlayerId, $submittedSecret, $submittedLegacyPlayerId); } + catch (RuntimeException $error) { respond(['ok' => false, 'error' => $error->getMessage()], 503); } + $playerId = $sessionCredentials['playerId']; + $secret = $sessionCredentials['playerSecret']; +} else { + $playerId = $submittedPlayerId; + $secret = $submittedSecret; + if (!valid_player_id($playerId)) respond(['ok' => false, 'error' => 'invalid_player'], 400); + if (!valid_player_secret($secret)) respond(['ok' => false, 'error' => 'invalid_owner'], 403); + if (!is_file(player_path($root, $playerId))) respond(['ok' => false, 'error' => 'unknown_player'], 403); +} +enforce_rate_limit($root, 'player:' . $playerId, RATE_PLAYER_WRITE_LIMIT); +$clientGeneration = max(0, (int)($payload['generation'] ?? 0)); +$generationRecovery = (bool)($payload['generationRecovery'] ?? false); +$submittedUnlocks = []; +$submittedProgress = default_progress_state(); + +if ($action === 'sync') { + if ((int)($payload['syncProtocol'] ?? 0) !== SYNC_PROTOCOL_VERSION) { + respond(['ok' => false, 'error' => 'sync_protocol_upgrade_required', 'requiredProtocol' => SYNC_PROTOCOL_VERSION], 426); + } + if ((string)($payload['mergeMode'] ?? '') !== 'grow-only-union-v5') { + respond(['ok' => false, 'error' => 'invalid_merge_mode'], 400); + } + $rawUnlocks = $payload['unlocked'] ?? null; + if (!is_array($rawUnlocks) || count($rawUnlocks) > count(ACHIEVEMENT_IDS)) respond(['ok' => false, 'error' => 'invalid_unlocks'], 400); + if ((bool)($payload['replace'] ?? false)) respond(['ok' => false, 'error' => 'destructive_sync_disabled'], 409); + $rawProgress = $payload['progressState'] ?? null; + if (!is_array($rawProgress)) respond(['ok' => false, 'error' => 'invalid_progress'], 400); + $submittedProgress = normalize_progress_state($rawProgress); + $receivedAt = time(); + foreach ($rawUnlocks as $id => $timestamp) { + if (!is_string($id) || !in_array($id, ACHIEVEMENT_IDS, true)) respond(['ok' => false, 'error' => 'invalid_achievement'], 400); + if ($id === COMPLETIONIST_ID) continue; // completionist is always server-derived. + $submittedUnlocks[$id] = normalize_client_unlock_time($timestamp, $receivedAt); + } +} +if ($action === 'reset') { + if ((int)($payload['syncProtocol'] ?? 0) !== SYNC_PROTOCOL_VERSION) { + respond(['ok' => false, 'error' => 'sync_protocol_upgrade_required', 'requiredProtocol' => SYNC_PROTOCOL_VERSION], 426); + } + if ((string)($payload['confirmReset'] ?? '') !== 'RESET_ACHIEVEMENTS') respond(['ok' => false, 'error' => 'reset_confirmation_required'], 409); + if ((string)($payload['catalogRevision'] ?? '') !== CATALOG_REVISION) respond(['ok' => false, 'error' => 'catalog_mismatch'], 409); +} + +try { + $result = with_lock($globalLock, LOCK_EX, function () use ($root, $summaryPath, $playerId, $secret, $payload, $action, $submittedUnlocks, $submittedProgress, $sessionCredentials, $clientGeneration, $generationRecovery): array { + $now = time(); + $summary = default_summary(); + $path = player_path($root, $playerId); + $exists = is_file($path); + if (!$exists && $action !== 'session') respond(['ok' => false, 'error' => 'unknown_player'], 403); + $ownerHash = hash('sha256', $secret); + $player = $exists ? normalize_player(read_json_file($path, []), $playerId) : default_player($playerId, $ownerHash, $now); + $legacyClaimId = (string)($sessionCredentials['legacyClaimId'] ?? ''); + if ($action === 'session' && $legacyClaimId !== '') { + $expectedClaimHash = hash('sha256', $legacyClaimId); + if (!hash_equals((string)($player['legacyClaimHash'] ?? ''), $expectedClaimHash)) { + respond(['ok' => false, 'error' => 'legacy_claim_invalid'], 403); + } + $storedOwnerHash = (string)($player['ownerHash'] ?? ''); + if ($storedOwnerHash !== '' && !hash_equals($storedOwnerHash, $ownerHash)) { + respond(['ok' => false, 'error' => 'legacy_player_already_claimed'], 403); + } + $player['ownerHash'] = $ownerHash; + if ((int)($player['legacyClaimedAt'] ?? 0) <= 0) $player['legacyClaimedAt'] = $now; + } + verify_owner($player, $secret); + + $beforeQualified = (bool)$player['qualified']; + $beforeUnlocks = $player['unlocked']; + $serverGeneration = max(0, (int)($player['generation'] ?? 0)); + + if ($action === 'sync') { + if ($clientGeneration < $serverGeneration) { + $summary = repair_summary_cache($root, $summaryPath); + $error = public_summary($summary, $player); + $error['ok'] = false; + $error['error'] = 'stale_generation'; + respond($error, 409); + } + if ($clientGeneration > $serverGeneration) { + if (!$generationRecovery) { + $summary = repair_summary_cache($root, $summaryPath); + $error = public_summary($summary, $player); + $error['ok'] = false; + $error['error'] = 'generation_ahead'; + respond($error, 409); + } + // A locally durable newer generation is authoritative after a + // server backup restore or a newly-issued replacement identity. + $player['generation'] = $clientGeneration; + $player['unlocked'] = []; + $player['progressState'] = default_progress_state(); + $serverGeneration = $clientGeneration; + } + } + + $player['lastSeen'] = $now; + $player['activityCount'] = max(0, (int)$player['activityCount']) + 1; + $version = substr((string)($payload['gameVersion'] ?? ''), 0, 32); + if ($version !== '') $player['gameVersion'] = $version; + + if ($action === 'sync') { + $player['progressState'] = merge_progress_state($player['progressState'] ?? null, $submittedProgress); + foreach ($submittedUnlocks as $id => $submittedAt) { + if (!isset($player['unlocked'][$id])) $player['unlocked'][$id] = $submittedAt; + else $player['unlocked'][$id] = min((int)$player['unlocked'][$id], $submittedAt); + } + // Cumulative achievements are independently reconstructed from the + // merged progress CRDT, so 99 actions on one device plus 1 on + // another unlocks exactly once without trusting request ordering. + reconcile_progress_unlocks($player['unlocked'], $player['progressState'], $now); + } elseif ($action === 'reset') { + // The client generation is the target generation. Equal or older + // reset requests are already-processed retries and must be no-ops. + if ($clientGeneration > $serverGeneration) { + $player['generation'] = $clientGeneration; + $player['unlocked'] = []; + $player['progressState'] = default_progress_state(); + } + } + reconcile_completionist($player['unlocked'], $now); + + if ($beforeQualified) apply_summary_delta($summary, $beforeUnlocks, $player['unlocked']); + maybe_qualify($player, $summary, $now); + + atomic_write_json($path, $player); + $summary = repair_summary_cache($root, $summaryPath); + $public = public_summary($summary, $player); + if ($action === 'session') { + $public['playerId'] = $playerId; + $public['playerSecret'] = $secret; + $public['serverIssued'] = (bool)($sessionCredentials['created'] ?? false); + $public['legacyClaimed'] = (string)($sessionCredentials['legacyClaimId'] ?? '') !== ''; + } + return $public; + }); + if ($action === 'session') set_session_cookie($root, $playerId, $secret); + respond($result); + +} catch (RuntimeException $error) { + $known = ['storage_corrupt', 'storage_unavailable', 'storage_write_failed', 'storage_encode_failed', 'storage_locked']; + $message = in_array($error->getMessage(), $known, true) ? $error->getMessage() : 'storage_unavailable'; + respond(['ok' => false, 'error' => $message], 503); +} diff --git a/app_manifest.json b/app_manifest.json index 67dee34..30886fe 100644 --- a/app_manifest.json +++ b/app_manifest.json @@ -1,5 +1,5 @@ { - "version": "39.16.137", + "version": "39.18.3", "css": [ "css/base.css", "css/layout.css", @@ -150,6 +150,7 @@ "js/text_catalog.js", "js/ui.js", "js/game_dialogs.js", + "js/achievement_catalog.js", "js/achievements.js", "js/ui_helpers.js", "js/ui_log.js", diff --git a/assets/ui/pwa-icon-192.png b/assets/ui/pwa-icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..480d662e6293709c6971859e31c37557fc3b0f85 GIT binary patch literal 26366 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 ThZlaY4M5?es%(|CN$~#w1-PO| literal 0 HcmV?d00001 diff --git a/assets/ui/pwa-icon-512.png b/assets/ui/pwa-icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..f84c2de0a5318db7c5bbfb2eb9453649f322cb44 GIT binary patch literal 119075 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⁡#0JELy=(ul5Om-3DZwrJgFaz%2r<*YAyUm-jbc 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

#sobL7`I!Ghi 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 literal 0 HcmV?d00001 diff --git a/css/mobile.css b/css/mobile.css index fa953da..9b35acf 100644 --- a/css/mobile.css +++ b/css/mobile.css @@ -429,3 +429,23 @@ body.touch-mobile-ui .tool-tip-popover.mobile-selection-tip { @media (max-width: 980px) { .top-actions { overflow-x: auto; overflow-y: hidden; } } + +/* Mobile toolbar: keep every primary action reachable without hidden horizontal overflow. */ +@media (max-width: 520px) { + .top-actions { + display: flex; + flex-wrap: wrap; + overflow: visible; + align-items: center; + } + .top-panel-search { + flex: 1 1 100%; + width: 100%; + min-width: 0; + } + .top-actions > .btn, + .top-actions > .top-menu, + .top-actions > .speed-menu { + flex: 0 1 auto; + } +} diff --git a/index.html b/index.html index 6c3a7ec..55f92be 100644 --- a/index.html +++ b/index.html @@ -4,14 +4,16 @@ たりない観察 - - - - - - - - + + + + + + + + + +
@@ -56,14 +58,14 @@
- +
-