diff --git a/hwe/compare/fixtures/monthly_boundary_core_catalog.json b/hwe/compare/fixtures/monthly_boundary_core_catalog.json new file mode 100644 index 00000000..6d7bcf92 --- /dev/null +++ b/hwe/compare/fixtures/monthly_boundary_core_catalog.json @@ -0,0 +1,226 @@ +{ + "action": "MonthlyBoundary", + "compactOutput": true, + "environment": { + "year": 190, + "month": 12, + "startyear": 190, + "turnterm": 10, + "starttime": "2026-07-25 00:00:00", + "turntime": "2026-07-25 02:00:00" + }, + "setup": { + "syncEnvironment": true, + "world": { + "hiddenSeed": "monthly-boundary-core-catalog" + }, + "gameEnvironment": { + "isunited": 1, + "refreshLimit": 3, + "block_change_scout": false + }, + "resetCities": true, + "deleteOtherCities": true, + "keepCityIds": [1, 2], + "city": [ + { + "id": 1, + "values": { + "name": "경계도시", + "level": 1, + "nation": 1, + "pop": 10000, + "pop_max": 20000, + "agri": 1000, + "agri_max": 2000, + "comm": 1000, + "comm_max": 2000, + "secu": 1000, + "secu_max": 2000, + "trust": 50, + "trade": 100, + "def": 1000, + "def_max": 2000, + "wall": 1000, + "wall_max": 2000, + "officer_set": 1, + "state": 31, + "term": 1, + "supply": 1, + "front": 2, + "conflict": {}, + "dead": 0 + } + }, + { + "id": 2, + "values": { + "name": "중립도시", + "level": 8, + "nation": 0, + "pop": 12000, + "pop_max": 22000, + "agri": 1200, + "agri_max": 2200, + "comm": 1200, + "comm_max": 2200, + "secu": 1200, + "secu_max": 2200, + "trust": 55, + "trade": 100, + "def": 1200, + "def_max": 2200, + "wall": 1200, + "wall_max": 2200, + "officer_set": 1, + "state": 0, + "term": 0, + "supply": 0, + "front": 0, + "conflict": {}, + "dead": 0 + } + } + ], + "resetGenerals": true, + "deleteOtherGenerals": true, + "keepGeneralIds": [1, 2], + "general": [ + { + "id": 1, + "values": { + "name": "경계군주", + "nation": 1, + "city": 1, + "officer_level": 12, + "npc": 0, + "age": 30, + "belong": 3, + "makelimit": 2, + "gold": 1000, + "rice": 1000, + "crew": 100, + "train": 80, + "atmos": 70, + "leadership": 50, + "strength": 50, + "intel": 50, + "experience": 0, + "dedication": 0, + "dex1": 0, + "dex2": 0, + "dex3": 0, + "dex4": 0, + "dex5": 0, + "personal": "None", + "special": "None", + "specage": 0, + "special2": "None", + "specage2": 0, + "betray": 0, + "bornyear": 160, + "deadyear": 250 + } + }, + { + "id": 2, + "values": { + "name": "경계재야", + "nation": 0, + "city": 2, + "officer_level": 0, + "npc": 0, + "age": 25, + "belong": 4, + "makelimit": 0, + "gold": 500, + "rice": 500, + "crew": 50, + "train": 60, + "atmos": 50, + "leadership": 40, + "strength": 40, + "intel": 40, + "experience": 0, + "dedication": 0, + "dex1": 0, + "dex2": 0, + "dex3": 0, + "dex4": 0, + "dex5": 0, + "personal": "None", + "special": "None", + "specage": 0, + "special2": "None", + "specage2": 0, + "betray": 0, + "bornyear": 165, + "deadyear": 250 + } + } + ], + "resetNations": true, + "deleteOtherNations": true, + "keepNationIds": [1], + "nation": [ + { + "id": 1, + "values": { + "name": "경계국", + "color": "#123456", + "capital": 1, + "gennum": 1, + "gold": 10000, + "rice": 20000, + "tech": 100, + "power": 10, + "level": 2, + "type": "che_유가", + "bill": 100, + "rate": 20, + "rate_tmp": 10, + "scout": 1, + "chief_set": 1, + "strategic_cmd_limit": 2, + "surlimit": 1, + "spy": "{}" + } + } + ], + "resetDiplomacy": true, + "resetEvents": true, + "event": [ + { + "id": 991001, + "target": "PRE_MONTH", + "priority": 1000, + "condition": true, + "action": [ + ["NoticeToHistoryLog", "aggregate-pre", 6] + ] + }, + { + "id": 991002, + "target": "MONTH", + "priority": 1000, + "condition": true, + "action": [ + ["ProcessIncome", "gold"], + ["NewYear"], + ["ResetOfficerLock"], + ["RandomizeCityTradeRate"], + ["ChangeCity", "all", {"pop": "+10", "trust": 60}], + ["NoticeToHistoryLog", "aggregate-month", 6], + ["DeleteEvent"] + ] + } + ] + }, + "observe": { + "generalIds": [1, 2], + "nationIds": [1], + "cityIds": [1, 2], + "eventIds": [991001, 991002], + "accessGeneralIds": [1, 2] + } +} diff --git a/hwe/compare/monthly_event_trace.php b/hwe/compare/monthly_event_trace.php index 8f5761b8..0d580c6c 100644 --- a/hwe/compare/monthly_event_trace.php +++ b/hwe/compare/monthly_event_trace.php @@ -33,7 +33,7 @@ function comparisonMonthlyPatch(string $table, int $id, array $values): void 'idColumn' => 'nation', 'allowed' => [ 'name', 'color', 'capital', 'gennum', 'gold', 'rice', 'tech', - 'power', 'level', 'type', 'rate_tmp', 'scout', 'chief_set', 'aux', + 'power', 'level', 'type', 'bill', 'rate_tmp', 'scout', 'chief_set', 'aux', 'rate', 'strategic_cmd_limit', 'surlimit', 'spy', ], ], @@ -299,6 +299,7 @@ function comparisonMonthlyEventTraceMain(): void 'ResetOfficerLock', 'PreUpdateMonthly', 'PostUpdateMonthly', + 'MonthlyBoundary', ]; if (!in_array($actionName, $supportedActions, true)) { throw new \InvalidArgumentException('unsupported monthly action'); @@ -325,6 +326,13 @@ function comparisonMonthlyEventTraceMain(): void comparisonMonthlyPatch('city', $row['id'], $row['values']); } } + if (($setup['deleteOtherCities'] ?? false) === true) { + $keepCityIds = comparisonIntegerList($setup['keepCityIds'] ?? [], 'keepCityIds'); + if ($keepCityIds === []) { + throw new \InvalidArgumentException('deleteOtherCities requires keepCityIds'); + } + DB::db()->query('DELETE FROM city WHERE city NOT IN %li', $keepCityIds); + } if (($setup['resetGenerals'] ?? false) === true) { DB::db()->query( 'UPDATE general SET nation = 0, officer_level = 1, npc = 2, killturn = 0, horse = %s, weapon = %s, book = %s, item = %s', @@ -509,6 +517,9 @@ function comparisonMonthlyEventTraceMain(): void ]; DB::db()->insertUpdate('diplomacy', $diplomacyPatch, $diplomacyPatch); } + if (($setup['resetEvents'] ?? false) === true) { + DB::db()->query('DELETE FROM event'); + } foreach ($setup['event'] ?? [] as $row) { if (!is_array($row) || !is_int($row['id'] ?? null) || !is_array($row['action'] ?? null)) { throw new \InvalidArgumentException('invalid setup.event row'); @@ -587,19 +598,30 @@ function comparisonMonthlyEventTraceMain(): void } if (($setup['syncEnvironment'] ?? false) === true) { $gameStorage = KVStorage::getStorage(DB::db(), 'game_env'); - foreach ([ + $environmentValues = [ 'year' => $year, 'month' => $month, 'startyear' => $startYear, 'killturn' => $environment['killturn'] ?? $gameStorage->killturn, 'turnterm' => $environment['turnterm'] ?? $gameStorage->turnterm, - ] as $key => $value) { + ]; + if (isset($environment['starttime'])) { + if (!is_string($environment['starttime']) || $environment['starttime'] === '') { + throw new \InvalidArgumentException('environment.starttime must be a non-empty string'); + } + $environmentValues['starttime'] = $environment['starttime']; + } + foreach ($environmentValues as $key => $value) { $gameStorage->setValue($key, $value); } + if ($actionName === 'MonthlyBoundary') { + $before = comparisonTurnStateSnapshot($snapshotRequest); + $beforeDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId); + } } $actionClass = "\\sammo\\Event\\Action\\{$actionName}"; - if (in_array($actionName, ['PreUpdateMonthly', 'PostUpdateMonthly'], true)) { + if (in_array($actionName, ['PreUpdateMonthly', 'PostUpdateMonthly', 'MonthlyBoundary'], true)) { $action = null; } elseif ($actionName === 'ProcessSemiAnnual') { $resource = $request['resource'] ?? null; @@ -707,6 +729,30 @@ function comparisonMonthlyEventTraceMain(): void $seedYear, $seedMonth, )))); + } elseif ($actionName === 'MonthlyBoundary') { + $turntime = $environment['turntime'] ?? null; + if (!is_string($turntime) || $turntime === '') { + throw new \InvalidArgumentException('MonthlyBoundary requires turntime'); + } + $gameStorage = KVStorage::getStorage($db, 'game_env'); + $seedYear = (int)$gameStorage->year; + $seedMonth = (int)$gameStorage->month; + TurnExecutionHelper::runEventHandler($db, $gameStorage, Enums\EventTarget::PreMonth); + if (!preUpdateMonthly()) { + throw new \RuntimeException('preUpdateMonthly failed'); + } + turnDate($turntime); + $gameStorage->resetCache(); + if ((int)$gameStorage->month === 1) { + checkStatistic(); + } + TurnExecutionHelper::runEventHandler($db, $gameStorage, Enums\EventTarget::Month); + postUpdateMonthly(new RandUtil(new LiteHashDRBG(Util::simpleSerialize( + UniqueConst::$hiddenSeed, + 'monthly', + $seedYear, + $seedMonth, + )))); } else { $action->run($actionEnvironment); }