diff --git a/hwe/compare/fixtures/monthly_finish_nation_betting.json b/hwe/compare/fixtures/monthly_finish_nation_betting.json
new file mode 100644
index 00000000..e6fa4bdb
--- /dev/null
+++ b/hwe/compare/fixtures/monthly_finish_nation_betting.json
@@ -0,0 +1,69 @@
+{
+ "action": "FinishNationBetting",
+ "args": [1],
+ "setup": {
+ "clearBetting": true,
+ "resetNations": true,
+ "deleteOtherNations": true,
+ "keepNationIds": [1, 2],
+ "nation": [
+ { "id": 1, "values": { "name": "위", "gennum": 1, "power": 100, "level": 2 } },
+ { "id": 2, "values": { "name": "촉", "gennum": 1, "power": 300, "level": 0 } }
+ ],
+ "resetGenerals": true,
+ "deleteOtherGenerals": true,
+ "keepGeneralIds": [1, 2],
+ "general": [
+ { "id": 1, "values": { "name": "장수1", "nation": 1, "owner": 1, "npc": 0 } },
+ { "id": 2, "values": { "name": "장수2", "nation": 2, "owner": 2, "npc": 0 } }
+ ],
+ "bettingInfo": [
+ {
+ "id": 1,
+ "type": "bettingNation",
+ "name": "천통국 예상",
+ "finished": false,
+ "selectCnt": 1,
+ "isExclusive": null,
+ "reqInheritancePoint": true,
+ "openYearMonth": 2400,
+ "closeYearMonth": 2424,
+ "candidates": [
+ {
+ "title": "위",
+ "info": "국력: 100
장수 수: 1
도시 수: 1",
+ "isHtml": true,
+ "aux": { "nation": 1 }
+ },
+ {
+ "title": "촉",
+ "info": "국력: 300
장수 수: 1
도시 수: 1",
+ "isHtml": true,
+ "aux": { "nation": 2 }
+ }
+ ],
+ "winner": null
+ }
+ ],
+ "bet": [
+ { "betting_id": 1, "general_id": 0, "user_id": null, "betting_type": "[-1]", "amount": 100 },
+ { "betting_id": 1, "general_id": 1, "user_id": 1, "betting_type": "[0]", "amount": 100 },
+ { "betting_id": 1, "general_id": 2, "user_id": 2, "betting_type": "[1]", "amount": 100 }
+ ],
+ "inheritancePrevious": {
+ "1": 900,
+ "2": 900
+ },
+ "syncEnvironment": true
+ },
+ "environment": {
+ "year": 202,
+ "month": 1,
+ "startyear": 190
+ },
+ "observe": {
+ "generalIds": [1, 2],
+ "nationIds": [1, 2],
+ "ownerIds": [1, 2]
+ }
+}
diff --git a/hwe/compare/fixtures/monthly_open_nation_betting.json b/hwe/compare/fixtures/monthly_open_nation_betting.json
new file mode 100644
index 00000000..d4f8eab6
--- /dev/null
+++ b/hwe/compare/fixtures/monthly_open_nation_betting.json
@@ -0,0 +1,65 @@
+{
+ "action": "OpenNationBetting",
+ "args": [1, 500],
+ "setup": {
+ "clearBetting": true,
+ "resetNations": true,
+ "deleteOtherNations": true,
+ "keepNationIds": [1, 2],
+ "nation": [
+ {
+ "id": 1,
+ "values": {
+ "name": "위",
+ "color": "#111111",
+ "capital": 1,
+ "gennum": 1,
+ "gold": 1000,
+ "rice": 2000,
+ "tech": 100,
+ "power": 100,
+ "level": 2,
+ "type": "che_유가"
+ }
+ },
+ {
+ "id": 2,
+ "values": {
+ "name": "촉",
+ "color": "#222222",
+ "capital": 2,
+ "gennum": 1,
+ "gold": 3000,
+ "rice": 4000,
+ "tech": 200,
+ "power": 300,
+ "level": 2,
+ "type": "che_병가"
+ }
+ }
+ ],
+ "resetCities": true,
+ "city": [
+ { "id": 1, "values": { "nation": 1, "level": 3 } },
+ { "id": 2, "values": { "nation": 2, "level": 3 } }
+ ],
+ "resetGenerals": true,
+ "deleteOtherGenerals": true,
+ "keepGeneralIds": [1, 2],
+ "general": [
+ { "id": 1, "values": { "name": "장수1", "nation": 1, "city": 1, "owner": 1, "npc": 0 } },
+ { "id": 2, "values": { "name": "장수2", "nation": 2, "city": 2, "owner": 2, "npc": 0 } }
+ ],
+ "syncEnvironment": true
+ },
+ "environment": {
+ "year": 200,
+ "month": 1,
+ "startyear": 190
+ },
+ "observe": {
+ "generalIds": [1, 2],
+ "cityIds": [1, 2],
+ "nationIds": [1, 2]
+ }
+}
diff --git a/hwe/compare/monthly_event_trace.php b/hwe/compare/monthly_event_trace.php
index 11f2a016..fa0214aa 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',
- 'level', 'type', 'rate_tmp', 'aux',
+ 'power', 'level', 'type', 'rate_tmp', 'aux',
],
],
'general' => [
@@ -207,6 +207,8 @@ function comparisonMonthlyEventTraceMain(): void
'InvaderEnding',
'ChangeCity',
'ProvideNPCTroopLeader',
+ 'OpenNationBetting',
+ 'FinishNationBetting',
];
if (!in_array($actionName, $supportedActions, true)) {
throw new \InvalidArgumentException('unsupported monthly action');
@@ -283,6 +285,45 @@ function comparisonMonthlyEventTraceMain(): void
foreach (comparisonIntegerList($setup['clearInheritanceOwnerIds'] ?? [], 'clearInheritanceOwnerIds') as $ownerId) {
DB::db()->delete('storage', 'namespace = %s', "inheritance_{$ownerId}");
}
+ if (($setup['clearBetting'] ?? false) === true) {
+ DB::db()->query('DELETE FROM ng_betting');
+ DB::db()->query('DELETE FROM storage WHERE namespace = %s', 'betting');
+ $gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
+ $gameStorage->setValue('last_betting_id', 0);
+ }
+ foreach ($setup['bettingInfo'] ?? [] as $rawBettingInfo) {
+ if (!is_array($rawBettingInfo) || !is_int($rawBettingInfo['id'] ?? null)) {
+ throw new \InvalidArgumentException('invalid setup.bettingInfo row');
+ }
+ KVStorage::getStorage(DB::db(), 'betting')->setValue(
+ "id_{$rawBettingInfo['id']}",
+ $rawBettingInfo,
+ );
+ }
+ foreach ($setup['bet'] ?? [] as $row) {
+ if (
+ !is_array($row)
+ || !is_int($row['betting_id'] ?? null)
+ || !is_int($row['general_id'] ?? null)
+ || !is_string($row['betting_type'] ?? null)
+ || !is_int($row['amount'] ?? null)
+ ) {
+ throw new \InvalidArgumentException('invalid setup.bet row');
+ }
+ DB::db()->insert('ng_betting', [
+ 'betting_id' => $row['betting_id'],
+ 'general_id' => $row['general_id'],
+ 'user_id' => $row['user_id'] ?? null,
+ 'betting_type' => $row['betting_type'],
+ 'amount' => $row['amount'],
+ ]);
+ }
+ foreach ($setup['inheritancePrevious'] ?? [] as $ownerId => $value) {
+ if (!is_numeric($ownerId) || !is_int($value)) {
+ throw new \InvalidArgumentException('invalid setup.inheritancePrevious entry');
+ }
+ KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->setValue('previous', [$value, null]);
+ }
if (($setup['resetDiplomacy'] ?? false) === true) {
DB::db()->query('DELETE FROM diplomacy');
}
@@ -379,7 +420,7 @@ function comparisonMonthlyEventTraceMain(): void
throw new \InvalidArgumentException('ProcessSemiAnnual requires resource');
}
$action = new $actionClass($resource);
- } elseif (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseInvader', 'AutoDeleteInvader', 'ChangeCity'], true)) {
+ } elseif (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseInvader', 'AutoDeleteInvader', 'ChangeCity', 'OpenNationBetting', 'FinishNationBetting'], true)) {
$args = $request['args'] ?? [];
if (!is_array($args)) {
throw new \InvalidArgumentException("{$actionName} args must be an array");
@@ -393,6 +434,12 @@ function comparisonMonthlyEventTraceMain(): void
if ($actionName === 'ChangeCity' && count($args) !== 2) {
throw new \InvalidArgumentException('ChangeCity args must have exactly two entries');
}
+ if ($actionName === 'OpenNationBetting' && count($args) > 2) {
+ throw new \InvalidArgumentException('OpenNationBetting args must have at most two entries');
+ }
+ if ($actionName === 'FinishNationBetting' && count($args) !== 1) {
+ throw new \InvalidArgumentException('FinishNationBetting args must have exactly one entry');
+ }
$action = new $actionClass(...$args);
} else {
$action = new $actionClass();
@@ -574,6 +621,42 @@ function comparisonMonthlyEventTraceMain(): void
'lastNPCTroopLeaderID',
]);
}
+ if (in_array($actionName, ['OpenNationBetting', 'FinishNationBetting'], true)) {
+ $bettingStorage = KVStorage::getStorage(DB::db(), 'betting');
+ $response['betting'] = $bettingStorage->getAll();
+ $response['bets'] = iterator_to_array(
+ $db->query(
+ 'SELECT betting_id AS bettingId, general_id AS generalId, user_id AS userId, betting_type AS selectionKey, amount FROM ng_betting ORDER BY id',
+ ),
+ );
+ $response['createdEvents'] = array_map(
+ static fn(array $row): array => comparisonPickRow(
+ $row,
+ [
+ 'id' => 'id',
+ 'targetCode' => 'target',
+ 'priority' => 'priority',
+ 'condition' => 'condition',
+ 'action' => 'action',
+ ],
+ ['condition', 'action'],
+ ),
+ iterator_to_array(
+ $db->query(
+ 'SELECT id, target, priority, `condition`, action FROM event WHERE id > %i ORDER BY id',
+ $eventIdBeforeAction,
+ ),
+ ),
+ );
+ $response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
+ 'last_betting_id',
+ ]);
+ $response['inheritancePrevious'] = [];
+ foreach (comparisonIntegerList($request['observe']['ownerIds'] ?? [], 'ownerIds') as $ownerId) {
+ $value = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->getValue('previous');
+ $response['inheritancePrevious'][(string)$ownerId] = is_array($value) ? ($value[0] ?? 0) : 0;
+ }
+ }
if ($actionName === 'RaiseInvader') {
$response['createdEvents'] = array_map(
static fn(array $row): array => comparisonPickRow(