From c49f6008a68c3e48b7d70fb9f4cf564cdf8477c7 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 23 Aug 2026 12:01:17 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EB=8C=80=EA=B7=9C=EB=AA=A8=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=BC=EC=A1=B1=20=EC=86=8C=ED=99=98=20=EC=A4=91?= =?UTF-8?q?=EB=8B=A8=EC=9D=84=20=EB=B0=A9=EC=A7=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hwe/sammo/Event/Action/RaiseInvader.php | 12 ++++++++++++ hwe/sammo/Scenario/GeneralBuilder.php | 17 ++++++++++++----- tests/ScenarioGeneralBuilderSpecialTest.php | 10 ++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/hwe/sammo/Event/Action/RaiseInvader.php b/hwe/sammo/Event/Action/RaiseInvader.php index 31124777..2c545500 100644 --- a/hwe/sammo/Event/Action/RaiseInvader.php +++ b/hwe/sammo/Event/Action/RaiseInvader.php @@ -100,6 +100,17 @@ class RaiseInvader extends \sammo\Event\Action } public function run(array $env) + { + $previousIgnoreUserAbort = ignore_user_abort(true); + try { + return $this->runToCompletion($env); + } + finally { + ignore_user_abort($previousIgnoreUserAbort !== 0); + } + } + + private function runToCompletion(array $env) { $db = DB::db(); @@ -196,6 +207,7 @@ class RaiseInvader extends \sammo\Event\Action ], true); $invaderNationIDList = []; + $env = GeneralBuilder::prepareEnvironment($env); refreshNationStaticInfo(); foreach ($cities as $cityObj) { if ($cityObj->level != 4) { diff --git a/hwe/sammo/Scenario/GeneralBuilder.php b/hwe/sammo/Scenario/GeneralBuilder.php index 498152bb..89112e35 100644 --- a/hwe/sammo/Scenario/GeneralBuilder.php +++ b/hwe/sammo/Scenario/GeneralBuilder.php @@ -539,11 +539,8 @@ class GeneralBuilder{ return $this->death; } - public function build($env){ - //scenario에 life==1인 경우 수명 제한이 없어지는 모양. - - - + public static function prepareEnvironment(array $env): array + { if(!key_exists('stored_icons', $env)){ try{ $text = \file_get_contents(\sammo\ServConfig::getSharedIconPath('../hook/list.json?1')); @@ -556,6 +553,16 @@ class GeneralBuilder{ $env['stored_icons'] = $storedIcons; } + return $env; + } + + public function build($env){ + //scenario에 life==1인 경우 수명 제한이 없어지는 모양. + + + + $env = static::prepareEnvironment($env); + $isFictionMode = (Util::array_get($env['fiction'], 0)!=0); $year = $env['year']; diff --git a/tests/ScenarioGeneralBuilderSpecialTest.php b/tests/ScenarioGeneralBuilderSpecialTest.php index fe37ca8e..58f6e36f 100644 --- a/tests/ScenarioGeneralBuilderSpecialTest.php +++ b/tests/ScenarioGeneralBuilderSpecialTest.php @@ -18,6 +18,16 @@ require_once __DIR__ . '/../hwe/sammo/Scenario/GeneralBuilder.php'; final class ScenarioGeneralBuilderSpecialTest extends TestCase { + public function testPreparedStoredIconsAreReusedWithoutChangingTheEnvironment(): void + { + $environment = [ + 'stored_icons' => ['.' => ['default.jpg']], + 'icon_path' => '.', + ]; + + self::assertSame($environment, GeneralBuilder::prepareEnvironment($environment)); + } + public function testAsiaPossessionScenarioWarSpecialsUseWarSlot(): void { $scenario = json_decode( From 5a7ab4d5ce5bb031ba0d18b0ac1f8094674f54f6 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 23 Aug 2026 12:01:30 +0000 Subject: [PATCH 2/2] =?UTF-8?q?test(compare):=20=EB=8C=80=EA=B7=9C?= =?UTF-8?q?=EB=AA=A8=20=EC=9D=B4=EB=AF=BC=EC=A1=B1=20=EC=86=8C=ED=99=98=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=20=EA=B2=BD=EA=B3=84=EB=A5=BC=20=EC=9E=AC?= =?UTF-8?q?=ED=98=84=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unification_invader_large_roster.json | 242 ++++++++++++++++++ hwe/compare/monthly_event_trace.php | 60 ++++- 2 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 hwe/compare/fixtures/unification_invader_large_roster.json diff --git a/hwe/compare/fixtures/unification_invader_large_roster.json b/hwe/compare/fixtures/unification_invader_large_roster.json new file mode 100644 index 00000000..4aa6b1ca --- /dev/null +++ b/hwe/compare/fixtures/unification_invader_large_roster.json @@ -0,0 +1,242 @@ +{ + "action": "UnificationInvaderResume", + "winnerGeneralId": 1, + "setup": { + "resetCities": true, + "setAllCitiesNation": 1, + "resetGenerals": true, + "deleteOtherGenerals": true, + "keepGeneralIds": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "resetNations": true, + "deleteOtherNations": true, + "keepNationIds": [ + 1 + ], + "resetDiplomacy": true, + "resetEvents": true, + "clearInheritanceOwnerIds": [ + 1 + ], + "inheritancePrevious": { + "1": 0 + }, + "nation": [ + { + "id": 1, + "values": { + "name": "재현국", + "color": "#336699", + "capital": 1, + "gennum": 8, + "gold": 100000, + "rice": 100000, + "tech": 1000, + "power": 1000, + "level": 2, + "type": "che_유가", + "aux": {} + } + } + ], + "general": [ + { + "id": 1, + "values": { + "name": "재현군주", + "nation": 1, + "city": 1, + "officer_level": 12, + "owner": 1, + "npc": 0, + "age": 30, + "bornyear": 170, + "deadyear": 260, + "belong": 240, + "killturn": 24, + "dex1": 1000, + "dex2": 1000, + "dex3": 1000, + "dex4": 1000, + "dex5": 1000, + "aux": {} + } + }, + { + "id": 2, + "values": { + "name": "재현관직11", + "nation": 1, + "city": 1, + "officer_level": 11, + "owner": 0, + "npc": 2, + "killturn": 24, + "dex1": 1000, + "dex2": 1000, + "dex3": 1000, + "dex4": 1000, + "dex5": 1000, + "aux": {} + } + }, + { + "id": 3, + "values": { + "name": "재현관직10", + "nation": 1, + "city": 1, + "officer_level": 10, + "owner": 0, + "npc": 2, + "killturn": 24, + "dex1": 1000, + "dex2": 1000, + "dex3": 1000, + "dex4": 1000, + "dex5": 1000, + "aux": {} + } + }, + { + "id": 4, + "values": { + "name": "재현관직9", + "nation": 1, + "city": 1, + "officer_level": 9, + "owner": 0, + "npc": 2, + "killturn": 24, + "dex1": 1000, + "dex2": 1000, + "dex3": 1000, + "dex4": 1000, + "dex5": 1000, + "aux": {} + } + }, + { + "id": 5, + "values": { + "name": "재현관직8", + "nation": 1, + "city": 1, + "officer_level": 8, + "owner": 0, + "npc": 2, + "killturn": 24, + "dex1": 1000, + "dex2": 1000, + "dex3": 1000, + "dex4": 1000, + "dex5": 1000, + "aux": {} + } + }, + { + "id": 6, + "values": { + "name": "재현관직7", + "nation": 1, + "city": 1, + "officer_level": 7, + "owner": 0, + "npc": 2, + "killturn": 24, + "dex1": 1000, + "dex2": 1000, + "dex3": 1000, + "dex4": 1000, + "dex5": 1000, + "aux": {} + } + }, + { + "id": 7, + "values": { + "name": "재현관직6", + "nation": 1, + "city": 1, + "officer_level": 6, + "owner": 0, + "npc": 2, + "killturn": 24, + "dex1": 1000, + "dex2": 1000, + "dex3": 1000, + "dex4": 1000, + "dex5": 1000, + "aux": {} + } + }, + { + "id": 8, + "values": { + "name": "재현관직5", + "nation": 1, + "city": 1, + "officer_level": 5, + "owner": 0, + "npc": 2, + "killturn": 24, + "dex1": 1000, + "dex2": 1000, + "dex3": 1000, + "dex4": 1000, + "dex5": 1000, + "aux": {} + } + } + ], + "nationEnvironment": [ + { + "nationId": 1, + "key": "nationNotice", + "value": { + "msg": "재현" + } + }, + { + "nationId": 1, + "key": "scout_msg", + "value": "재현" + }, + { + "nationId": 1, + "key": "max_power", + "value": {} + } + ], + "gameEnvironment": { + "isunited": 0, + "refreshLimit": 30, + "block_change_scout": true + }, + "syncEnvironment": true + }, + "environment": { + "year": 200, + "month": 1, + "startyear": 180, + "turnterm": 10, + "turntime": "0200-01-01 00:00:00", + "show_img_level": 3, + "icon_path": ".", + "fiction": [ + 0 + ] + }, + "preSummonGeneralCopies": 350, + "invaderChoiceIndex": 0, + "stopAfterSummon": true, + "maxSummonElapsedMs": 30000 +} diff --git a/hwe/compare/monthly_event_trace.php b/hwe/compare/monthly_event_trace.php index 1266b504..9a11cf59 100644 --- a/hwe/compare/monthly_event_trace.php +++ b/hwe/compare/monthly_event_trace.php @@ -875,13 +875,40 @@ function comparisonUnificationInvaderResumeTrace(array $request, array $environm $pausedAnchor = '2000-01-01 00:00:00.000000'; $gameStorage->autoreset_united_wall_anchor = $pausedAnchor; - $selectedMessageId = $invaderMessages[array_key_last($invaderMessages)]['id']; + $preSummonGeneralCopies = $request['preSummonGeneralCopies'] ?? 0; + if (!is_int($preSummonGeneralCopies) || $preSummonGeneralCopies < 0 || $preSummonGeneralCopies > 1000) { + throw new \InvalidArgumentException('preSummonGeneralCopies must be an integer between 0 and 1000'); + } + if ($preSummonGeneralCopies > 0) { + $templateGeneral = $db->queryFirstRow('SELECT * FROM general WHERE no = %i', $winnerGeneralId); + if (!$templateGeneral) { + throw new \RuntimeException('winner general template is missing'); + } + unset($templateGeneral['no']); + $templateGeneral['owner'] = 0; + $templateGeneral['owner_name'] = null; + $templateGeneral['npc'] = 2; + $templateGeneral['npc_org'] = 2; + $templateGeneral['officer_level'] = 0; + for ($copyIndex = 1; $copyIndex <= $preSummonGeneralCopies; $copyIndex++) { + $templateGeneral['name'] = "ⓧ재현장수{$copyIndex}"; + $db->insert('general', $templateGeneral); + } + } + + $invaderChoiceIndex = $request['invaderChoiceIndex'] ?? array_key_last($invaderMessages); + if (!is_int($invaderChoiceIndex) || !array_key_exists($invaderChoiceIndex, $invaderMessages)) { + throw new \InvalidArgumentException('invaderChoiceIndex must select one of the three invader messages'); + } + $selectedMessageId = $invaderMessages[$invaderChoiceIndex]['id']; $message = Message::getMessageByID($selectedMessageId); if (!$message instanceof RaiseInvaderMessage) { throw new \RuntimeException('selected invader message did not hydrate as RaiseInvaderMessage'); } $reason = ''; + $summonStartedAt = hrtime(true); $decisionResult = $message->agreeMessage($winnerGeneralId, $reason); + $summonElapsedMs = (hrtime(true) - $summonStartedAt) / 1_000_000; $gameStorage->resetCache(); $afterSummon = [ @@ -898,19 +925,50 @@ function comparisonUnificationInvaderResumeTrace(array $request, array $environm 'eventCount' => (int)$db->queryFirstField( "SELECT COUNT(*) FROM event WHERE action LIKE '%Invader%'", ), + 'selectedChoiceIndex' => $invaderChoiceIndex, + 'preSummonGeneralCopies' => $preSummonGeneralCopies, + 'summonElapsedMs' => round($summonElapsedMs, 3), + 'generalCount' => (int)$db->queryFirstField('SELECT COUNT(*) FROM general'), + 'minimumInvaderGeneralCount' => (int)$db->queryFirstField( + "SELECT MIN(general_count) FROM (SELECT COUNT(*) AS general_count FROM general g JOIN nation n ON n.nation = g.nation WHERE n.name LIKE 'ⓞ%족' GROUP BY g.nation) invader_counts", + ), + 'maximumInvaderGeneralCount' => (int)$db->queryFirstField( + "SELECT MAX(general_count) FROM (SELECT COUNT(*) AS general_count FROM general g JOIN nation n ON n.nation = g.nation WHERE n.name LIKE 'ⓞ%족' GROUP BY g.nation) invader_counts", + ), 'autoresetAnchor' => $gameStorage->autoreset_united_wall_anchor, 'autoresetAnchorRefreshed' => $gameStorage->autoreset_united_wall_anchor !== $pausedAnchor, ]; + $maxSummonElapsedMs = $request['maxSummonElapsedMs'] ?? null; + if ($maxSummonElapsedMs !== null && (!is_int($maxSummonElapsedMs) || $maxSummonElapsedMs <= 0)) { + throw new \InvalidArgumentException('maxSummonElapsedMs must be a positive integer'); + } if ( $decisionResult !== RaiseInvaderMessage::ACCEPTED || $afterSummon['isunited'] !== 1 || $afterSummon['plock'] !== 0 || $afterSummon['invaderNationCount'] !== 7 || $afterSummon['autoresetAnchorRefreshed'] !== true + || ($maxSummonElapsedMs !== null && $summonElapsedMs >= $maxSummonElapsedMs) ) { throw new \RuntimeException('invader choice did not start an unlocked, watchdog-active event game'); } + $stopAfterSummon = $request['stopAfterSummon'] ?? false; + if (!is_bool($stopAfterSummon)) { + throw new \InvalidArgumentException('stopAfterSummon must be a boolean'); + } + if ($stopAfterSummon) { + return [ + 'schemaVersion' => 1, + 'engine' => 'ref', + 'action' => 'UnificationInvaderResume', + 'phase' => 'summon', + 'before' => $before, + 'buttonSnapshot' => $buttonSnapshot, + 'afterSummon' => $afterSummon, + ]; + } + $resumeBoundary = addTurn( cutTurn(Util::toInt($gameStorage->turntime), Util::toInt($gameStorage->turnterm)), Util::toInt($gameStorage->turnterm),