Compare commits

..
2 Commits
Author SHA1 Message Date
sars f71aa79b0d Merge branch 'devel' into asia_scenario 2022-08-02 16:20:18 +09:00
sars 15cf23dd4d 아시아 명장전 시나리오(비급) 추가 2022-08-02 00:17:37 -07:00
825 changed files with 17093 additions and 174939 deletions
+15
View File
@@ -0,0 +1,15 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3,
"modules": false
}
]
],
"plugins": [
"lodash"
]
}
-7
View File
@@ -10,10 +10,8 @@
*.log *.log
logs/*.txt logs/*.txt
api_log.db
d_log/*.txt d_log/*.txt
d_log/*.zip d_log/*.zip
d_log/*.db
sess_* sess_*
*/logs/*.txt */logs/*.txt
*/logs/*/*.txt */logs/*/*.txt
@@ -29,7 +27,6 @@ d_pic/*.jpg
d_pic/*.gif d_pic/*.gif
d_pic/*.png d_pic/*.png
d_pic/*.webp d_pic/*.webp
d_pic/*.avif
d_pic/uploaded_image d_pic/uploaded_image
/dist_js /dist_js
@@ -68,7 +65,3 @@ ingame/js/*
ingame/css/* ingame/css/*
gateway/js/* gateway/js/*
gateway/css/* gateway/css/*
*.db-journal
d_log/cachegrind.*
+12 -2
View File
@@ -15,8 +15,9 @@ return [
// Note that the **only** effect of choosing `'5.6'` is to infer // Note that the **only** effect of choosing `'5.6'` is to infer
// that functions removed in php 7.0 exist. // that functions removed in php 7.0 exist.
// (See `backward_compatibility_checks` for additional options) // (See `backward_compatibility_checks` for additional options)
"target_php_version" => '8.2', "target_php_version" => '8.1',
"minimum_target_php_version" => '8.2', "minimum_target_php_version" => '8.1',
'backward_compatibility_checks ' => true,
'minimum_severity' => \Phan\Issue::SEVERITY_NORMAL, 'minimum_severity' => \Phan\Issue::SEVERITY_NORMAL,
'file_list' => [ 'file_list' => [
@@ -28,6 +29,8 @@ return [
'hwe/_admin1_submit.php', 'hwe/_admin1_submit.php',
'hwe/_admin2.php', 'hwe/_admin2.php',
'hwe/_admin2_submit.php', 'hwe/_admin2_submit.php',
'hwe/_admin4.php',
'hwe/_admin4_submit.php',
'hwe/_admin5.php', 'hwe/_admin5.php',
'hwe/_admin5_submit.php', 'hwe/_admin5_submit.php',
'hwe/_admin7.php', 'hwe/_admin7.php',
@@ -95,6 +98,12 @@ return [
'hwe/j_load_scenarios.php', 'hwe/j_load_scenarios.php',
'hwe/j_map.php', 'hwe/j_map.php',
'hwe/j_map_recent.php', 'hwe/j_map_recent.php',
'hwe/j_msg_contact_list.php',
'hwe/j_msg_decide_opt.php',
'hwe/j_msg_delete.php',
'hwe/j_msg_get_old.php',
'hwe/j_msg_get_recent.php',
'hwe/j_msg_submit.php',
'hwe/j_myBossInfo.php', 'hwe/j_myBossInfo.php',
'hwe/j_raise_event.php', 'hwe/j_raise_event.php',
'hwe/j_select_npc.php', 'hwe/j_select_npc.php',
@@ -103,6 +112,7 @@ return [
'hwe/j_set_my_setting.php', 'hwe/j_set_my_setting.php',
'hwe/j_set_npc_control.php', 'hwe/j_set_npc_control.php',
'hwe/j_simulate_battle.php', 'hwe/j_simulate_battle.php',
'hwe/j_troop.php',
'hwe/j_update_picked_general.php', 'hwe/j_update_picked_general.php',
'hwe/j_vacation.php', 'hwe/j_vacation.php',
'hwe/lib.php', 'hwe/lib.php',
+1 -1
View File
@@ -29,5 +29,5 @@
"josa", "josa",
"sammo" "sammo"
], ],
"editor.tabSize": 4 "editor.tabSize": 2
} }
-1
View File
@@ -2,7 +2,6 @@
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="color-scheme" content="dark">
<title>닫혀 있습니다.</title> <title>닫혀 있습니다.</title>
<script> <script>
setTimeout(function(){ setTimeout(function(){
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License The MIT License
Copyright (c) 2023 Hide_D, 62che Copyright (c) 2022 Hide_D, 62che
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+2 -27
View File
@@ -42,32 +42,7 @@ sudo -u www-data git clone https://storage.hided.net/gitea/devsam/core.git
sudo -u www-data git clone https://storage.hided.net/gitea/devsam/image.git sudo -u www-data git clone https://storage.hided.net/gitea/devsam/image.git
``` ```
> 이미지는 Gitea webhook을 기본으로 동기화합니다. Webhook 전달이 누락된 경우에도 서버 설치 과정에 이미지 갱신 API와 `core` 전용 비밀값을 지정하면 게임 서버 업데이트 명령이 서명된 동기화를 요청합니다. 이미지 서버의 브랜치 변경 권한은 부여되지 않습니다. > 이미지는 hook/git_hook.php을 통해 동기화되며, 서버 설치 과정에 이미지 갱신 키를 지정하는 것으로 '훼' 서버 업데이트 시 동기화됩니다. 이미지 서버가 게임 서버와 별개여도 동작하나, php와 git을 지원해야합니다.
CLI에서 수동으로 복구 동기화를 요청할 수도 있습니다.
```sh
IMAGE_SYNC_SECRET_FILE=/run/secrets/image_sync_core_secret \
php scripts/sync-image-repository.php
```
Ref의 사용자 아이콘 원격 업로드 구현은 기본적으로 꺼져 있습니다. 이미지 서버의
`image_upload_core_secret`과 동일한 값을 Git 제외 파일
`d_setting/image_upload_core_secret`에 저장한 뒤 실제
`d_setting/ServConfig.php`에서 다음 값만 변경하면 기존 화면을 그대로 둔 채
원격 bind 저장소로 전환됩니다.
```php
public static $remoteUserIconUploadEnabled = true;
public static $remoteUserIconUploadPath = 'https://sam-image.hided.net';
public static $remoteUserIconUploadSecretFile = 'd_setting/image_upload_core_secret';
```
플래그가 `false`이면 기존 `d_pic``IMGSVR=1` 동작을 유지합니다. `true`이면
PHP 서버가 인증 사용자와 이미지 규격을 먼저 검사한 후 60초 HMAC 권한으로
이미지 서버에 직접 업로드하고 `IMGSVR=0` 공유 이미지 경로를 저장합니다.
국방·외교 등 TipTap 편집기 첨부 이미지도 같은 플래그로 `/uploads/core/` bind
저장소로 전환됩니다. 공유 비밀값은 브라우저나 Cloudflare로 보내지 않습니다.
### 설치 ### 설치
@@ -92,4 +67,4 @@ Database 수는 로그인 관리 서버 1개, 내부 서버 7개로, 총 8개의
* MIT License * MIT License
* GPL 2.0 또는 이후 * GPL 2.0 또는 이후
만약 별도의 라이선스를 적용하고자 할 경우 Hide_D에게 문의하여 주십시오. 만약 별도의 라이선스를 적용하고자 할 경우 Hide_D에게 문의하여 주십시오.
+14 -13
View File
@@ -8,25 +8,26 @@
], ],
"config": { "config": {
"platform": { "platform": {
"php": "8.3" "php": "8.1"
} }
}, },
"require": { "require": {
"sergeytsalkov/meekrodb": "^2.5", "brandonwamboldt/utilphp": "^1.1",
"league/plates": "^3.5", "sergeytsalkov/meekrodb": "^2.3",
"league/plates": "^3.3",
"vlucas/valitron": "^1.4", "vlucas/valitron": "^1.4",
"php-extended/php-tail": "^5.0", "php-extended/php-tail": "^2.1",
"symfony/lock": "^6.2", "pguardiario/phpuri": "^1.0",
"symfony/lock": "^6.0",
"ezyang/htmlpurifier": "^4.10", "ezyang/htmlpurifier": "^4.10",
"xemlock/htmlpurifier-html5": "^0.1.7", "xemlock/htmlpurifier-html5": "^0.1.7",
"catfan/medoo": "^2.1", "catfan/medoo": "^2.1",
"phan/phan": "^5.4", "phan/phan": "^5.3",
"nette/caching": "^3.3", "nette/caching": "^3.0",
"illuminate/database": "^10.0", "illuminate/database": "^9.12",
"illuminate/events": "^10.0", "illuminate/events": "^9.12",
"php-ds/php-ds": "^1.5", "spatie/data-transfer-object": "^3.7",
"hided62/ldto": "^1.0", "php-ds/php-ds": "^1.4"
"league/uri": "^6.8"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@@ -47,6 +48,6 @@
] ]
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^10.0" "phpunit/phpunit": "^9.5"
} }
} }
Generated
+1086 -1420
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+1 -4
View File
@@ -99,10 +99,7 @@ function logError(string $err, string $errstr, string $errpath, array $trace)
$err = str_replace(ROOT, '{ROOT}', $err); $err = str_replace(ROOT, '{ROOT}', $err);
$errstr = str_replace(ROOT, '{ROOT}', $errstr); $errstr = str_replace(ROOT, '{ROOT}', $errstr);
$errpath = str_replace(ROOT, '{ROOT}', $errpath); $errpath = str_replace(ROOT, '{ROOT}', $errpath);
$trace = array_map(function (string|array $text) { $trace = array_map(function (string $text) {
if (is_array($text)) {
$text = Json::encode($text);
}
return str_replace(ROOT, '{ROOT}', $text); return str_replace(ROOT, '{ROOT}', $text);
}, $trace); }, $trace);
+7 -15
View File
@@ -10,7 +10,6 @@ require(__DIR__ . '/../vendor/autoload.php');
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>설치</title> <title>설치</title>
@@ -30,10 +29,10 @@ require(__DIR__ . '/../vendor/autoload.php');
<body> <body>
<div class="container"> <div class="container">
<h1 class="row justify-content-lg-center">삼국지 모의전투 HiDCHe 설치</h1> <h1 class="row justify-content-md-center">삼국지 모의전투 HiDCHe 설치</h1>
<div class="row justify-content-lg-center"> <div class="row justify-content-md-center">
<div class="col col-12 col-lg-10 col-lg-7"> <div class="col col-12 col-md-10 col-lg-7">
<div class="card" id="db_form_card" style="display:none"> <div class="card" id="db_form_card" style="display:none">
<h3 class="card-header"> <h3 class="card-header">
설치(DB 설정) 설치(DB 설정)
@@ -86,28 +85,21 @@ require(__DIR__ . '/../vendor/autoload.php');
<div class="form-group row"> <div class="form-group row">
<label for="shared_icon_path" class="col-sm-4 col-form-label">공용 아이콘 주소</label> <label for="shared_icon_path" class="col-sm-4 col-form-label">공용 아이콘 주소</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" class="form-control" name="shared_icon_path" id="shared_icon_path" placeholder="공용 아이콘 주소(웹 주소, 또는 접속 경로에 따른 상대 주소)" value="https://sam-image.hided.net/icons" /> <input type="text" class="form-control" name="shared_icon_path" id="shared_icon_path" placeholder="공용 아이콘 주소(웹 주소, 또는 접속 경로에 따른 상대 주소)" value="../image/icons" />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="game_image_path" class="col-sm-4 col-form-label">게임 이미지 주소</label> <label for="game_image_path" class="col-sm-4 col-form-label">게임 이미지 주소</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input type="text" class="form-control" name="game_image_path" id="game_image_path" placeholder="게임 이미지 주소(웹 주소, 또는 접속 경로에 따른 상대 주소)" value="https://sam-image.hided.net/game" /> <input type="text" class="form-control" name="game_image_path" id="game_image_path" placeholder="게임 이미지 주소(웹 주소, 또는 접속 경로에 따른 상대 주소)" value="../image/game" />
</div>
</div>
<div class="form-group row">
<label for="image_request_path" class="col-sm-4 col-form-label">이미지 갱신 API</label>
<div class="col-sm-8">
<input type="url" class="form-control" name="image_request_path" id="image_request_path" value="https://sam-image.hided.net/v1/sync" required />
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label for="image_request_key" class="col-sm-4 col-form-label">이미지 갱신 키</label> <label for="image_request_key" class="col-sm-4 col-form-label">이미지 갱신 키</label>
<div class="input-group col-sm-8"> <div class="input-group col-sm-8">
<input type="text" class="form-control" name="image_request_key" id="image_request_key" placeholder="이미지 서버의 core 동기화 비밀값과 동일하게" value="" /> <input type="text" class="form-control" name="image_request_key" id="image_request_key" placeholder="이미지 서버의 hook/HashKey.php의 값과 동일하게" value="" />
<div class="input-group-text"> <div class="input-group-text">
<button id="btn_random_generate_key" class="btn btn-secondary" type="button">랜덤 생성</button> <button id="btn_random_generate_key" class="btn btn-secondary" type="button">랜덤 생성</button>
</div> </div>
@@ -196,4 +188,4 @@ require(__DIR__ . '/../vendor/autoload.php');
</div> </div>
</body> </body>
</html> </html>
+1 -1
View File
@@ -56,7 +56,7 @@ $rootDB->insert('member', [
'token_valid_until' => '2999-01-01 00:00:00', 'token_valid_until' => '2999-01-01 00:00:00',
'pw' => $finalPassword, 'pw' => $finalPassword,
'salt' => $userSalt, 'salt' => $userSalt,
'grade' => 7, 'grade' => 6,
'name' => $nickname, 'name' => $nickname,
'reg_date' => $nowDate 'reg_date' => $nowDate
]); ]);
+9 -2
View File
@@ -17,11 +17,18 @@ if (!class_exists('\\sammo\\RootDB')) {
$rootDB = RootDB::db(); $rootDB = RootDB::db();
$rootDB->addHook('run_failed', function ($params) { $rootDB->throw_exception_on_nonsql_error = false;
$rootDB->nonsql_error_handler = function ($params) {
Json::die([
'step' => 'conn_fail'
]);
};
$rootDB->error_handler = function ($params) {
Json::die([ Json::die([
'step' => 'sql_fail' 'step' => 'sql_fail'
]); ]);
}); };
$memberCnt = $rootDB->queryFirstField('SELECT count(`NO`) from member'); $memberCnt = $rootDB->queryFirstField('SELECT count(`NO`) from member');
if ($memberCnt == 0) { if ($memberCnt == 0) {
+22 -28
View File
@@ -12,14 +12,13 @@ $dbName = Util::getPost('db_name');
$servHost = Util::getPost('serv_host'); $servHost = Util::getPost('serv_host');
$sharedIconPath = Util::getPost('shared_icon_path'); $sharedIconPath = Util::getPost('shared_icon_path');
$gameImagePath = Util::getPost('game_image_path'); $gameImagePath = Util::getPost('game_image_path');
$imageRequestPath = Util::getPost('image_request_path');
$imageRequestKey = Util::getPost('image_request_key'); $imageRequestKey = Util::getPost('image_request_key');
$kakaoRESTKey = Util::getPost('kakao_rest_key', 'string', ''); $kakaoRESTKey = Util::getPost('kakao_rest_key', 'string', '');
$kakaoAdminKey = Util::getPost('kakao_admin_key', 'string', ''); $kakaoAdminKey = Util::getPost('kakao_admin_key', 'string', '');
if (!$host || !$port || !$username || !$password || !$dbName || !$servHost || !$sharedIconPath || !$gameImagePath || !$imageRequestPath) { if (!$host || !$port || !$username || !$password || !$dbName || !$servHost || !$sharedIconPath || !$gameImagePath) {
Json::die([ Json::die([
'result' => false, 'result' => false,
'reason' => '입력 값이 올바르지 않습니다' 'reason' => '입력 값이 올바르지 않습니다'
@@ -33,21 +32,6 @@ if (!filter_var($servHost, FILTER_VALIDATE_URL)) {
]); ]);
} }
if (!filter_var($imageRequestPath, FILTER_VALIDATE_URL)
|| parse_url($imageRequestPath, PHP_URL_SCHEME) !== 'https') {
Json::die([
'result' => false,
'reason' => '이미지 갱신 API는 HTTPS URL이어야 합니다.'
]);
}
if ($imageRequestKey !== null && $imageRequestKey !== '' && strlen($imageRequestKey) < 32) {
Json::die([
'result' => false,
'reason' => '이미지 동기화 비밀값은 32자 이상이어야 합니다.'
]);
}
if (file_exists(ROOT . '/d_setting/RootDB.php') && is_dir(ROOT . '/d_setting/RootDB.php')) { if (file_exists(ROOT . '/d_setting/RootDB.php') && is_dir(ROOT . '/d_setting/RootDB.php')) {
Json::die([ Json::die([
'result' => false, 'result' => false,
@@ -106,19 +90,19 @@ if (
//기본 파일 생성 //기본 파일 생성
if (!file_exists(AppConf::getUserIconPathFS())) { if (!file_exists(AppConf::getUserIconPathFS())) {
mkdir(AppConf::getUserIconPathFS(), 0775); mkdir(AppConf::getUserIconPathFS());
} }
if (!file_exists(ROOT . '/d_log')) { if (!file_exists(ROOT . '/d_log')) {
mkdir(ROOT . '/d_log', 0775); mkdir(ROOT . '/d_log');
} }
if (!file_exists(ROOT . '/d_setting')) { if (!file_exists(ROOT . '/d_setting')) {
mkdir(ROOT . '/d_setting', 0775); mkdir(ROOT . '/d_setting');
} }
if (!file_exists(ROOT . '/d_shared')) { if (!file_exists(ROOT . '/d_shared')) {
mkdir(ROOT . '/d_shared', 0775); mkdir(ROOT . '/d_shared');
} }
} }
@@ -166,12 +150,20 @@ if (!file_exists(ROOT . '/d_setting/.htaccess')) {
$rootDB = new \MeekroDB($host, $username, $password, $dbName, $port, 'utf8mb4'); $rootDB = new \MeekroDB($host, $username, $password, $dbName, $port, 'utf8mb4');
$rootDB->connect_options[MYSQLI_OPT_INT_AND_FLOAT_NATIVE] = true; $rootDB->connect_options[MYSQLI_OPT_INT_AND_FLOAT_NATIVE] = true;
$rootDB->addHook('run_failed', function ($args) { $rootDB->throw_exception_on_nonsql_error = false;
$rootDB->nonsql_error_handler = function ($params) {
Json::die([ Json::die([
'result' => false, 'result' => false,
'reason' => '에러.'. $args['error'] 'reason' => 'DB 접속에 실패했습니다.'
]); ]);
}); };
$rootDB->error_handler = function ($params) {
Json::die([
'result' => false,
'reason' => 'SQL을 제대로 실행하지 못했습니다. DB상태를 확인해 주세요.'
]);
};
$mysqli_obj = $rootDB->get(); //로그인에 실패할 경우 자동으로 dbConnFail()이 실행됨. $mysqli_obj = $rootDB->get(); //로그인에 실패할 경우 자동으로 dbConnFail()이 실행됨.
@@ -201,7 +193,8 @@ $globalSalt = bin2hex(random_bytes(16));
$sharedIconPath = WebUtil::resolveRelativePath($sharedIconPath, $servHost); $sharedIconPath = WebUtil::resolveRelativePath($sharedIconPath, $servHost);
$gameImagePath = WebUtil::resolveRelativePath($gameImagePath, $servHost); $gameImagePath = WebUtil::resolveRelativePath($gameImagePath, $servHost);
$imageRequestPath = WebUtil::resolveRelativePath($imageRequestPath, $servHost); $imageRequestPath = WebUtil::resolveRelativePath($gameImagePath . '/../hook/git_pull.php', $servHost);
$imageKeyInstallPath = WebUtil::resolveRelativePath($gameImagePath . '/../hook/InstallKey.php', $servHost);
$result = Util::generateFileUsingSimpleTemplate( $result = Util::generateFileUsingSimpleTemplate(
__DIR__ . '/templates/ServConfig.orig.php', __DIR__ . '/templates/ServConfig.orig.php',
@@ -212,9 +205,6 @@ $result = Util::generateFileUsingSimpleTemplate(
'gameImagePath' => $gameImagePath, 'gameImagePath' => $gameImagePath,
'imageRequestPath' => $imageRequestPath, 'imageRequestPath' => $imageRequestPath,
'imageRequestKey' => $imageRequestKey, 'imageRequestKey' => $imageRequestKey,
'remoteUserIconUploadEnabled' => 'false',
'remoteUserIconUploadPath' => 'https://sam-image.hided.net',
'remoteUserIconUploadSecretFile' => 'd_setting/image_upload_core_secret',
'serverList' => [ 'serverList' => [
['che', '체', 'white'], ['che', '체', 'white'],
['kwe', '퀘', 'yellow'], ['kwe', '퀘', 'yellow'],
@@ -227,6 +217,10 @@ $result = Util::generateFileUsingSimpleTemplate(
true true
); );
if ($imageRequestKey) {
@file_get_contents($imageKeyInstallPath . '?key=' . $imageRequestKey);
}
if ($result !== true) { if ($result !== true) {
Json::die([ Json::die([
'result' => false, 'result' => false,
+2 -12
View File
@@ -33,12 +33,7 @@ if ($servHost) {
[ [
'serverBasePath' => $servHost, 'serverBasePath' => $servHost,
'sharedIconPath' => $sharedIconPath, 'sharedIconPath' => $sharedIconPath,
'gameImagePath' => $gameImagePath, 'gameImagePath' => $gameImagePath
'imageRequestPath' => ServConfig::$imageRequestPath,
'imageRequestKey' => ServConfig::$imageRequestKey,
'remoteUserIconUploadEnabled' => ServConfig::$remoteUserIconUploadEnabled ? 'true' : 'false',
'remoteUserIconUploadPath' => ServConfig::$remoteUserIconUploadPath,
'remoteUserIconUploadSecretFile' => ServConfig::$remoteUserIconUploadSecretFile
], ],
true true
); );
@@ -69,12 +64,7 @@ if ($servHost) {
[ [
'serverBasePath' => $servHost, 'serverBasePath' => $servHost,
'sharedIconPath' => $sharedIconPath, 'sharedIconPath' => $sharedIconPath,
'gameImagePath' => $gameImagePath, 'gameImagePath' => $gameImagePath
'imageRequestPath' => ServConfig::$imageRequestPath,
'imageRequestKey' => ServConfig::$imageRequestKey,
'remoteUserIconUploadEnabled' => ServConfig::$remoteUserIconUploadEnabled ? 'true' : 'false',
'remoteUserIconUploadPath' => ServConfig::$remoteUserIconUploadPath,
'remoteUserIconUploadSecretFile' => ServConfig::$remoteUserIconUploadSecretFile
], ],
true true
); );
-17
View File
@@ -1,17 +0,0 @@
CREATE TABLE IF NOT EXISTS "api_log" (
"id" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL,
"ip" TEXT NOT NULL,
"date" TEXT NOT NULL,
"path" TEXT NOT NULL,
"arg" TEXT,
"aux" TEXT,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE INDEX IF NOT EXISTS "by_date" ON "api_log" (
"date" DESC
);
CREATE INDEX IF NOT EXISTS "by_user" ON "api_log" (
"user_id" ASC,
"date" DESC
);
+1 -11
View File
@@ -92,14 +92,4 @@ CREATE TABLE `login_token` (
INDEX `by_date` (`user_id`, `expire_date`) INDEX `by_date` (`user_id`, `expire_date`)
) )
COLLATE='utf8mb4_general_ci' COLLATE='utf8mb4_general_ci'
ENGINE=Aria; ENGINE=Aria;
CREATE TABLE `banned_member` (
`no` INT NOT NULL AUTO_INCREMENT,
`hashed_email` VARCHAR(128) NOT NULL COLLATE 'utf8mb4_general_ci' COMMENT 'SHA512(salt | email | salt )',
`info` TEXT NULL COLLATE 'utf8mb4_general_ci' COMMENT '부가정보',
PRIMARY KEY (`no`),
UNIQUE INDEX `email` (`hashed_email`(128))
)
COLLATE='utf8mb4_general_ci'
ENGINE = Aria;
+5
View File
@@ -61,6 +61,11 @@ class RootDB
if (self::$uDB === null) { if (self::$uDB === null) {
self::$uDB = new \MeekroDB(self::$host, self::$user, self::$password, self::$dbName, self::$port, self::$encoding); self::$uDB = new \MeekroDB(self::$host, self::$user, self::$password, self::$dbName, self::$port, self::$encoding);
self::$uDB->connect_options[MYSQLI_OPT_INT_AND_FLOAT_NATIVE] = true; self::$uDB->connect_options[MYSQLI_OPT_INT_AND_FLOAT_NATIVE] = true;
self::$uDB->error_handler = function () {
};
self::$uDB->throw_exception_on_error = true;
self::$uDB->throw_exception_on_nonsql_error = true;
} }
return self::$uDB; return self::$uDB;
} }
+3 -30
View File
@@ -13,9 +13,6 @@ class ServConfig
public static $gameImagePath = "_tK_gameImagePath_"; public static $gameImagePath = "_tK_gameImagePath_";
public static $imageRequestPath = "_tK_imageRequestPath_"; public static $imageRequestPath = "_tK_imageRequestPath_";
public static $imageRequestKey = '_tK_imageRequestKey_'; public static $imageRequestKey = '_tK_imageRequestKey_';
public static $remoteUserIconUploadEnabled = _tK_remoteUserIconUploadEnabled_;
public static $remoteUserIconUploadPath = '_tK_remoteUserIconUploadPath_';
public static $remoteUserIconUploadSecretFile = '_tK_remoteUserIconUploadSecretFile_';
private static $serverList = null; private static $serverList = null;
public static function getSharedIconPath(string $filepath = ''): string public static function getSharedIconPath(string $filepath = ''): string
@@ -41,33 +38,9 @@ class ServConfig
public static function getImagePullURI(): string public static function getImagePullURI(): string
{ {
return static::$imageRequestPath; $now = time();
} $req_hash = Util::hashPassword(sprintf("%016x", $now), static::$imageRequestKey);
return static::$imageRequestPath . "?req={$req_hash}&time={$now}";
public static function isRemoteUserIconUploadEnabled(): bool
{
return static::$remoteUserIconUploadEnabled;
}
public static function getRemoteUserIconUploadURI(string $filename): string
{
return rtrim(static::$remoteUserIconUploadPath, '/') . '/v1/uploads/user-icons/core/' . $filename;
}
public static function getRemoteUserIconUploadSecret(): string
{
$path = static::$remoteUserIconUploadSecretFile;
if ($path === '' || str_contains($path, "\0")) {
throw new \RuntimeException('Remote user icon upload secret file is not configured');
}
if ($path[0] !== '/') {
$path = ROOT . '/' . $path;
}
$secret = trim((string)file_get_contents($path));
if (strlen($secret) < 32) {
throw new \RuntimeException('Remote user icon upload secret must be at least 32 characters');
}
return $secret;
} }
/** /**
-58
View File
@@ -165,61 +165,3 @@
background: url('_tK_gameImagePath_/map/pokemon_v1/back_pal8.png') no-repeat; background: url('_tK_gameImagePath_/map/pokemon_v1/back_pal8.png') no-repeat;
background-size: 700px 500px; background-size: 700px 500px;
} }
/*CR 지도*/
.map_theme_cr .map_bglayer1{
background: url('_tK_gameImagePath_/map/cr/bg-fs8.png') no-repeat;
background-size: 700px 500px;
}
.map_theme_cr.map_detail .city_base .city_bg {
background-image: none !important;
width: 39.5px !important;
height: 39.5px !important;
left: 1px !important;
top: -4px !important;
opacity: 0.5;
}
.map_theme_cr.map_detail.small_width_map .city_base .city_bg {
background-image: none !important;
width: 28.5px !important;
height: 28.5px !important;
left: 6px !important;
top: 4px !important;
opacity: 0.5;
}
.map_theme_cr.map_detail .city_bg.bFF0000 { background-color: #FF0000; }
.map_theme_cr.map_detail .city_bg.b800000 { background-color: #800000; }
.map_theme_cr.map_detail .city_bg.bA0522D { background-color: #A0522D; }
.map_theme_cr.map_detail .city_bg.bFF6347 { background-color: #FF6347; }
.map_theme_cr.map_detail .city_bg.bFFA500 { background-color: #FFA500; }
.map_theme_cr.map_detail .city_bg.bFFDAB9 { background-color: #FFDAB9; }
.map_theme_cr.map_detail .city_bg.bFFD700 { background-color: #FFD700; }
.map_theme_cr.map_detail .city_bg.bFFFF00 { background-color: #FFFF00; }
.map_theme_cr.map_detail .city_bg.b7CFC00 { background-color: #7CFC00; }
.map_theme_cr.map_detail .city_bg.b00FF00 { background-color: #00FF00; }
.map_theme_cr.map_detail .city_bg.b808000 { background-color: #808000; }
.map_theme_cr.map_detail .city_bg.b008000 { background-color: #008000; }
.map_theme_cr.map_detail .city_bg.b2E8B57 { background-color: #2E8B57; }
.map_theme_cr.map_detail .city_bg.b008080 { background-color: #008080; }
.map_theme_cr.map_detail .city_bg.b20B2AA { background-color: #20B2AA; }
.map_theme_cr.map_detail .city_bg.b6495ED { background-color: #6495ED; }
.map_theme_cr.map_detail .city_bg.b7FFFD4 { background-color: #7FFFD4; }
.map_theme_cr.map_detail .city_bg.bAFEEEE { background-color: #AFEEEE; }
.map_theme_cr.map_detail .city_bg.b87CEEB { background-color: #87CEEB; }
.map_theme_cr.map_detail .city_bg.b00FFFF { background-color: #00FFFF; }
.map_theme_cr.map_detail .city_bg.b00BFFF { background-color: #00BFFF; }
.map_theme_cr.map_detail .city_bg.b0000FF { background-color: #0000FF; }
.map_theme_cr.map_detail .city_bg.b000080 { background-color: #000080; }
.map_theme_cr.map_detail .city_bg.b483D8B { background-color: #483D8B; }
.map_theme_cr.map_detail .city_bg.b7B68EE { background-color: #7B68EE; }
.map_theme_cr.map_detail .city_bg.bBA55D3 { background-color: #BA55D3; }
.map_theme_cr.map_detail .city_bg.b800080 { background-color: #800080; }
.map_theme_cr.map_detail .city_bg.bFF00FF { background-color: #FF00FF; }
.map_theme_cr.map_detail .city_bg.bFFC0CB { background-color: #FFC0CB; }
.map_theme_cr.map_detail .city_bg.bF5F5DC { background-color: #F5F5DC; }
.map_theme_cr.map_detail .city_bg.bE0FFFF { background-color: #E0FFFF; }
.map_theme_cr.map_detail .city_bg.bFFFFFF { background-color: #FFFFFF; }
.map_theme_cr.map_detail .city_bg.bA9A9A9 { background-color: #A9A9A9; }
+2 -6
View File
@@ -13,9 +13,6 @@ $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
[$turntime, $tnmt_time] = $gameStor->getValuesAsArray(['turntime','tnmt_time']); [$turntime, $tnmt_time] = $gameStor->getValuesAsArray(['turntime','tnmt_time']);
$clock = GameClock::fromStorage($gameStor);
$turntimeDisplay = $clock->formatTick(Util::toInt($turntime), true);
$tnmtTimeDisplay = $tnmt_time === null ? '-' : $clock->formatTick(Util::toInt($tnmt_time), true);
$plock = $db->queryFirstField('SELECT plock FROM plock WHERE `type` ="GAME"'); $plock = $db->queryFirstField('SELECT plock FROM plock WHERE `type` ="GAME"');
?> ?>
@@ -24,7 +21,6 @@ $plock = $db->queryFirstField('SELECT plock FROM plock WHERE `type` ="GAME"');
<head> <head>
<title>삼국지 모의전투 HiDCHe</title> <title>삼국지 모의전투 HiDCHe</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<?=WebUtil::printCSS('../d_shared/common.css')?> <?=WebUtil::printCSS('../d_shared/common.css')?>
@@ -33,8 +29,8 @@ $plock = $db->queryFirstField('SELECT plock FROM plock WHERE `type` ="GAME"');
</head> </head>
<body> <body>
<form action=_119_b.php method=post> <form action=_119_b.php method=post>
시간조정 : <input type=text size=3 name=minute><input type=submit name=btn value='분당김'><input type=submit name=btn value='분지연'> 최종갱신 : <?=$turntimeDisplay?> (tick <?=$turntime?>)<br> 시간조정 : <input type=text size=3 name=minute><input type=submit name=btn value='분당김'><input type=submit name=btn value='분지연'> 최종갱신 : <?=$turntime?><br>
시간조정 : <input type=text size=3 name=minute2><input type=submit name=btn value='토너분당김'><input type=submit name=btn value='토너분지연'> 토너먼트 : <?=$tnmtTimeDisplay?> (tick <?=$tnmt_time?>)<br> 시간조정 : <input type=text size=3 name=minute2><input type=submit name=btn value='토너분당김'><input type=submit name=btn value='토너분지연'> 토너먼트 : <?=$tnmt_time?><br>
봉급지급 : <input type=submit name=btn value='금지급'><input type=submit name=btn value='쌀지급'><br> 봉급지급 : <input type=submit name=btn value='금지급'><input type=submit name=btn value='쌀지급'><br>
락 풀 기 : <input type=submit name=btn value='락걸기'><input type=submit name=btn value='락풀기'> 현재 : <?=$plock>0?"동결중":"가동중"?><br> 락 풀 기 : <input type=submit name=btn value='락걸기'><input type=submit name=btn value='락풀기'> 현재 : <?=$plock>0?"동결중":"가동중"?><br>
</form> </form>
+36 -9
View File
@@ -38,9 +38,21 @@ switch ($btn) {
usleep(500000); usleep(500000);
} }
$clock = GameClock::fromStorage($gameStor); $gameStor->cacheValues(['turntime', 'starttime', 'tnmt_time']);
// 스케줄 전체를 벽시계에서 빼지 않고 논리 현재 tick만 앞으로 이동합니다. $turntime = (new \DateTimeImmutable($gameStor->turntime))->sub(new \DateInterval("PT{$minute}M"));
$clock->advance($gameStor, $clock->ticksFromMinutes($minute)); $starttime = (new \DateTimeImmutable($gameStor->starttime))->sub(new \DateInterval("PT{$minute}M"));
$tnmt_time = (new \DateTimeImmutable($gameStor->tnmt_time))->sub(new \DateInterval("PT{$minute}M"));
$gameStor->turntime = $turntime->format('Y-m-d H:i:s.u');
$gameStor->starttime = $starttime->format('Y-m-d H:i:s');
$gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s');
$db->update('general', [
'turntime' => $db->sqleval('DATE_SUB(turntime, INTERVAL %i MINUTE)', $minute)
], true);
$db->update('ng_auction', [
'close_date' => $db->sqleval('DATE_SUB(close_date, INTERVAL %i MINUTE)', $minute)
], 'finished = 0');
if ($locked) { if ($locked) {
unlock(); unlock();
} }
@@ -54,19 +66,34 @@ switch ($btn) {
} }
usleep(500000); usleep(500000);
} }
$clock = GameClock::fromStorage($gameStor); $gameStor->cacheValues(['turntime', 'starttime', 'tnmt_time']);
$clock->advance($gameStor, -$clock->ticksFromMinutes($minute)); $turntime = (new \DateTimeImmutable($gameStor->turntime))->add(new \DateInterval("PT{$minute}M"));
$starttime = (new \DateTimeImmutable($gameStor->starttime))->add(new \DateInterval("PT{$minute}M"));
$tnmt_time = (new \DateTimeImmutable($gameStor->tnmt_time))->add(new \DateInterval("PT{$minute}M"));
$gameStor->turntime = $turntime->format('Y-m-d H:i:s.u');
$gameStor->starttime = $starttime->format('Y-m-d H:i:s');
$gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s');
$db->update('general', [
'turntime' => $db->sqleval('DATE_ADD(turntime, INTERVAL %i MINUTE)', $minute)
], true);
$db->update('ng_auction', [
'close_date' => $db->sqleval('DATE_ADD(close_date, INTERVAL %i MINUTE)', $minute)
], 'finished = 0');
if ($locked) { if ($locked) {
unlock(); unlock();
} }
break; break;
case "토너분당김": case "토너분당김":
$clock = GameClock::fromStorage($gameStor); $tnmt_time = new \DateTime($gameStor->tnmt_time);
$gameStor->tnmt_time = Util::toInt($gameStor->tnmt_time) - $clock->ticksFromMinutes($minute2); $tnmt_time->sub(new \DateInterval("PT{$minute2}M"));
$gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s');
break; break;
case "토너분지연": case "토너분지연":
$clock = GameClock::fromStorage($gameStor); $tnmt_time = new \DateTimeImmutable($gameStor->tnmt_time);
$gameStor->tnmt_time = Util::toInt($gameStor->tnmt_time) + $clock->ticksFromMinutes($minute2); $tnmt_time->add(new \DateInterval("PT{$minute2}M"));
$gameStor->tnmt_time = $tnmt_time->format('Y-m-d H:i:s');
break; break;
case "금지급": case "금지급":
processGoldIncome(); processGoldIncome();
+5 -4
View File
@@ -6,7 +6,7 @@ include "func.php";
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if($session->userGrade < 6) { if($session->userGrade < 5) {
die(requireAdminPermissionHTML()); die(requireAdminPermissionHTML());
} }
@@ -19,7 +19,6 @@ $admin = getAdmin();
<head> <head>
<title>게임관리</title> <title>게임관리</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<?=WebUtil::printCSS('../d_shared/common.css')?> <?=WebUtil::printCSS('../d_shared/common.css')?>
@@ -40,7 +39,9 @@ $admin = getAdmin();
</tr> </tr>
<tr> <tr>
<td width=110 align=right>시작시간변경</td> <td width=110 align=right>시작시간변경</td>
<td width=285><input type=text size=20 maxlength=20 style=color:white;background-color:black;text-align:right; name=starttime value='<?=substr($admin['starttime_display'], 0, 19)?>'><input type=submit name=btn value=변경1></td> <td width=285><input type=text size=20 maxlength=20 style=color:white;background-color:black;text-align:right; name=starttime value='<?=substr($admin['starttime'], 0, 19)?>'><input type=submit name=btn value=변경1></td>
<td width=110 align=right>현재도시훈사</td>
<td width=285><?=$admin['city_rate']?></td>
</tr> </tr>
<tr> <tr>
<td width=110 align=right>최대 장수</td> <td width=110 align=right>최대 장수</td>
@@ -52,7 +53,7 @@ $admin = getAdmin();
<td width=110 align=right>시작 년도</td> <td width=110 align=right>시작 년도</td>
<td width=285><input type=text size=3 maxlength=3 style=color:white;background-color:black;text-align:right; name=startyear value='<?=$admin['startyear']?>'><input type=submit name=btn value=변경4></td> <td width=285><input type=text size=3 maxlength=3 style=color:white;background-color:black;text-align:right; name=startyear value='<?=$admin['startyear']?>'><input type=submit name=btn value=변경4></td>
<td width=110 align=right>최근 갱신 시간</td> <td width=110 align=right>최근 갱신 시간</td>
<td width=285>&nbsp;<?=$admin['turntime_display']?> (tick <?=$admin['turntime']?>)</td> <td width=285>&nbsp;<?=$admin['turntime']?></td>
</tr> </tr>
<tr> <tr>
<td width=110 align=right>턴시간</td> <td width=110 align=right>턴시간</td>
+26 -11
View File
@@ -6,7 +6,7 @@ include "func.php";
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 6) { if ($session->userGrade < 5) {
header('location:_admin1.php'); header('location:_admin1.php');
die(); die();
} }
@@ -25,15 +25,13 @@ if (!$v->validate()) {
$msg = Util::getPost('msg'); $msg = Util::getPost('msg');
$btn = Util::getPost('btn'); $btn = Util::getPost('btn');
$log = Util::getPost('log'); $log = Util::getPost('log');
$starttime = Util::getPost('starttime', 'string', null); $starttime = Util::getPost('starttime', 'string', (new \DateTime())->format('Y-m-d H:i:s'));
$maxgeneral = Util::getPost('maxgeneral', 'int', GameConst::$defaultMaxGeneral); $maxgeneral = Util::getPost('maxgeneral', 'int', GameConst::$defaultMaxGeneral);
$maxnation = Util::getPost('maxnation', 'int', GameConst::$defaultMaxNation); $maxnation = Util::getPost('maxnation', 'int', GameConst::$defaultMaxNation);
$startyear = Util::getPost('startyear', 'int', GameConst::$defaultStartYear); $startyear = Util::getPost('startyear', 'int', GameConst::$defaultStartYear);
$db = DB::db(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
$clock = GameClock::fromStorage($gameStor);
$starttime ??= $clock->formatTick($clock->nowTick());
$admin = getAdmin(); $admin = getAdmin();
@@ -45,11 +43,7 @@ switch ($btn) {
pushGlobalHistoryLog(["<R>★</><S>{$log}</>"]); pushGlobalHistoryLog(["<R>★</><S>{$log}</>"]);
break; break;
case "변경1": case "변경1":
$gameStor->clock_base_time = TimeUtil::format(GameClock::baseTimeForProjection( $gameStor->starttime = (new \DateTime($starttime))->format('Y-m-d H:i:s');
new \DateTimeImmutable($starttime),
Util::toInt($gameStor->starttime),
$clock->getTurnTermMinutes(),
), true);
break; break;
case "변경2": case "변경2":
$gameStor->maxgeneral = $maxgeneral; $gameStor->maxgeneral = $maxgeneral;
@@ -80,8 +74,29 @@ switch ($btn) {
case "120분턴": $turnterm = 120; break; case "120분턴": $turnterm = 120; break;
default: throw new \Exception("알 수 없는 턴 기간"); default: throw new \Exception("알 수 없는 턴 기간");
} }
ServerTool::changeServerTerm($turnterm); $oldunit = $admin['turnterm'] * 60;
$unit = $turnterm * 60;
$unitDiff = $unit / $oldunit;
$servTurnTime = new \DateTimeImmutable($admin['turntime']);
foreach ($db->query('SELECT no,turntime FROM general') as $gen) {
$genTurnTime = new \DateTimeImmutable($gen['turntime']);
$timeDiff = TimeUtil::DateIntervalToSeconds($genTurnTime->diff($servTurnTime));
$timeDiff *= $unitDiff;
$newGenTurnTime = $servTurnTime->add(TimeUtil::secondsToDateInterval($timeDiff));
$db->update('general', [
'turntime'=>$newGenTurnTime->format('Y-m-d H:i:s.u')
], 'no=%i', $gen['no']);
}
$turn = ($admin['year'] - $admin['startyear']) * 12 + $admin['month'] - 1;
$starttime = $servTurnTime->sub(TimeUtil::secondsToDateInterval($turn * $unit))->format('Y-m-d H:i:s');
$starttime = cutTurn($starttime, $turnterm, false);
$gameStor->turnterm = $turnterm;
$gameStor->starttime = $starttime;
pushGlobalHistoryLog(["<R>★</>턴시간이 <C>$btn</>으로 변경됩니다."]);
break; break;
} }
header('location:_admin1.php'); header('location:_admin1.php');
+1 -2
View File
@@ -7,7 +7,7 @@ include "func.php";
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 6) { if ($session->userGrade < 5) {
die(requireAdminPermissionHTML()); die(requireAdminPermissionHTML());
} }
@@ -20,7 +20,6 @@ $db = DB::db();
<head> <head>
<title>회원관리</title> <title>회원관리</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<?= WebUtil::printCSS('../d_shared/common.css') ?> <?= WebUtil::printCSS('../d_shared/common.css') ?>
+21 -23
View File
@@ -2,8 +2,6 @@
namespace sammo; namespace sammo;
use sammo\Enums\MessageType;
include "lib.php"; include "lib.php";
include "func.php"; include "func.php";
@@ -15,7 +13,7 @@ $msg = Util::getPost('msg', 'string');
//로그인 검사 //로그인 검사
$session = Session::requireLogin()->loginGame()->setReadOnly(); $session = Session::requireLogin()->loginGame()->setReadOnly();
if ($session->userGrade < 6) { if ($session->userGrade < 5) {
header('location:_admin2.php'); header('location:_admin2.php');
} }
@@ -33,18 +31,18 @@ $src = MessageTarget::buildQuick($session->generalID);
$genObjList = []; $genObjList = [];
$env = []; $env = [];
if ($genlist) { if ($genlist) {
$genObjList = General::createObjListFromDB($genlist); $genObjList = General::createGeneralObjListFromDB($genlist);
$env = $gameStor->cacheAll(); $env = $gameStor->cacheAll();
} }
switch ($btn) { switch ($btn) {
case "전체 접속허용": case "전체 접속허용":
$db->update('general_access_log', [ $db->update('general', [
'refresh_score' => 0 'con' => 0
], true); ], true);
break; break;
case "전체 접속제한": case "전체 접속제한":
$db->update('general_access_log', [ $db->update('general', [
'refresh_score' => 1000 'con' => 1000
], true); ], true);
break; break;
case "블럭 해제": case "블럭 해제":
@@ -99,7 +97,7 @@ switch ($btn) {
], '`no` IN %li', $genlist); ], '`no` IN %li', $genlist);
break; break;
case "강제 사망": case "강제 사망":
$date = GameClock::fromStorage($gameStor)->nowTick(); $date = TimeUtil::now(true);
$db->update('general', [ $db->update('general', [
'killturn' => 0, 'killturn' => 0,
'turntime' => $date, 'turntime' => $date,
@@ -113,7 +111,7 @@ switch ($btn) {
case "경험치1000": case "경험치1000":
$text = $btn . " 지급!"; $text = $btn . " 지급!";
foreach ($genlist as $generalID) { foreach ($genlist as $generalID) {
$msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -124,7 +122,7 @@ switch ($btn) {
case "공헌치1000": case "공헌치1000":
$text = $btn . " 지급!"; $text = $btn . " 지급!";
foreach ($genlist as $generalID) { foreach ($genlist as $generalID) {
$msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -135,7 +133,7 @@ switch ($btn) {
case "보숙10000": case "보숙10000":
$text = "보병숙련도+10000 지급!"; $text = "보병숙련도+10000 지급!";
foreach ($genlist as $generalID) { foreach ($genlist as $generalID) {
$msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -145,7 +143,7 @@ switch ($btn) {
case "궁숙10000": case "궁숙10000":
$text = "궁병숙련도+10000 지급!"; $text = "궁병숙련도+10000 지급!";
foreach ($genlist as $generalID) { foreach ($genlist as $generalID) {
$msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -156,7 +154,7 @@ switch ($btn) {
$src = MessageTarget::buildQuick($session->generalID); $src = MessageTarget::buildQuick($session->generalID);
$text = "기병숙련도+10000 지급!"; $text = "기병숙련도+10000 지급!";
foreach ($genlist as $generalID) { foreach ($genlist as $generalID) {
$msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -167,7 +165,7 @@ switch ($btn) {
$src = MessageTarget::buildQuick($session->generalID); $src = MessageTarget::buildQuick($session->generalID);
$text = "귀병숙련도+10000 지급!"; $text = "귀병숙련도+10000 지급!";
foreach ($genlist as $generalID) { foreach ($genlist as $generalID) {
$msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -178,7 +176,7 @@ switch ($btn) {
$src = MessageTarget::buildQuick($session->generalID); $src = MessageTarget::buildQuick($session->generalID);
$text = "차병숙련도+10000 지급!"; $text = "차병숙련도+10000 지급!";
foreach ($genlist as $generalID) { foreach ($genlist as $generalID) {
$msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true); $msg->send(true);
} }
$db->update('general', [ $db->update('general', [
@@ -186,19 +184,19 @@ switch ($btn) {
], '`no` IN %li', $genlist); ], '`no` IN %li', $genlist);
break; break;
case "접속 허용": case "접속 허용":
$db->update('general_access_log', [ $db->update('general', [
'refresh_score' => 0 'con' => 0
], '`general_id` IN %li', $genlist); ], '`no` IN %li', $genlist);
break; break;
case "접속 제한": case "접속 제한":
$db->update('general_access_log', [ $db->update('general', [
'refresh_score' => 1000 'con' => 1000
], '`general_id` IN %li', $genlist); ], '`no` IN %li', $genlist);
break; break;
case "메세지 전달": case "메세지 전달":
$text = $msg ?? ''; $text = $msg ?? '';
foreach ($genlist as $generalID) { foreach ($genlist as $generalID) {
$msg = new Message(MessageType::private, $src, MessageTarget::buildQuick($generalID), $text, Message::gameNow(), new \DateTime('9999-12-31'), []); $msg = new Message(Message::MSGTYPE_PRIVATE, $src, MessageTarget::buildQuick($generalID), $text, new \DateTime(), new \DateTime('9999-12-31'), []);
$msg->send(true); $msg->send(true);
} }
break; break;
+132
View File
@@ -0,0 +1,132 @@
<?php
namespace sammo;
include "lib.php";
include "func.php";
//로그인 검사
$session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 5) {
die(requireAdminPermissionHTML());
}
$db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env');
$conlimit = $gameStor->conlimit;
$ipGroupList = Util::arrayGroupBy(
$db->query(
'SELECT name,ip,lastconnect,owner,block,substring_index(ip,".",3) as ip_c from general WHERE ip!="" and npc<2'
),
'ip_c'
);
function colorBlockedName($general)
{
if (!$general['blocked']) {
return $general['name'];
}
return "<span style='color:magenta;'>{$general['name']}</span>";
}
?>
<!DOCTYPE html>
<html>
<head>
<title>멀티관리</title>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" />
<?= WebUtil::printCSS('../d_shared/common.css') ?>
<?= WebUtil::printDist('ts', 'common', true) ?>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css" />
</head>
<body>
<table align=center width=1000 class='tb_layout bg0'>
<tr>
<td>멀 티 관 리<br><?= backButton() ?></td>
</tr>
</table>
<form name=form1 method=post action=_admin4_submit.php>
<table align=center width=1000 class='tb_layout bg0'>
<tr>
<td width=80 align=center rowspan=3>회원선택<br><br>
<font color=cyan>NPC</font><br>
<font color=skyblue>NPC유저</font><br>
<font color=red>접속제한</font><br><b style=background-color:red;>블럭회원</b>
</td>
<td width=105 rowspan=3>
<?php
echo "
<select name=genlist[] size=20 multiple style='color:white;background-color:black;font-size:14px'>";
foreach ($db->query('SELECT no,name,npc,block,con from general where ip!=\'\' order by npc,ip') as $general) {
$style = "style=;";
if ($general['block'] > 0) {
$style .= "background-color:red;";
}
$npcColor = getNPCColor($general['npc']);
if($npcColor !== null){
$style .= "color:{$npcColor};";
}
echo "
<option value={$general['no']} $style>{$general['name']}</option>";
}
echo "
</select>";
?>
</td>
<td width=100 align=center>블럭</td>
<td width=504>
<input type=submit name=btn value='블럭 해제'><input type=submit name=btn value='1단계 블럭'><input type=submit name=btn value='2단계 블럭'><input type=submit name=btn value='3단계 블럭'><input type=submit name=btn value='무한삭턴'><br>
1단계:발언권, 2단계:턴블럭
</td>
</tr>
<tr>
<td align=center>강제 사망</td>
<td><input type=submit name=btn value='강제 사망'></td>
</tr>
<tr>
<td align=center>메세지 전달</td>
<td><input type=textarea size=60 maxlength=255 name=msg style=background-color:black;color:white;><input type=submit name=btn value='메세지 전달'></td>
</tr>
</table>
<table align=center width=1000 class='tb_layout bg0'>
<tr>
<td align=center width=100>장수명</td>
<td align=center width=180>최근로그인</td>
<td align=center width=129>IP</td>
<td align=center width=100>ID</td>
</tr>
<?php foreach ($ipGroupList as $ipGroupC => $users) : ?>
<tr>
<td><?= join('<br>', array_map('\sammo\colorBlockedName', $users)) ?></td>
<td><?= join('<br>', array_column($users, 'lastconnect')) ?></td>
<td><?= join('<br>', array_column($users, 'ip')) ?></td>
<td><?= join('<br>', array_column($users, 'owner')) ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php
//NOTE: password의 md5 해시가 같은지 확인하는 방식으로는 앞으로 잡아낼 수 없다. 폐기
?>
</form>
<table align=center width=1000 class='tb_layout bg0'>
<tr>
<td><?= backButton() ?></td>
</tr>
<tr>
<td><?= banner() ?> </td>
</tr>
</table>
</body>
</html>
+84
View File
@@ -0,0 +1,84 @@
<?php
namespace sammo;
include "lib.php";
include "func.php";
//로그인 검사
$session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 5) {
header('location:_admin4.php');
die();
}
$btn = Util::getPost('btn');
$genlist = Util::getPost('genlist', 'array_int');
$msg = Util::getPost('msg', 'string', '메시지');
$db = DB::db();
//NOTE: 왜 기능이 admin2와 admin4가 같이 있는가?
//NOTE: 왜 블럭 시 admin4에선 금쌀을 없애지 않는가?
switch ($btn) {
case "블럭 해제":
DB::db()->query('update general set block=0 where no IN %li', $genlist);
break;
case "1단계 블럭":
$date = TimeUtil::now();
$db = DB::db();
$db->query('update general set block=1,killturn=24 where no IN %li', $genlist);
//FIXME: subquery로 하는게 더 빠를 듯.
$uid = $db->queryFirstColumn('select owner from general where no IN %li', $genlist);
RootDB::db()->query('update member set block_num=block_num+1,block_date=%s where id IN %ls', $date, $uid);
break;
case "2단계 블럭":
$date = TimeUtil::now();
$db = DB::db();
$db->query('update general set block=2,killturn=24 where no IN %li', $genlist);
$uid = $db->queryFirstColumn('select owner from general where no IN %li', $genlist);
RootDB::db()->query('update member set block_num=block_num+1,block_date=%s where id IN %ls', $date, $uid);
break;
case "3단계 블럭":
$date = TimeUtil::now();
$db = DB::db();
$db->query('update general set block=3,killturn=24 where no IN %li', $genlist);
$uid = $db->queryFirstColumn('select owner from general where no IN %li', $genlist);
RootDB::db()->query('update member set block_num=block_num+1,block_date=%s where id IN %ls', $date, $uid);
break;
case "무한삭턴":
DB::db()->query('update general set killturn=8000 where no IN %li', $genlist);
break;
case "강제 사망":
$date = TimeUtil::now(true);
$db->update('general', [
'killturn' => 0,
'turntime' => $date,
], '`no` IN %li', $genlist);
$db->update('general_turn', [
'action' => '휴식',
'arg' => '{}',
'brief' => '휴식'
], 'general_id IN %li AND turn_idx = 0', $genlist);
break;
case "메세지 전달":
$date = TimeUtil::now();
$src = MessageTarget::buildQuick($session->generalID);
foreach($genlist as $generalID){
$msgObj = new Message(
Message::MSGTYPE_PRIVATE,
$src,
MessageTarget::buildQuick($generalID),
$msg,
new \DateTime(),
new \DateTime('9999-12-31'),
[]
);
if ($msgObj) {
$msgObj->send(true);
}
}
break;
}
header('location:_admin4.php');
+15 -2
View File
@@ -20,7 +20,7 @@ if ($type2 < 0 || $type2 > 6) {
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 6) { if ($session->userGrade < 5) {
die(requireAdminPermissionHTML()); die(requireAdminPermissionHTML());
} }
@@ -38,7 +38,6 @@ $sel2[$type2] = "selected";
<head> <head>
<title>일제정보</title> <title>일제정보</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<?= WebUtil::printCSS('../d_shared/common.css') ?> <?= WebUtil::printCSS('../d_shared/common.css') ?>
@@ -66,6 +65,8 @@ $sel2[$type2] = "selected";
<option <?= $sel[8] ?? '' ?> value=8>평무</option> <option <?= $sel[8] ?? '' ?> value=8>평무</option>
<option <?= $sel[9] ?? '' ?> value=9>평지</option> <option <?= $sel[9] ?? '' ?> value=9>평지</option>
<option <?= $sel[10] ?? '' ?> value=10>평Lv</option> <option <?= $sel[10] ?? '' ?> value=10>평Lv</option>
<option <?= $sel[11] ?? '' ?> value=11>접속률</option>
<option <?= $sel[12] ?? '' ?> value=12>단기접</option>
<option <?= $sel[13] ?? '' ?> value=13>보숙</option> <option <?= $sel[13] ?? '' ?> value=13>보숙</option>
<option <?= $sel[14] ?? '' ?> value=14>궁숙</option> <option <?= $sel[14] ?? '' ?> value=14>궁숙</option>
<option <?= $sel[15] ?? '' ?> value=15>기숙</option> <option <?= $sel[15] ?? '' ?> value=15>기숙</option>
@@ -103,6 +104,8 @@ $sel2[$type2] = "selected";
<table align=center width=1600 class="tb_layout bg0"> <table align=center width=1600 class="tb_layout bg0">
<tr class='bg1'> <tr class='bg1'>
<td align=center>국명</td> <td align=center>국명</td>
<td align=center>접률</td>
<td align=center>단접</td>
<td align=center>국력</td> <td align=center>국력</td>
<td align=center>장수</td> <td align=center>장수</td>
<td align=center>속령</td> <td align=center>속령</td>
@@ -143,6 +146,8 @@ SELECT
A.gold, A.gold,
A.rice, A.rice,
COUNT(B.nation) AS gennum, COUNT(B.nation) AS gennum,
ROUND(AVG(B.connect), 1) AS connect,
ROUND(AVG(B.con), 1) AS con,
ROUND(AVG(B.dex1)) AS dex1, ROUND(AVG(B.dex1)) AS dex1,
ROUND(AVG(B.dex2)) AS dex2, ROUND(AVG(B.dex2)) AS dex2,
ROUND(AVG(B.dex3)) AS dex3, ROUND(AVG(B.dex3)) AS dex3,
@@ -187,6 +192,12 @@ GROUP BY B.nation
case 10: case 10:
$query .= " order by avg(B.explevel) desc"; $query .= " order by avg(B.explevel) desc";
break; break;
case 11:
$query .= " order by avg(B.connect) desc";
break;
case 12:
$query .= " order by avg(B.con) desc";
break;
case 13: case 13:
$query .= " order by avg(B.dex1) desc"; $query .= " order by avg(B.dex1) desc";
break; break;
@@ -228,6 +239,8 @@ from city where nation=%i', $nation['nation']);
echo " echo "
<tr> <tr>
<td align=center style=background-color:{$nation['color']};color:" . newColor($nation['color']) . ";>{$nation['name']}</td> <td align=center style=background-color:{$nation['color']};color:" . newColor($nation['color']) . ";>{$nation['name']}</td>
<td align=center>&nbsp;{$nation['connect']}&nbsp;</td>
<td align=center>&nbsp;{$nation['con']}&nbsp;</td>
<td align=center>&nbsp;{$nation['power']}&nbsp;</td> <td align=center>&nbsp;{$nation['power']}&nbsp;</td>
<td align=center>&nbsp;{$gen['cnt']}&nbsp;</td> <td align=center>&nbsp;{$gen['cnt']}&nbsp;</td>
<td align=center>&nbsp;{$city['cnt']}&nbsp;</td> <td align=center>&nbsp;{$city['cnt']}&nbsp;</td>
+1 -1
View File
@@ -12,7 +12,7 @@ $nation = Util::getPost('nation', 'int');
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
$userID = Session::getUserID(); $userID = Session::getUserID();
if ($session->userGrade < 6) { if ($session->userGrade < 5) {
header('location:_admin5.php'); header('location:_admin5.php');
die(); die();
} }
+5 -9
View File
@@ -2,8 +2,6 @@
namespace sammo; namespace sammo;
use sammo\Enums\GeneralQueryMode;
include "lib.php"; include "lib.php";
include "func.php"; include "func.php";
@@ -31,18 +29,17 @@ $queryMap = [
]; ];
if ($reqQueryType === null || !key_exists($reqQueryType, $queryMap)) { if ($reqQueryType === null || !key_exists($reqQueryType, $queryMap)) {
$reqQueryType = array_key_first($queryMap); $reqQueryType = Util::array_first_key($queryMap);
} }
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 6) { if ($session->userGrade < 5) {
die(requireAdminPermissionHTML()); die(requireAdminPermissionHTML());
} }
$db = DB::db(); $db = DB::db();
$clock = GameClock::fromStorage(KVStorage::getStorage($db, 'game_env'));
if ($btn == '정렬하기') { if ($btn == '정렬하기') {
$gen = 0; $gen = 0;
@@ -77,7 +74,7 @@ if (!$gen) {
$gen = $generalBasicList[0]['no']; $gen = $generalBasicList[0]['no'];
} }
$generalObj = General::createObjFromDB($gen, null, GeneralQueryMode::FullWithAccessLog); $generalObj = General::createGeneralObjFromDB($gen);
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
@@ -86,7 +83,6 @@ $generalObj = General::createObjFromDB($gen, null, GeneralQueryMode::FullWithAcc
<head> <head>
<title>로그정보</title> <title>로그정보</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<?= WebUtil::printCSS('../d_shared/common.css') ?> <?= WebUtil::printCSS('../d_shared/common.css') ?>
@@ -112,7 +108,7 @@ $generalObj = General::createObjFromDB($gen, null, GeneralQueryMode::FullWithAcc
대상장수 : 대상장수 :
<select name=gen size=1> <select name=gen size=1>
<?php foreach ($generalBasicList as $general) : ?> <?php foreach ($generalBasicList as $general) : ?>
<option <?= $gen == $general['no'] ? 'selected' : '' ?> value='<?= $general['no'] ?>'><?= $general['name'] ?> (<?= substr($clock->formatTick(Util::toInt($general['turntime'])), 14, 5) ?>)</option> <option <?= $gen == $general['no'] ? 'selected' : '' ?> value='<?= $general['no'] ?>'><?= $general['name'] ?> (<?= substr($general['turntime'], 14, 5) ?>)</option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<input type=submit name=btn value='조회하기'> <input type=submit name=btn value='조회하기'>
@@ -180,4 +176,4 @@ $generalObj = General::createObjFromDB($gen, null, GeneralQueryMode::FullWithAcc
</table> </table>
</body> </body>
</html> </html>
+4 -3
View File
@@ -12,7 +12,7 @@ $type = 0;
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 6) { if ($session->userGrade < 5) {
die(requireAdminPermissionHTML()); die(requireAdminPermissionHTML());
} }
@@ -30,7 +30,6 @@ $sel[$type] = "selected";
<head> <head>
<title>외교정보</title> <title>외교정보</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<?= WebUtil::printCSS('../d_shared/common.css') ?> <?= WebUtil::printCSS('../d_shared/common.css') ?>
@@ -100,6 +99,8 @@ $sel[$type] = "selected";
throw new \Exception("알 수 없는 외교 상태: {$dip['state']}"); throw new \Exception("알 수 없는 외교 상태: {$dip['state']}");
} }
$date = TimeUtil::now();
echo " echo "
<tr> <tr>
<td align=center style=color:" . newColor($nationColor[$me]) . ";background-color:{$nationColor[$me]};>$nationName[$me]</td> <td align=center style=color:" . newColor($nationColor[$me]) . ";background-color:{$nationColor[$me]};>$nationName[$me]</td>
@@ -120,4 +121,4 @@ $sel[$type] = "selected";
</table> </table>
</body> </body>
</html> </html>
+4 -3
View File
@@ -7,7 +7,7 @@ include "func.php";
//로그인 검사 //로그인 검사
$session = Session::requireGameLogin()->setReadOnly(); $session = Session::requireGameLogin()->setReadOnly();
if ($session->userGrade < 6) { if ($session->userGrade < 5) {
die('권한 부족'); die('권한 부족');
} }
@@ -26,7 +26,8 @@ foreach ($db->queryFirstColumn(
} }
$inheritPointManager = InheritancePointManager::getInstance(); $inheritPointManager = InheritancePointManager::getInstance();
foreach(General::createObjListFromDB($db->queryFirstColumn('SELECT `no` FROM general WHERE npc = 0')) as $genObj){ foreach(General::createGeneralObjListFromDB($db->queryFirstColumn('SELECT `no` FROM general WHERE npc = 0')) as $genObj){
$inheritPointManager->mergeTotalInheritancePoint($genObj); $inheritPointManager->mergeTotalInheritancePoint($genObj, true);
$inheritPointManager->applyInheritanceUser($genObj->getVar('owner')); $inheritPointManager->applyInheritanceUser($genObj->getVar('owner'));
$inheritPointManager->clearInheritancePoint($genObj);
} }
+2 -6
View File
@@ -28,7 +28,6 @@ $templates = new \League\Plates\Engine(__DIR__ . '/templates');
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=500" /> <meta name="viewport" content="width=500" />
<title><?= UniqueConst::$serverName ?>: 명장일람</title> <title><?= UniqueConst::$serverName ?>: 명장일람</title>
@@ -243,14 +242,11 @@ $templates = new \League\Plates\Engine(__DIR__ . '/templates');
"SELECT nation,no,name,owner_name as ownerName, owner, picture, imgsvr, "SELECT nation,no,name,owner_name as ownerName, owner, picture, imgsvr,
experience, dedication, experience, dedication,
dex1, dex2, dex3, dex4, dex5, dex1, dex2, dex3, dex4, dex5,
horse, weapon, book, item, aux horse, weapon, book, item
FROM general WHERE %l", FROM general WHERE %l",
$btn == "NPC 보기" ? "npc>=2" : "npc<2" $btn == "NPC 보기" ? "npc>=2" : "npc<2"
) as $general) { ) as $general) {
$generalID = $general['no']; $generalID = $general['no'];
foreach (['dex1', 'dex2', 'dex3', 'dex4', 'dex5'] as $dexKey) {
$general[$dexKey] = CentennialAllStarGrowthService::recordableRawValue($general, $dexKey);
}
$general['bgColor'] = $nationColor[$general['nation']] ?? GameConst::$basecolor4; $general['bgColor'] = $nationColor[$general['nation']] ?? GameConst::$basecolor4;
$general['fgColor'] = newColor($general['bgColor']); $general['fgColor'] = newColor($general['bgColor']);
$general['nationName'] = $nationName[$general['nation']]; $general['nationName'] = $nationName[$general['nation']];
@@ -430,4 +426,4 @@ $templates = new \League\Plates\Engine(__DIR__ . '/templates');
</div> </div>
</body> </body>
</html> </html>
-1
View File
@@ -14,7 +14,6 @@ increaseRefresh("왕조일람", 1);
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 왕조일람</title> <title><?= UniqueConst::$serverName ?>: 왕조일람</title>
-1
View File
@@ -24,7 +24,6 @@ $serverID = $emperior['server_id'] ?? ($emperior['serverID'] ?? null);
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 왕조일람</title> <title><?= UniqueConst::$serverName ?>: 왕조일람</title>
+8 -17
View File
@@ -16,13 +16,10 @@ $gameStor = KVStorage::getStorage($db, 'game_env');
increaseRefresh("장수일람", 2); increaseRefresh("장수일람", 2);
$me = $db->queryFirstRow( $me = $db->queryFirstRow('SELECT con,turntime FROM general WHERE owner = %i', $userID);
'SELECT refresh_score,turntime FROM `general`
LEFT JOIN general_access_log AS l ON `general`.no = l.general_id WHERE owner = %i', $userID
);
$limitState = checkLimit($me['refresh_score']); $con = checkLimit($me['con']);
if ($limitState >= 2) { if ($con >= 2) {
printLimitMsg($me['turntime']); printLimitMsg($me['turntime']);
exit(); exit();
} }
@@ -44,7 +41,6 @@ if ($gameStor->isunited) {
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 장수일람</title> <title><?= UniqueConst::$serverName ?>: 장수일람</title>
@@ -105,7 +101,7 @@ if ($gameStor->isunited) {
6 => ['dedication', true], 6 => ['dedication', true],
7 => ['officer_level', true], 7 => ['officer_level', true],
8 => ['killturn', false], 8 => ['killturn', false],
9 => ['refresh_score_total', true], 9 => ['connect', true],
10 => ['experience', true], 10 => ['experience', true],
11 => ['personal', true], 11 => ['personal', true],
12 => ['special', true], 12 => ['special', true],
@@ -114,12 +110,7 @@ if ($gameStor->isunited) {
15 => ['npc', true], 15 => ['npc', true],
][$type]; ][$type];
$generalList = $db->query( $generalList = $db->query('SELECT owner,no,picture,imgsvr,npc,age,nation,special,special2,personal,name,injury,leadership,strength,intel,experience,dedication,officer_level,killturn,connect from general order by %b %l', $orderKey, $orderDesc ? 'desc' : '');
'SELECT owner,no,picture,imgsvr,npc,age,nation,special,special2,personal,name,injury,leadership,strength,intel,experience,dedication,officer_level,killturn,refresh_score_total
FROM `general` LEFT JOIN `general_access_log` ON general.no = general_access_log.general_id order by %b %l',
$orderKey,
$orderDesc ? 'desc' : ''
);
echo " echo "
<table align=center width=1000 class='tb_layout bg0'> <table align=center width=1000 class='tb_layout bg0'>
@@ -171,7 +162,7 @@ if ($gameStor->isunited) {
$name = $name . '<br><small>(' . $ownerNameList[$general['owner']] . ')</small>'; $name = $name . '<br><small>(' . $ownerNameList[$general['owner']] . ')</small>';
} }
$general['refresh_score_total'] = Util::round($general['refresh_score_total'], -1); $general['connect'] = Util::round($general['connect'], -1);
$imageTemp = GetImageURL($general['imgsvr']); $imageTemp = GetImageURL($general['imgsvr']);
echo " echo "
@@ -200,8 +191,8 @@ if ($gameStor->isunited) {
<td align=center>$strength</td> <td align=center>$strength</td>
<td align=center>$intel</td> <td align=center>$intel</td>
<td align=center>{$general['killturn']}</td> <td align=center>{$general['killturn']}</td>
<td align=center>" . $general['refresh_score_total'] ?? 0; <td align=center>{$general['connect']}";
echo "<br>【" . getRefreshScoreText($general['refresh_score_total'] ?? 0) . "】</td> echo "<br>【" . getConnect($general['connect']) . "】</td>
</tr>"; </tr>";
} }
echo " echo "
-1
View File
@@ -43,7 +43,6 @@ if ($scenarioIdx && key_exists($scenarioIdx, $scenarioList[$seasonIdx] ?? [])) {
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=500" /> <meta name="viewport" content="width=500" />
<title><?= UniqueConst::$serverName ?>: 명예의 전당</title> <title><?= UniqueConst::$serverName ?>: 명예의 전당</title>
+3 -7
View File
@@ -13,13 +13,10 @@ $gameStor = KVStorage::getStorage($db, 'game_env');
$gameStor->cacheValues(['killturn', 'autorun_user', 'turnterm']); $gameStor->cacheValues(['killturn', 'autorun_user', 'turnterm']);
increaseRefresh("세력일람", 2); increaseRefresh("세력일람", 2);
$me = $db->queryFirstRow( $me = $db->queryFirstRow('SELECT con, turntime FROM general WHERE owner=%i', $userID);
'SELECT refresh_score, turntime FROM `general`
LEFT JOIN general_access_log AS l ON `general`.no = l.general_id WHERE owner=%i', $userID
);
$limitState = checkLimit($me['refresh_score']); $con = checkLimit($me['con']);
if ($limitState >= 2) { if ($con >= 2) {
printLimitMsg($me['turntime']); printLimitMsg($me['turntime']);
exit(); exit();
} }
@@ -29,7 +26,6 @@ if ($limitState >= 2) {
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 세력일람</title> <title><?= UniqueConst::$serverName ?>: 세력일람</title>
-1
View File
@@ -24,7 +24,6 @@ $sel[$type] = "selected";
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 빙의일람</title> <title><?= UniqueConst::$serverName ?>: 빙의일람</title>
+34 -34
View File
@@ -2,30 +2,36 @@
namespace sammo; namespace sammo;
use sammo\Enums\GeneralAccessLogColumn;
include "lib.php"; include "lib.php";
include "func.php"; include "func.php";
$db = DB::db(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
$clock = GameClock::fromStorage($gameStor);
increaseRefresh("갱신정보", 1); increaseRefresh("갱신정보", 2);
$admin = $gameStor->getValues(['year', 'month', 'refresh', 'maxrefresh', 'maxonline', 'recentTraffic']); $admin = $gameStor->getValues(['year', 'month', 'refresh', 'maxrefresh', 'maxonline']);
$recentTraffic = $admin['recentTraffic'] ?? []; $log = getRawFileLogRecent(__DIR__ . '/logs/' . UniqueConst::$serverID . '/_traffic.txt', 11, 100);
$date = [];
$year = [];
$month = [];
$refresh = [];
$online = [];
$curonline = getOnlineNum(); $curonline = getOnlineNum();
foreach ($log as $i => $value) {
$recentTraffic[] = [ $parse = Json::decode($value);
'year' => $admin['year'], $date[$i] = $parse[0];
'month' => $admin['month'], $year[$i] = $parse[1];
'refresh' => $admin['refresh'], $month[$i] = $parse[2];
'online' => $curonline, $refresh[$i] = $parse[3];
'date' => $clock->formatNow() $online[$i] = $parse[4];
]; }
$year[] = $admin['year'];
$month[] = $admin['month'];
$date[] = TimeUtil::now();
if ($admin['maxrefresh'] == 0) { if ($admin['maxrefresh'] == 0) {
$admin['maxrefresh'] = 1; $admin['maxrefresh'] = 1;
@@ -46,7 +52,6 @@ if ($admin['maxonline'] < $curonline) {
<head> <head>
<title><?= UniqueConst::$serverName ?>: 트래픽정보</title> <title><?= UniqueConst::$serverName ?>: 트래픽정보</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<?= WebUtil::printCSS('../d_shared/common.css') ?> <?= WebUtil::printCSS('../d_shared/common.css') ?>
@@ -99,13 +104,13 @@ if ($admin['maxonline'] < $curonline) {
</td> </td>
</tr> </tr>
<?php <?php
foreach ($recentTraffic as $trafficItem) { $refresh[] = $admin['refresh'];
$value = $trafficItem['refresh']; foreach ($refresh as $i => $value) {
$w = round($value / $admin['maxrefresh'] * 100, 1); $w = round($value / $admin['maxrefresh'] * 100, 1);
$color = getTrafficColor($w); $color = getTrafficColor($w);
$dt = substr($trafficItem['date'], 11, 5); ?> $dt = substr($date[$i], 11, 5); ?>
<tr height=30> <tr height=30>
<td width=100 align=center><?= $trafficItem['year'] ?>년 <?= $trafficItem['month'] ?>월</td> <td width=100 align=center><?= $year[$i] ?>년 <?= $month[$i] ?>월</td>
<td width=60 align=center class='bg2'><?= $dt ?></td> <td width=60 align=center class='bg2'><?= $dt ?></td>
<td width=2 align=center class='bg1'></td> <td width=2 align=center class='bg1'></td>
<td width=320> <td width=320>
@@ -140,13 +145,13 @@ if ($admin['maxonline'] < $curonline) {
</td> </td>
</tr> </tr>
<?php <?php
foreach ($recentTraffic as $trafficItem) { $online[] = $curonline;
$value = $trafficItem['online']; foreach ($online as $i => $value) {
$w = round($value / $admin['maxonline'] * 100, 1); $w = round($value / $admin['maxonline'] * 100, 1);
$color = getTrafficColor($w); $color = getTrafficColor($w);
$dt = substr($trafficItem['date'], 11, 5); ?> $dt = substr($date[$i], 11, 5); ?>
<tr height=30> <tr height=30>
<td width=100 align=center><?= $trafficItem['year'] ?>년 <?= $trafficItem['month'] ?>월</td> <td width=100 align=center><?= $year[$i] ?>년 <?= $month[$i] ?>월</td>
<td width=60 align=center class='bg2'><?= $dt ?></td> <td width=60 align=center class='bg2'><?= $dt ?></td>
<td width=2 align=center class='bg1'></td> <td width=2 align=center class='bg1'></td>
<td width=320> <td width=320>
@@ -181,24 +186,19 @@ if ($admin['maxonline'] < $curonline) {
</td> </td>
</tr> </tr>
<?php <?php
$totalRefresh = [ $max_refresh = $db->queryFirstRow('SELECT sum(refresh) as refresh, sum(`connect`) as `connect` from general');
...$db->queryFirstRow('SELECT sum(`refresh`) as `refresh`, sum(`refresh_score_total`) as `refresh_score_total` from general_access_log'), $max_refresh['name'] = '접속자 총합';
'name' => '접속자 총합'
];
$top5Refresh = $db->query( $refresh_result = array_merge([$max_refresh], $db->query('SELECT `name`,refresh,`connect` FROM general ORDER BY refresh DESC LIMIT 5'));
'SELECT `name`, `log`.`refresh`, `refresh_score_total` FROM `general_access_log` AS `log`
INNER JOIN `general` ON `log`.`general_id` = `general`.`no` ORDER BY `log`.`refresh` DESC LIMIT 5'
);
foreach (array_merge([$totalRefresh], $top5Refresh) as $i => $user) { foreach ($refresh_result as $i => $user) {
$w = round($user['refresh'] / max(1, $totalRefresh['refresh']) * 100, 1); $w = round($user['refresh'] / max(1, $max_refresh['refresh']) * 100, 1);
$w2 = round(100 - $w, 1); $w2 = round(100 - $w, 1);
$color = getTrafficColor($w); $color = getTrafficColor($w);
?> ?>
<tr> <tr>
<td width=98 align=center><?= $user['name'] ?></td> <td width=98 align=center><?= $user['name'] ?></td>
<td width=98 align=center><?= $user['refresh_score_total'] ?>(<?= $user['refresh'] ?>)</td> <td width=98 align=center><?= $user['connect'] ?>(<?= $user['refresh'] ?>)</td>
<td width=798> <td width=798>
<?php if ($w == 0) : ?> <?php if ($w == 0) : ?>
<?php elseif ($w < 10) : ?> <?php elseif ($w < 10) : ?>
+11 -17
View File
@@ -2,8 +2,6 @@
namespace sammo; namespace sammo;
use sammo\Enums\GeneralLiteQueryMode;
use sammo\Enums\GeneralQueryMode;
use sammo\Enums\RankColumn; use sammo\Enums\RankColumn;
include "lib.php"; include "lib.php";
@@ -23,15 +21,12 @@ $generalID = $session->generalID;
$me = $db->queryFirstRow( $me = $db->queryFirstRow('SELECT no,tournament,con,turntime from general where owner=%i', $userID);
'SELECT no,tournament,refresh_score,turntime FROM `general`
LEFT JOIN general_access_log AS l ON `general`.no = l.general_id where owner=%i', $userID
);
$admin = $gameStor->getValues(['tournament', 'phase', 'tnmt_type', 'turnterm', 'develcost']); $admin = $gameStor->getValues(['tournament', 'phase', 'tnmt_type', 'turnterm', 'develcost']);
$turnTerm = $admin['turnterm']; $turnTerm = $admin['turnterm'];
$limitState = checkLimit($me['refresh_score']); $con = checkLimit($me['con']);
if ($limitState >= 2) { if ($con >= 2) {
printLimitMsg($me['turntime']); printLimitMsg($me['turntime']);
exit(); exit();
} }
@@ -112,13 +107,12 @@ if ($str3) {
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<?php if ($limitState == 1) { <?php if ($con == 1) {
MessageBox("접속제한이 얼마 남지 않았습니다!"); MessageBox("접속제한이 얼마 남지 않았습니다!");
} ?> } ?>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 베팅장</title> <title><?= UniqueConst::$serverName ?>: 베팅장</title>
@@ -507,28 +501,28 @@ if ($str3) {
$tournamentType = [ $tournamentType = [
'전 력 전' => [ '전 력 전' => [
'종합', '종합',
function (GeneralLite $general) { function (General $general) {
return $general->getVar('leadership') + $general->getVar('strength') + $general->getVar('intel'); return $general->getVar('leadership') + $general->getVar('strength') + $general->getVar('intel');
}, },
'tt', 'tt',
], ],
'통 솔 전' => [ '통 솔 전' => [
'통솔', '통솔',
function (GeneralLite $general) { function (General $general) {
return $general->getVar('leadership'); return $general->getVar('leadership');
}, },
'tl', 'tl',
], ],
'일 기 토' => [ '일 기 토' => [
'무력', '무력',
function (GeneralLite $general) { function (General $general) {
return $general->getVar('strength'); return $general->getVar('strength');
}, },
'ts', 'ts',
], ],
'설 전' => [ '설 전' => [
'지력', '지력',
function (GeneralLite $general) { function (General $general) {
return $general->getVar('intel'); return $general->getVar('intel');
}, },
'ti', 'ti',
@@ -565,12 +559,12 @@ if ($str3) {
$winColumn = RankColumn::from("{$rankColumn}w"); $winColumn = RankColumn::from("{$rankColumn}w");
$drawColumn = RankColumn::from("{$rankColumn}d"); $drawColumn = RankColumn::from("{$rankColumn}d");
$loseColumn = RankColumn::from("{$rankColumn}l"); $loseColumn = RankColumn::from("{$rankColumn}l");
$tournamentRankerList = GeneralLite::createObjListFromDB( $tournamentRankerList = General::createGeneralObjListFromDB(
$db->queryFirstColumn('SELECT general_id FROM rank_data WHERE `type`= %s ORDER BY value DESC LIMIT 40', $gameColumn->value), $db->queryFirstColumn('SELECT general_id FROM rank_data WHERE `type`= %s ORDER BY value DESC LIMIT 40', $gameColumn->value),
[$prizeColumn->value, $gameColumn->value, $winColumn->value, $drawColumn->value, $loseColumn->value, 'leadership', 'strength', 'intel', 'no', 'npc', 'name'], [$prizeColumn->value, $gameColumn->value, $winColumn->value, $drawColumn->value, $loseColumn->value, 'leadership', 'strength', 'intel', 'no', 'npc', 'name'],
GeneralLiteQueryMode::Core 0
); );
usort($tournamentRankerList, function (GeneralLite $lhs, GeneralLite $rhs) use ($gameColumn, $winColumn, $drawColumn, $loseColumn) { usort($tournamentRankerList, function (General $lhs, General $rhs) use ($gameColumn, $winColumn, $drawColumn, $loseColumn) {
$result = - ($lhs->getRankVar($gameColumn) <=> $rhs->getRankVar($gameColumn)); $result = - ($lhs->getRankVar($gameColumn) <=> $rhs->getRankVar($gameColumn));
if ($result !== 0) return $result; if ($result !== 0) return $result;
$result = - ( $result = - (
+7 -7
View File
@@ -16,7 +16,7 @@ $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
increaseRefresh("현재도시", 1); increaseRefresh("현재도시", 1);
$lastExecute = substr(GameClock::fromStorage($gameStor)->formatTick(Util::toInt($gameStor->turntime)), 5, 14); $lastExecute = substr($gameStor->turntime, 5, 14);
$me = $db->queryFirstRow('SELECT no,nation,officer_level,city from general where owner=%i', $userID); $me = $db->queryFirstRow('SELECT no,nation,officer_level,city from general where owner=%i', $userID);
$myNation = $db->queryFirstRow('SELECT nation,level,spy FROM nation WHERE nation=%i', $me['nation']) ?? [ $myNation = $db->queryFirstRow('SELECT nation,level,spy FROM nation WHERE nation=%i', $me['nation']) ?? [
@@ -34,7 +34,6 @@ $templates = new \League\Plates\Engine('templates');
<head> <head>
<title><?= UniqueConst::$serverName ?>: 도시정보</title> <title><?= UniqueConst::$serverName ?>: 도시정보</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<?= WebUtil::printJS('../d_shared/common_path.js') ?> <?= WebUtil::printJS('../d_shared/common_path.js') ?>
@@ -177,7 +176,7 @@ $templates = new \League\Plates\Engine('templates');
$valid = 1; $valid = 1;
} }
if ($userGrade >= 6) { if ($userGrade >= 5) {
$valid = true; $valid = true;
$showDetailedInfo = true; $showDetailedInfo = true;
} }
@@ -211,7 +210,7 @@ $templates = new \League\Plates\Engine('templates');
$city['trade'] = "- "; $city['trade'] = "- ";
} }
$dbColumns = General::mergeQueryColumn(['npc', 'defence_train', 'no', 'picture', 'imgsvr', 'name', 'injury', 'leadership', 'strength', 'intel', 'officer_level', 'nation', 'crewtype', 'crew', 'train', 'atmos'])[0]; $dbColumns = General::mergeQueryColumn(['npc', 'defence_train', 'no', 'picture', 'imgsvr', 'name', 'injury', 'leadership', 'strength', 'intel', 'officer_level', 'nation', 'crewtype', 'crew', 'train', 'atmos'], 2)[0];
if ($showDetailedInfo) { if ($showDetailedInfo) {
$generals = $db->query( $generals = $db->query(
'SELECT %l from general where city=%i order by turntime', 'SELECT %l from general where city=%i order by turntime',
@@ -275,7 +274,7 @@ $templates = new \League\Plates\Engine('templates');
$ourGeneral = false; $ourGeneral = false;
} }
if ($userGrade == 7) { if ($userGrade == 6) {
$ourGeneral = true; $ourGeneral = true;
} }
@@ -327,7 +326,8 @@ $templates = new \League\Plates\Engine('templates');
if ($ourGeneral && !$isNPC) { if ($ourGeneral && !$isNPC) {
$turnText = []; $turnText = [];
foreach ($generalTurnList[$general['no']] as $turnRawIdx => $turn) { $generalObj = new General($general, null, null, null, null, null, false);
foreach ($generalTurnList[$generalObj->getID()] as $turnRawIdx => $turn) {
$turnIdx = $turnRawIdx + 1; $turnIdx = $turnRawIdx + 1;
$turnText[] = "{$turnIdx} : $turn"; $turnText[] = "{$turnIdx} : $turn";
} }
@@ -537,4 +537,4 @@ $templates = new \League\Plates\Engine('templates');
</table> </table>
</body> </body>
</html> </html>
+5 -11
View File
@@ -19,15 +19,12 @@ $gameStor = KVStorage::getStorage($db, 'game_env');
increaseRefresh("암행부", 1); increaseRefresh("암행부", 1);
$me = $db->queryFirstRow( $me = $db->queryFirstRow('SELECT no,nation,officer_level,con,turntime,belong,permission,penalty from general where owner=%i', $userID);
'SELECT no,nation,officer_level,refresh_score,turntime,belong,permission,penalty FROM `general`
LEFT JOIN general_access_log AS l ON `general`.no = l.general_id WHERE owner=%i', $userID
);
$nationLevel = $db->queryFirstField('SELECT level FROM nation WHERE nation=%i', $me['nation']); $nationLevel = $db->queryFirstField('SELECT level FROM nation WHERE nation=%i', $me['nation']);
$limitState = checkLimit($me['refresh_score']); $con = checkLimit($me['con']);
if ($limitState >= 2) { if ($con >= 2) {
printLimitMsg($me['turntime']); printLimitMsg($me['turntime']);
exit(); exit();
} }
@@ -48,13 +45,12 @@ $templates = new \League\Plates\Engine('templates');
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<?php if ($limitState == 1) { <?php if ($con == 1) {
MessageBox("접속제한이 얼마 남지 않았습니다!"); MessageBox("접속제한이 얼마 남지 않았습니다!");
} ?> } ?>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 암행부</title> <title><?= UniqueConst::$serverName ?>: 암행부</title>
@@ -129,7 +125,6 @@ $templates = new \League\Plates\Engine('templates');
); );
$generalTurnList = []; $generalTurnList = [];
$clock = GameClock::fromStorage($gameStor);
foreach ($db->queryAllLists( foreach ($db->queryAllLists(
'SELECT general_id, turn_idx, brief FROM general_turn WHERE general_id IN %li AND turn_idx < 5 ORDER BY general_id ASC, turn_idx ASC', 'SELECT general_id, turn_idx, brief FROM general_turn WHERE general_id IN %li AND turn_idx < 5 ORDER BY general_id ASC, turn_idx ASC',
@@ -143,7 +138,6 @@ $templates = new \League\Plates\Engine('templates');
$genCntEff = 0; $genCntEff = 0;
foreach ($generals as &$general) { foreach ($generals as &$general) {
$general['turntime'] = $clock->formatTick(Util::toInt($general['turntime']), true);
$general['cityText'] = CityConst::byID($general['city'])->name; $general['cityText'] = CityConst::byID($general['city'])->name;
$general['troopText'] = $troopName[$general['troop']] ?? '-'; $general['troopText'] = $troopName[$general['troop']] ?? '-';
@@ -286,4 +280,4 @@ $templates = new \League\Plates\Engine('templates');
</table> </table>
</body> </body>
</html> </html>
-1
View File
@@ -42,7 +42,6 @@ if ($meLevel >= 5) {
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 인사부</title> <title><?= UniqueConst::$serverName ?>: 인사부</title>
-1
View File
@@ -33,7 +33,6 @@ $sel = [$type => "selected"];
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 세력도시</title> <title><?= UniqueConst::$serverName ?>: 세력도시</title>
+4 -7
View File
@@ -42,7 +42,6 @@ if ($gameStor->isunited) {
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 세력장수</title> <title><?= UniqueConst::$serverName ?>: 세력장수</title>
@@ -97,7 +96,7 @@ if ($gameStor->isunited) {
7 => ['gold', true], 7 => ['gold', true],
8 => ['rice', true], 8 => ['rice', true],
9 => ['crew', true], 9 => ['crew', true],
10 => ['refresh_score_total', true], 10 => ['connect', true],
11 => ['personal', true], 11 => ['personal', true],
12 => ['special', true], 12 => ['special', true],
13 => ['special2', true], 13 => ['special2', true],
@@ -105,9 +104,7 @@ if ($gameStor->isunited) {
15 => ['npc', true], 15 => ['npc', true],
][$type]; ][$type];
$generalList = $db->query( $generalList = $db->query('SELECT owner,no,picture,imgsvr,npc,age,nation,special,special2,personal,name,injury,leadership,strength,intel,experience,dedication,officer_level,killturn,connect,gold,rice,crew,belong from general where nation = %i order by %b %l', $me['nation'], $orderKey, $orderDesc ? 'desc' : '');
'SELECT owner,no,picture,imgsvr,npc,age,nation,special,special2,personal,name,injury,leadership,strength,intel,experience,dedication,officer_level,killturn,refresh_score_total,gold,rice,crew,belong
FROM `general` LEFT JOIN `general_access_log` ON general.no = general_access_log.general_id where nation = %i order by %b %l', $me['nation'], $orderKey, $orderDesc ? 'desc' : '');
echo " echo "
<table align=center class='tb_layout bg0'> <table align=center class='tb_layout bg0'>
@@ -175,8 +172,8 @@ if ($gameStor->isunited) {
<td align=center>" . displayCharInfo($general['personal']) . "</td> <td align=center>" . displayCharInfo($general['personal']) . "</td>
<td align=center>" . displaySpecialDomesticInfo($general['special']) . " / " . displaySpecialWarInfo($general['special2']) . "</td> <td align=center>" . displaySpecialDomesticInfo($general['special']) . " / " . displaySpecialWarInfo($general['special2']) . "</td>
<td align=center>{$general['belong']}</td> <td align=center>{$general['belong']}</td>
<td align=center>". $general['refresh_score_total'] ?? 0; <td align=center>{$general['connect']}";
echo "<br>(" . getRefreshScoreText($general['refresh_score_total'] ?? 0) . ")</td> echo "<br>(" . getConnect($general['connect']) . ")</td>
</tr>"; </tr>";
} }
echo " echo "
-1
View File
@@ -25,7 +25,6 @@ if ($nationID == 0) {
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 세력정보</title> <title><?= UniqueConst::$serverName ?>: 세력정보</title>
+22 -75
View File
@@ -2,10 +2,6 @@
namespace sammo; namespace sammo;
use sammo\Enums\GeneralAccessLogColumn;
use sammo\Enums\GeneralQueryMode;
use sammo\Enums\TableName;
include "lib.php"; include "lib.php";
include "func.php"; include "func.php";
@@ -19,12 +15,12 @@ $userID = Session::getUserID();
$generalID = $session->generalID; $generalID = $session->generalID;
$db = DB::db(); $db = DB::db();
$gameStor = KVStorage::getStorage($db, 'game_env'); $gameStor = KVStorage::getStorage($db, 'game_env');
$gameStor->cacheValues(['turntime', 'opentime', 'autorun_user', 'npcmode']); $gameStor->cacheValues(['turntime', 'opentime', 'autorun_user', 'npcmode']);
$clock = GameClock::fromStorage($gameStor);
$nowTick = $clock->nowTick();
$me = General::createObjFromDB($generalID, null, GeneralQueryMode::FullWithAccessLog); increaseRefresh("내정보", 1);
$me = General::createGeneralObjFromDB($generalID);
$myset = $me->getVar('myset'); $myset = $me->getVar('myset');
if ($myset > 0) { if ($myset > 0) {
@@ -33,62 +29,29 @@ if ($myset > 0) {
$submit = 'hidden'; $submit = 'hidden';
} }
$lastRefresh = $db->queryFirstField( $targetTime = addTurn($me->getVar('lastrefresh'), $gameStor->turnterm, 2);
'SELECT %b FROM %b WHERE %b = %i', if ($gameStor->turntime <= $gameStor->opentime) {
GeneralAccessLogColumn::lastRefresh->value, //서버 가오픈시 할 수 있는 행동
TableName::generalAccessLog->value, if ($me->getNPCType() == 0 && $me->getNationID() == 0) {
GeneralAccessLogColumn::generalID->value,
$generalID
);
$nextChange = $me->getAuxVar('next_change');
if (!is_int($nextChange)) {
$nextChange = null;
}
$nextChangeDisplay = $nextChange === null ? null : $clock->formatTick($nextChange);
increaseRefresh("내정보", 1);
if ($gameStor->turntime <= $gameStor->opentime) {
$targetTime = $me->getAuxVar('prestart_delete_after');
if (!is_int($targetTime)) {
$targetTime = addTurn(
$lastRefresh === null ? $nowTick : Util::toInt($lastRefresh),
$gameStor->turnterm,
GameConst::$minTurnDieOnPrestart
);
$me->setAuxVar('prestart_delete_after', $targetTime);
$me->applyDB($db);
}
$targetTimeDisplay = $clock->formatTick($targetTime);
//서버 가오픈시 할 수 있는 행동
if ($me->getNPCType() == 0 && $me->getNationID() == 0) {
$showDieOnPrestartBtn = true; $showDieOnPrestartBtn = true;
if ($targetTime <= $nowTick) { if ($targetTime <= TimeUtil::now()) {
$availableDieOnPrestart = true; $availableDieOnPrestart = true;
} }
} }
if ($me->getNationID() == 0) { if($me->getNationID() == 0){
$showBuildNationCandidateBtn = true; $showBuildNationCandidateBtn = true;
} }
} }
$use_treatment = $me->getAuxVar('use_treatment') ?? 10; $use_treatment = $me->getAuxVar('use_treatment') ?? 10;
$use_auto_nation_turn = $me->getAuxVar('use_auto_nation_turn') ?? 1; $use_auto_nation_turn = $me->getAuxVar('use_auto_nation_turn') ?? 1;
$changeDefence999Train = -3;
$changeDefence999Atmos = -6;
$changeDefence999Train = $me->onCalcDomestic('changeDefenceTrain', "train999", $changeDefence999Train);
$changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $changeDefence999Atmos);
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=500" /> <meta name="viewport" content="width=500" />
<title><?= UniqueConst::$serverName ?>: 내정보</title> <title><?= UniqueConst::$serverName ?>: 내정보</title>
@@ -117,13 +80,13 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
<div class="col">내 정 보<br><?= backButton() ?></div> <div class="col">내 정 보<br><?= backButton() ?></div>
</div> </div>
<div class="row gx-0"> <div class="row gx-0">
<div class="col col-12 col-lg-6"> <div class="col col-12 col-md-6">
<div class="row"> <div class="row">
<div class="col"><?php generalInfo($me); ?><?php generalInfo2($me); ?></div> <div class="col"><?php generalInfo($me); ?><?php generalInfo2($me); ?></div>
</div> </div>
</div> </div>
<div class="col col-12 col-lg-6"> <div class="col col-12 col-md-6">
<div class="row mx-0 gx-0"> <div class="row mx-0 gx-0">
<div class="col" style='padding-left:2ch;'> <div class="col" style='padding-left:2ch;'>
토너먼트 【 토너먼트 【
@@ -153,22 +116,11 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
<?php foreach ([90, 80, 60, 40] as $targetDefenceTrain) : ?> <?php foreach ([90, 80, 60, 40] as $targetDefenceTrain) : ?>
<option value='<?= $targetDefenceTrain ?>' <?= $me->getVar('defence_train') == $targetDefenceTrain ? "selected" : ""; ?>><?= formatDefenceTrain($targetDefenceTrain) ?>(훈사<?= $targetDefenceTrain ?>)</option> <option value='<?= $targetDefenceTrain ?>' <?= $me->getVar('defence_train') == $targetDefenceTrain ? "selected" : ""; ?>><?= formatDefenceTrain($targetDefenceTrain) ?>(훈사<?= $targetDefenceTrain ?>)</option>
<?php endforeach; ?> <?php endforeach; ?>
<option value=999 <?= $me->getVar('defence_train') == 999 ? "selected" : ""; ?>><?= formatDefenceTrain(999) ?> <option value=999 <?= $me->getVar('defence_train') == 999 ? "selected" : ""; ?>><?= formatDefenceTrain(999) ?>[훈련, 사기 -3]</option>
<?php if ($changeDefence999Train != 0 || $changeDefence999Train != 0) :
?>[<?= $changeDefence999Train != 0 ? "훈련 {$changeDefence999Train}," : '' ?><?= $changeDefence999Atmos != 0 ? "사기 {$changeDefence999Atmos}" : ''
?>]<?php endif ?></option>
</select> </select>
】<br><br> 】<br><br>
<input type=<?= $submit ?> id='set_my_setting' name=btn style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px; value=설정저장><br> <input type=<?= $submit ?> id='set_my_setting' name=btn style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px; value=설정저장><br>
∞<span style='color:orange'>설정저장은 이달중 <?= $myset ?>회 남았습니다.</span><br><br> ∞<span style='color:orange'>설정저장은 이달중 <?= $myset ?>회 남았습니다.</span><br><br>
<?php if ($me->getPenaltyList()->count()) : ?>
징계 목록(저장 시 갱신)<br>
<?php foreach ($me->getPenaltyList() as $penaltyKey => $penalty) : ?>
<span style='color:red'><?= $penaltyKey->getHelptext() ?> : <?= $penalty ?></span><br>
<?php endforeach; ?>
<br />
<?php endif; ?>
<?php if (!($gameStor->autorun_user['limit_minutes'] ?? false)) : ?> <?php if (!($gameStor->autorun_user['limit_minutes'] ?? false)) : ?>
휴 가 신 청<br> 휴 가 신 청<br>
<button type="button" id='vacation' style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>휴가 신청</button><br><br> <button type="button" id='vacation' style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>휴가 신청</button><br><br>
@@ -177,7 +129,7 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
<a href="b_myPage.php?detachNPC=1"><button type="button" style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>빙의 해체 요청</button></a>--> <a href="b_myPage.php?detachNPC=1"><button type="button" style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>빙의 해체 요청</button></a>-->
<?php if ($showDieOnPrestartBtn) : ?> <?php if ($showDieOnPrestartBtn) : ?>
가오픈 기간 내 장수 삭제 (<?= $targetTimeDisplay ?> 부터)<br> 가오픈 기간 내 장수 삭제 (<?= substr($targetTime, 0, 19) ?> 부터)<br>
<button type="button" id='dieOnPrestart' style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>장수 삭제</button><br><br> <button type="button" id='dieOnPrestart' style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>장수 삭제</button><br><br>
<?php endif; ?> <?php endif; ?>
@@ -186,13 +138,8 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
<button type="button" id='buildNationCandidate' style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>사전 거병</button><br><br> <button type="button" id='buildNationCandidate' style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>사전 거병</button><br><br>
<?php endif; ?> <?php endif; ?>
<?php if (GameConst::$availableInstantAction['instantRetreat'] ?? false) : ?>
거리 3칸 이내 아국 도시로 즉시 이동<br>
<button type="button" id='instantRetreat' style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>접경 귀환</button><br><br>
<?php endif; ?>
<?php if ($gameStor->npcmode == 2 && $me->getNPCType() == 0) : ?> <?php if ($gameStor->npcmode == 2 && $me->getNPCType() == 0) : ?>
다른 장수 선택 (<?= $nextChangeDisplay ?? '지금' ?>부터)<br> 다른 장수 선택 (<?= substr($me->getAuxVar('next_change') ?? TimeUtil::now(), 0, 19) ?> 부터)<br>
<a href="select_general_from_pool.php" id='select_general_from_pool'><button type="button" style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>다른 장수 선택</button></a><br><br> <a href="select_general_from_pool.php" id='select_general_from_pool'><button type="button" style=background-color:<?= GameConst::$basecolor2 ?>;color:white;width:160px;height:30px;font-size:14px;>다른 장수 선택</button></a><br><br>
<?php endif; ?> <?php endif; ?>
@@ -219,7 +166,7 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
<div class="row mx-1"> <div class="row mx-1">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<?php foreach ($me->getItems() as $itemKey => $item) : ?> <?php foreach ($me->getItems() as $itemKey => $item) : ?>
<button type="button" data-item-type='<?= $itemKey ?>' class="drop-item-btn btn btn-primary <?= $item->getName() == '-' ? 'disabled' : '' ?>"><?= $item->getName() ?></button> <button type="button" data-item-type='<?=$itemKey?>' class="drop-item-btn btn btn-primary <?= $item->getName() == '-' ? 'disabled' : '' ?>"><?= $item->getName() ?></button>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
</div> </div>
@@ -230,7 +177,7 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
</div> </div>
</div> </div>
</div> </div>
<div class="col col-12 col-lg-6"> <div class="col col-12 col-md-6">
<div class="row gx-0"> <div class="row gx-0">
<div class="col bg1 text-center"> <div class="col bg1 text-center">
<h4 style='color:skyblue'>개인 기록</h4> <h4 style='color:skyblue'>개인 기록</h4>
@@ -243,7 +190,7 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
<button type="button" class="load_old_log btn btn-secondary" data-log_type="generalAction">이전 로그 불러오기</button> <button type="button" class="load_old_log btn btn-secondary" data-log_type="generalAction">이전 로그 불러오기</button>
</div> </div>
</div> </div>
<div class="col col-12 col-lg-6"> <div class="col col-12 col-md-6">
<div class="row gx-0"> <div class="row gx-0">
<div class="col bg1 text-center"> <div class="col bg1 text-center">
<h4 style='color:orange'>전투 기록</h4> <h4 style='color:orange'>전투 기록</h4>
@@ -257,7 +204,7 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
</div> </div>
</div> </div>
<div class="col col-12 col-lg-6"> <div class="col col-12 col-md-6">
<div class="row gx-0"> <div class="row gx-0">
<div class="col bg1 text-center"> <div class="col bg1 text-center">
<h4 style='color:skyblue'>장수 열전</h4> <h4 style='color:skyblue'>장수 열전</h4>
@@ -267,7 +214,7 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
<?= formatHistoryToHTML(getGeneralHistoryLogAll($generalID)) ?> <?= formatHistoryToHTML(getGeneralHistoryLogAll($generalID)) ?>
</div> </div>
</div> </div>
<div class="col col-12 col-lg-6"> <div class="col col-12 col-md-6">
<div class="row gx-0"> <div class="row gx-0">
<div class="col bg1 text-center"> <div class="col bg1 text-center">
<h4 style='color:orange'>전투 결과</h4> <h4 style='color:orange'>전투 결과</h4>
@@ -290,4 +237,4 @@ $changeDefence999Atmos = $me->onCalcDomestic('changeDefenceTrain', "atmos999", $
</div> </div>
</body> </body>
</html> </html>
+63 -10
View File
@@ -14,16 +14,14 @@ $gameStor = KVStorage::getStorage($db, 'game_env');
increaseRefresh("토너먼트", 1); increaseRefresh("토너먼트", 1);
TurnExecutionHelper::executeAllCommand(); TurnExecutionHelper::executeAllCommand();
$me = $db->queryFirstRow( $me = $db->queryFirstRow('select no,tournament,con,turntime from general where owner=%i', $userID);
'SELECT no,tournament,refresh_score,turntime from `general` $generalID = $session->generalID;
LEFT JOIN general_access_log AS l ON `general`.no = l.general_id where owner=%i', $userID
);
$generalID = $me['no'];
$admin = $gameStor->getValues(['tournament', 'phase', 'turnterm', 'tnmt_msg', 'tnmt_type', 'develcost', 'tnmt_trig']); $admin = $gameStor->getValues(['tournament', 'phase', 'turnterm', 'tnmt_msg', 'tnmt_type', 'develcost', 'tnmt_trig']);
$turnTerm = $admin['turnterm']; $turnTerm = $admin['turnterm'];
$limitState = checkLimit($me['refresh_score']); $con = checkLimit($me['con']);
if ($limitState >= 2) { if ($con >= 2) {
printLimitMsg($me['turntime']); printLimitMsg($me['turntime']);
exit(); exit();
} }
@@ -102,13 +100,12 @@ $globalBetTotal = array_sum($globalBet);
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<?php if ($limitState == 1) { <?php if ($con == 1) {
MessageBox("접속제한이 얼마 남지 않았습니다! 제한량이 모자라다면 참여를 해보세요^^"); MessageBox("접속제한이 얼마 남지 않았습니다! 제한량이 모자라다면 참여를 해보세요^^");
} ?> } ?>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1024" /> <meta name="viewport" content="width=1024" />
<title><?= UniqueConst::$serverName ?>: 토너먼트</title> <title><?= UniqueConst::$serverName ?>: 토너먼트</title>
@@ -125,7 +122,63 @@ $globalBetTotal = array_sum($globalBet);
</tr> </tr>
</table> </table>
<table align=center class='tb_layout bg0'> <table align=center class='tb_layout bg0'>
<?php if ($me['no'] > 0 && $me['tournament'] == 0 && $admin['tournament'] == 1) : ?> <?php if ($session->userGrade >= 5) : ?>
<form method=post action=c_tournament.php>
<tr>
<td colspan=8><input type=textarea size=150 style=color:white;background-color:black; name=msg><input type=submit name=btn value='메시지'></td>
</tr>
<tr>
<td colspan=8>
<button type="button" class="btn btn-secondary" onclick='location.reload()'>갱신</button>
<?php if ($admin['tournament'] == 0) : ?>
<select name=auto size=1 style=color:white;background-color:black;>
<option style=color:white; value=0 <?= !$admin['tnmt_trig'] ? 'selected' : '' ?>>수동진행</option>
<option style=color:white; value=1 <?= $admin['tnmt_trig'] ? 'selected' : '' ?>>자동진행</option>
</select>
<input type=submit name=btn value='자동개최설정'>
<?php else : ?>
<input type=submit name=btn value='중단' onclick='return confirm("진짜 중단하시겠습니까?")'>
<?php endif; ?>
<?php switch ($admin['tournament']) {
case 1:
echo "<input type=submit name=btn value='랜덤투입'>";
echo "<input type=submit name=btn value='랜덤전부투입'>";
break;
case 2:
echo "<input type=submit name=btn value='예선'><input type=submit name=btn value='예선전부'>";
break;
case 3:
echo "<input type=submit name=btn value='추첨'><input type=submit name=btn value='추첨전부'>";
break;
case 4:
echo "<input type=submit name=btn value='본선'><input type=submit name=btn value='본선전부'>";
break;
case 5:
echo "<input type=submit name=btn value='배정'>";
break;
case 6:
echo "<input type=submit name=btn value='베팅마감'>";
break;
case 7:
echo "<input type=submit name=btn value='16강'>";
break;
case 8:
echo "<input type=submit name=btn value='8강'>";
break;
case 9:
echo "<input type=submit name=btn value='4강'>";
break;
case 10:
echo "<input type=submit name=btn value='결승'>";
break;
} ?>
</td>
</tr>
</form>
<?php elseif ($me['no'] > 0 && $me['tournament'] == 0 && $admin['tournament'] == 1) : ?>
<form method=post action=c_tournament.php> <form method=post action=c_tournament.php>
<tr> <tr>
<td colspan=8><button type="button" class="btn btn-secondary" onclick='location.reload()'>갱신</button><button type="submit" class="btn btn-sammo-base2" onclick='return confirm("참가비 금<?= $admin['develcost'] ?>이 필요합니다. 참가하시겠습니까?")'>참가</button><input type='hidden' name='btn' value='참가'/></td> <td colspan=8><button type="button" class="btn btn-secondary" onclick='location.reload()'>갱신</button><button type="submit" class="btn btn-sammo-base2" onclick='return confirm("참가비 금<?= $admin['develcost'] ?>이 필요합니다. 참가하시겠습니까?")'>참가</button><input type='hidden' name='btn' value='참가'/></td>
+3 -31
View File
@@ -43,7 +43,6 @@ if ($nationID) {
<head> <head>
<title><?= UniqueConst::$serverName ?>: 전투 시뮬레이터</title> <title><?= UniqueConst::$serverName ?>: 전투 시뮬레이터</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=1100" /> <meta name="viewport" content="width=1100" />
<?= WebUtil::printCSS('../d_shared/common.css') ?> <?= WebUtil::printCSS('../d_shared/common.css') ?>
@@ -66,8 +65,8 @@ if ($nationID) {
전역 설정 전역 설정
</div> </div>
<div class="card-body dragpad_battle"> <div class="card-body dragpad_battle">
<div class="row gx-0"> <div class="row">
<div class="col-sm-4"> <div class="col-sm-6">
<div class="input-group"> <div class="input-group">
<input type="number" class="form-control" aria-describedby="text_year" value="<?= $startYear ?>" disabled> <input type="number" class="form-control" aria-describedby="text_year" value="<?= $startYear ?>" disabled>
<div class="input-group-text"> 년 시작 </div> <div class="input-group-text"> 년 시작 </div>
@@ -77,13 +76,9 @@ if ($nationID) {
<div class="input-group-text"> 월 </div> <div class="input-group-text"> 월 </div>
</div> </div>
</div> </div>
<div class="col-sm-5 px-2"> <div class="col-sm-6">
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<div class="input-group me-2" role="group"> <div class="input-group me-2" role="group">
<div class="input-group-text">
시드
</div>
<input type="text" class="form-control" id="seed" aria-describedby="text_seed">
<div class="input-group-text"> <div class="input-group-text">
반복 횟수 반복 횟수
</div> </div>
@@ -92,10 +87,6 @@ if ($nationID) {
<option value="1000">1000회 (요약 표기)</option> <option value="1000">1000회 (요약 표기)</option>
</select> </select>
</div> </div>
</div>
</div>
<div class="col-sm-3">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group me-2" role="group"> <div class="btn-group me-2" role="group">
<button type="button" class="btn btn-danger btn-begin_battle">전투</button> <button type="button" class="btn btn-danger btn-begin_battle">전투</button>
</div> </div>
@@ -435,25 +426,6 @@ if ($nationID) {
</div> </div>
<input type="number" class="form-control form_killcrew" value="0" step="1"> <input type="number" class="form-control form_killcrew" value="0" step="1">
</div> </div>
<div class="input-group mb-0">
<div class="input-group-text">자신</div>
<div class="input-group-text">회피 확률</div>
<input type="number" class="form-control form_war_avoid_ratio" value="0" step="1" min="0" max="5">
<div class="input-group-text">필살 확률</div>
<input type="number" class="form-control form_war_critical_ratio" value="0" step="1" min="0" max="5">
<div class="input-group-text">계략 시도 확률</div>
<input type="number" class="form-control form_war_magic_trial_prob" value="0" step="1" min="0" max="5">
</div>
<div class="input-group mb-1">
<div class="input-group-text">상대</div>
<div class="input-group-text">회피 확률</div>
<input type="number" class="form-control form_oppose_war_avoid_ratio" value="0" step="1" min="0" max="5">
<div class="input-group-text">필살 확률</div>
<input type="number" class="form-control form_oppose_war_critical_ratio" value="0" step="1" min="0" max="5">
<div class="input-group-text">계략 시도 확률</div>
<input type="number" class="form-control form_oppose_war_magic_trial_prob" value="0" step="1" min="0" max="5">
</div>
</div> </div>
</div> </div>
+3 -3
View File
@@ -228,10 +228,10 @@ if($btn == "자동개최설정") {
$phase = 0; $phase = 0;
startBetting($admin['tnmt_type'], 720); startBetting($admin['tnmt_type'], 720);
} elseif($btn == "베팅마감") { } elseif($btn == "베팅마감") {
$clock = GameClock::fromStorage($gameStor); $dt = date("Y-m-d H:i:s", time() + 60);
$gameStor->tournament=7; $gameStor->tournament=7;
$gameStor->phase=0; $gameStor->phase=0;
$gameStor->tnmt_time = $clock->nowTick() + $clock->ticksFromSeconds(60); $gameStor->tnmt_time = $dt;
} elseif($btn == "16강") { } elseif($btn == "16강") {
finalFight($admin['tnmt_type'], $admin['tournament'], $admin['phase'], 16); finalFight($admin['tnmt_type'], $admin['tournament'], $admin['phase'], 16);
} elseif($btn == "8강") { } elseif($btn == "8강") {
@@ -248,4 +248,4 @@ if($btn == "자동개최설정") {
$gameStor->tnmt_msg = $msg; $gameStor->tnmt_msg = $msg;
} }
header('location:b_tournament.php'); header('location:b_tournament.php');
-102
View File
@@ -1,102 +0,0 @@
<?php
declare(strict_types=1);
namespace sammo;
use sammo\Enums\RankColumn;
/**
* Build the existing battle simulator request from live, pre-battle objects.
*
* This file is loaded only by the deterministic CLI comparison path. It must
* remain read-only: callers emit the returned value without changing the live
* objects or consuming RNG.
*/
function comparisonBuildBattleGeneralFixture(General $general): array
{
$raw = $general->getRaw(true);
$inheritBuff = $general->getAuxVar('inheritBuff');
$fixture = [
'no' => $general->getID(),
'name' => $general->getName(),
'nation' => $general->getNationID(),
'city' => $general->getCityID(),
'turntime' => $general->getTurnTime(General::TURNTIME_FULL_MS),
'turntick' => $general->getTurnTick(),
'personal' => $raw['personal'] ?? null,
'special' => $raw['special'] ?? null,
'special2' => $raw['special2'] ?? null,
'crew' => $raw['crew'],
'crewtype' => $raw['crewtype'],
'atmos' => $raw['atmos'],
'train' => $raw['train'],
'intel' => $raw['intel'],
'intel_exp' => $raw['intel_exp'],
'book' => $raw['book'] ?? 'None',
'strength' => $raw['strength'],
'strength_exp' => $raw['strength_exp'],
'weapon' => $raw['weapon'] ?? 'None',
'injury' => $raw['injury'],
'leadership' => $raw['leadership'],
'leadership_exp' => $raw['leadership_exp'],
'horse' => $raw['horse'] ?? 'None',
'item' => $raw['item'] ?? 'None',
'explevel' => $raw['explevel'],
'experience' => $raw['experience'],
'dedication' => $raw['dedication'],
'officer_level' => $raw['officer_level'],
'officer_city' => $raw['officer_city'],
'gold' => $raw['gold'],
'rice' => $raw['rice'],
'dex1' => $raw['dex1'],
'dex2' => $raw['dex2'],
'dex3' => $raw['dex3'],
'dex4' => $raw['dex4'],
'dex5' => $raw['dex5'],
'defence_train' => $raw['defence_train'],
'recent_war' => $raw['recent_war'] ?? null,
'warnum' => $general->getRankVar(RankColumn::warnum, 0),
'killnum' => $general->getRankVar(RankColumn::killnum, 0),
'killcrew' => $general->getRankVar(RankColumn::killcrew, 0),
];
if ($inheritBuff !== null) {
$fixture['inheritBuff'] = $inheritBuff;
}
return $fixture;
}
function comparisonBuildBattleFixture(
string $seed,
int $year,
int $month,
int $startYear,
General $attacker,
array $attackerNation,
array $defenders,
array $defenderCity,
array $defenderNation,
): array {
return [
'action' => 'battle',
'seed' => $seed,
'repeatCnt' => 1,
'year' => $year,
'month' => $month,
'startYear' => $startYear,
'scenarioEffect' => GameConst::$scenarioEffect,
'attackerGeneral' => comparisonBuildBattleGeneralFixture($attacker),
'attackerCity' => $attacker->getRawCity(),
'attackerNation' => $attackerNation,
// General::getGeneralList() can retain IDs as array keys. JSON would
// then encode the collection as an object, while both simulators use
// an ordered list. Keep the legacy iteration order but discard keys.
'defenderGenerals' => array_values(array_map(
static fn(General $general): array => comparisonBuildBattleGeneralFixture($general),
$defenders,
)),
'defenderCity' => $defenderCity,
'defenderNation' => $defenderNation,
];
}
-32
View File
@@ -1,32 +0,0 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli' || getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1') {
http_response_code(404);
exit(1);
}
chdir(dirname(__DIR__));
require_once 'lib.php';
$rows = DB::db()->query(
'SELECT general_id, log_type, year, month, text
FROM general_record
WHERE log_type IN ("battle_brief", "battle")
ORDER BY id'
);
echo json_encode([
'schemaVersion' => 1,
'engine' => 'ref',
'logs' => array_map(static fn(array $row): array => [
'generalId' => (int)$row['general_id'],
'category' => $row['log_type'] === 'battle_brief' ? 'BATTLE_BRIEF' : 'BATTLE_DETAIL',
'year' => (int)$row['year'],
'month' => (int)$row['month'],
'text' => (string)$row['text'],
], $rows),
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), PHP_EOL;
-314
View File
@@ -1,314 +0,0 @@
<?php
declare(strict_types=1);
namespace sammo;
use Ds\Map;
use sammo\Enums\RankColumn;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';
final class ComparisonTracingRNG implements RNG
{
private int $sequence = 0;
/** @var array<int, array<string, mixed>> */
public array $calls = [];
public function __construct(private readonly RNG $inner)
{
}
public static function getMaxInt(): int
{
return LiteHashDRBG::getMaxInt();
}
public function nextBytes(int $bytes): string
{
$value = $this->inner->nextBytes($bytes);
$this->record('nextBytes', ['bytes' => $bytes], bin2hex($value));
return $value;
}
public function nextBits(int $bits): string
{
$value = $this->inner->nextBits($bits);
$this->record('nextBits', ['bits' => $bits], bin2hex($value));
return $value;
}
public function nextInt(?int $max = null): int
{
$value = $this->inner->nextInt($max);
$this->record('nextInt', ['maxInclusive' => $max], $value);
return $value;
}
public function nextFloat1(): float
{
$value = $this->inner->nextFloat1();
$this->record('nextFloat1', [], $value);
return $value;
}
private function record(string $operation, array $arguments, mixed $result): void
{
$this->calls[] = [
'seq' => $this->sequence++,
'operation' => $operation,
'arguments' => $arguments,
'result' => $result,
];
}
}
final class ComparisonTracingRandUtil extends RandUtil
{
/** @var array<int, array{rngSeq: int, probability: int|float, result: bool}> */
public array $boolCalls = [];
public function __construct(private readonly ComparisonTracingRNG $tracingRng)
{
parent::__construct($tracingRng);
}
public function nextBool(int|float $prob = 0.5): bool
{
$rngSeq = count($this->tracingRng->calls);
$result = parent::nextBool($prob);
$this->boolCalls[] = [
'rngSeq' => $rngSeq,
'probability' => $prob,
'result' => $result,
];
return $result;
}
}
final class ComparisonGeneral extends General
{
public function getTurnTime(int $short = self::TURNTIME_FULL_MS): ?string
{
$formatted = (string)($this->getRaw()['turntime'] ?? '1970-01-01 00:00:00');
return match ($short) {
self::TURNTIME_FULL_MS => $formatted,
self::TURNTIME_FULL => substr($formatted, 0, 19),
self::TURNTIME_HMS => substr($formatted, 11, 8),
self::TURNTIME_HM => substr($formatted, 11, 5),
};
}
public function getTurnTick(): ?int
{
return (int)($this->getRaw()['turntick'] ?? 0);
}
}
function comparisonExtractRankVar(array $raw): Map
{
$rankVars = new Map();
foreach ($raw as $rawKey => $rawValue) {
$key = RankColumn::tryFrom($rawKey);
if ($key !== null) {
$rankVars[$key] = $rawValue;
}
}
return $rankVars;
}
function comparisonBuildGeneral(
array $raw,
array $city,
array $nation,
int $year,
int $month,
bool $defender = false,
): General {
$aux = [];
if (array_key_exists('inheritBuff', $raw)) {
$aux['inheritBuff'] = $raw['inheritBuff'];
}
$raw['aux'] = Json::encode($aux);
$raw['owner'] = 0;
return new ComparisonGeneral(
$raw,
comparisonExtractRankVar($raw),
null,
$city,
$nation,
$year,
$month,
$defender,
);
}
function comparisonRunBattle(array $fixture): array
{
$seed = (string)($fixture['seed'] ?? 'battle-differential');
$year = (int)$fixture['year'];
$month = (int)$fixture['month'];
$startYear = (int)($fixture['startYear'] ?? 180);
$rawAttacker = $fixture['attackerGeneral'];
$rawAttackerCity = $fixture['attackerCity'];
$rawAttackerNation = $fixture['attackerNation'];
$rawDefenderCity = $fixture['defenderCity'];
$rawDefenderNation = $fixture['defenderNation'];
// 비교 fixture에 명시된 경우에만 ref runtime의 scenario action chain을
// 활성화합니다. 일반 비교 fixture의 기존 동작은 그대로 유지합니다.
$scenarioEffect = $fixture['scenarioEffect'] ?? null;
if ($scenarioEffect === '' || $scenarioEffect === 'None') {
$scenarioEffect = null;
}
if (
$scenarioEffect !== null
&& (!is_string($scenarioEffect) || getScenarioEffectClass($scenarioEffect) === null)
) {
throw new \InvalidArgumentException('scenarioEffect is invalid');
}
GameConst::$scenarioEffect = $scenarioEffect;
$tracingRng = new ComparisonTracingRNG(new LiteHashDRBG($seed));
$warRng = new ComparisonTracingRandUtil($tracingRng);
$attacker = new WarUnitGeneral(
$warRng,
comparisonBuildGeneral($rawAttacker, $rawAttackerCity, $rawAttackerNation, $year, $month),
$rawAttackerNation,
true,
);
$city = new WarUnitCity($warRng, $rawDefenderCity, $rawDefenderNation, $year, $month, $startYear);
$defenderList = [];
foreach ($fixture['defenderGenerals'] as $rawDefender) {
$defenderList[] = new WarUnitGeneral(
$warRng,
comparisonBuildGeneral($rawDefender, $rawDefenderCity, $rawDefenderNation, $year, $month, true),
$rawDefenderNation,
false,
);
}
if (count($defenderList) && extractBattleOrder($city, $attacker) > 0) {
$defenderList[] = $city;
}
$summarizeDefenderOrder = static fn(WarUnit $unit): array => [
'id' => $unit instanceof WarUnitGeneral ? $unit->getGeneral()->getID() : 0,
'order' => extractBattleOrder($unit, $attacker),
];
$defenderOrderBeforeSort = array_map($summarizeDefenderOrder, $defenderList);
usort(
$defenderList,
fn(WarUnit $lhs, WarUnit $rhs): int =>
-(extractBattleOrder($lhs, $attacker) <=> extractBattleOrder($rhs, $attacker)),
);
$defenderOrderAfterSort = array_map($summarizeDefenderOrder, $defenderList);
$iterDefender = new \ArrayIterator($defenderList);
$iterDefender->rewind();
$finishedDefenders = [];
$getNextDefender = function (?WarUnit $previous, bool $requestNext) use (
$iterDefender,
$attacker,
&$finishedDefenders,
): ?WarUnit {
if ($previous !== null) {
$finishedDefenders[] = buildWarTraceUnitSnapshot($previous);
}
if (!$requestNext || !$iterDefender->valid()) {
return null;
}
$next = $iterDefender->current();
if (extractBattleOrder($next, $attacker) <= 0) {
return null;
}
$iterDefender->next();
return $next;
};
$events = [];
$conquered = processWar_NG(
$seed,
$attacker,
$getNextDefender,
$city,
static function (array $event) use (&$events): void {
$events[] = $event;
},
);
$attackerLogs = $attacker->getLogger()->rollback();
$defenderLogs = [];
foreach ($defenderList as $defenderUnit) {
if (!($defenderUnit instanceof WarUnitGeneral)) {
continue;
}
$snapshot = buildWarTraceUnitSnapshot($defenderUnit);
$defenderLogs[(string)$snapshot['id']] = $defenderUnit->getLogger()->rollback();
}
$cityLogs = $city->getLogger()->rollback();
return [
'engine' => 'ref',
'seed' => $seed,
'conquered' => $conquered,
'attacker' => buildWarTraceUnitSnapshot($attacker),
'city' => buildWarTraceUnitSnapshot($city),
'finishedDefenders' => $finishedDefenders,
'defenderOrder' => [
'before' => $defenderOrderBeforeSort,
'after' => $defenderOrderAfterSort,
],
'events' => $events,
'rng' => $tracingRng->calls,
'boolRng' => $warRng->boolCalls,
'logs' => [
'attacker' => $attackerLogs,
'defenders' => $defenderLogs,
'city' => $cityLogs,
],
];
}
$fixturePath = $argv[1] ?? null;
if ($fixturePath === '--jsonl') {
try {
while (($line = fgets(STDIN)) !== false) {
$line = trim($line);
if ($line === '') {
continue;
}
$fixture = json_decode($line, true, flags: JSON_THROW_ON_ERROR);
echo json_encode(
comparisonRunBattle($fixture),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
}
exit(0);
} catch (\Throwable $throwable) {
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
exit(1);
}
}
if ($fixturePath === null || ($fixturePath !== '-' && !is_file($fixturePath))) {
fwrite(STDERR, "usage: php compare/battle_trace.php <fixture.json|-|--jsonl>\n");
exit(2);
}
try {
$fixtureJson = $fixturePath === '-' ? stream_get_contents(STDIN) : file_get_contents($fixturePath);
$fixture = json_decode((string)$fixtureJson, true, flags: JSON_THROW_ON_ERROR);
echo json_encode(
comparisonRunBattle($fixture),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
} catch (\Throwable $throwable) {
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
exit(1);
}
@@ -1,23 +0,0 @@
{
"action": "AddGlobalBetray",
"args": [2, 1],
"setup": {
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2, 3],
"general": [
{ "id": 1, "values": { "name": "배신0", "betray": 0 } },
{ "id": 2, "values": { "name": "배신1", "betray": 1 } },
{ "id": 3, "values": { "name": "배신2", "betray": 2 } }
]
},
"environment": {
"year": 194,
"month": 1,
"startyear": 190
},
"observe": {
"generalIds": [1, 2, 3]
},
"compactOutput": true
}
@@ -1,79 +0,0 @@
{
"action": "AssignGeneralSpeciality",
"setup": {
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2, 3],
"general": [
{
"id": 1,
"values": {
"name": "내정대상",
"nation": 1,
"age": 30,
"leadership": 40,
"strength": 45,
"intel": 80,
"special": "None",
"specage": 30,
"special2": "che_신산",
"specage2": 99,
"aux": {
"prev_types_special": ["che_경작"]
}
}
},
{
"id": 2,
"values": {
"name": "전투대상",
"nation": 1,
"age": 30,
"leadership": 80,
"strength": 75,
"intel": 40,
"special": "che_인덕",
"specage": 99,
"special2": "None",
"specage2": 30,
"dex1": 200,
"dex2": 10,
"dex3": 10,
"dex4": 10,
"dex5": 10,
"aux": {
"prev_types_special2": ["che_돌격"]
}
}
},
{
"id": 3,
"values": {
"name": "계승대상",
"nation": 2,
"age": 30,
"leadership": 50,
"strength": 50,
"intel": 50,
"special": "che_경작",
"specage": 99,
"special2": "None",
"specage2": 30,
"aux": {
"inheritSpecificSpecialWar": "che_의술",
"marker": 3
}
}
}
]
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190
},
"observe": {
"generalIds": [1, 2, 3]
},
"compactOutput": true
}
@@ -1,54 +0,0 @@
{
"action": "AutoDeleteInvader",
"compactOutput": true,
"args": [1],
"setup": {
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"resetDiplomacy": true,
"nation": [
{
"id": 1,
"values": {
"name": "ⓞ남만족",
"capital": 69,
"gennum": 1,
"tech": 100,
"level": 2,
"type": "che_병가"
}
}
],
"general": [
{
"id": 1,
"values": {
"name": "ⓞ남만대왕",
"nation": 1,
"city": 69,
"officer_level": 12,
"npc": 9
}
}
],
"event": [
{
"id": 990001,
"action": [["AutoDeleteInvader", 1]]
}
]
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"currentEventID": 990001
},
"observe": {
"generalIds": [1]
}
}
@@ -1,24 +0,0 @@
{
"action": "BlockScoutAction",
"args": [true],
"setup": {
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2],
"nation": [
{ "id": 1, "values": { "name": "위", "level": 2, "scout": 0 } },
{ "id": 2, "values": { "name": "촉", "level": 2, "scout": 0 } }
],
"gameEnvironment": {
"block_change_scout": false
}
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190
},
"observe": {
"nationIds": [1, 2]
}
}
@@ -1,226 +0,0 @@
{
"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]
}
}
@@ -1,34 +0,0 @@
{
"action": "NewYear",
"environment": {
"year": 191,
"month": 1,
"startyear": 190
},
"setup": {
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"general": [
{
"id": 1,
"values": {
"nation": 1,
"age": 30,
"belong": 3
}
},
{
"id": 2,
"values": {
"nation": 0,
"age": 30,
"belong": 4
}
}
]
},
"observe": {
"generalIds": [1, 2]
}
}
@@ -1,11 +0,0 @@
{
"action": "NoticeToHistoryLog",
"args": ["<S>새해 알림</>", 6],
"environment": {
"year": 191,
"month": 1,
"startyear": 190
},
"setup": {},
"observe": {}
}
@@ -1,75 +0,0 @@
{
"action": "ProcessIncome",
"args": ["gold"],
"environment": {
"year": 190,
"month": 7,
"startyear": 190
},
"setup": {
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"general": [
{
"id": 1,
"values": {
"nation": 1,
"city": 1,
"officer_level": 5,
"npc": 0,
"gold": 1000,
"rice": 1000,
"dedication": 100
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "갑국",
"capital": 1,
"gold": 10000,
"rice": 20000,
"level": 1,
"rate_tmp": 20
}
}
],
"city": [
{
"id": 1,
"values": {
"name": "갑성",
"nation": 1,
"level": 4,
"supply": 1,
"pop": 10000,
"pop_max": 20000,
"agri": 1000,
"agri_max": 2000,
"comm": 1000,
"comm_max": 2000,
"secu": 1000,
"secu_max": 2000,
"trust": 80,
"def": 500,
"def_max": 1000,
"wall": 500,
"wall_max": 1000
}
}
]
},
"observe": {
"generalIds": [1],
"nationIds": [1],
"cityIds": [1],
"nationEnvironmentKeys": ["prev_income_gold", "prev_income_rice"]
}
}
@@ -1,33 +0,0 @@
{
"action": "ResetOfficerLock",
"environment": {
"year": 191,
"month": 1,
"startyear": 190
},
"setup": {
"resetCities": true,
"resetNations": true,
"city": [
{
"id": 1,
"values": {
"nation": 1,
"officer_set": 1
}
}
],
"nation": [
{
"id": 1,
"values": {
"chief_set": 1
}
}
]
},
"observe": {
"cityIds": [1],
"nationIds": [1]
}
}
@@ -1,9 +0,0 @@
{
"action": "CreateAdminNPC",
"environment": {
"year": 193,
"month": 12,
"startyear": 190
},
"observe": {}
}
@@ -1,15 +0,0 @@
{
"action": "CreateManyNPC",
"args": [2, 0],
"environment": {
"year": 193,
"month": 5,
"startyear": 190,
"turnterm": 10,
"turntime": "0193-05-01 00:00:00"
},
"setup": {
"resetGenerals": true
},
"observe": {}
}
@@ -1,69 +0,0 @@
{
"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<br>장수 수: 1<br>도시 수: 1",
"isHtml": true,
"aux": { "nation": 1 }
},
{
"title": "촉",
"info": "국력: 300<br>장수 수: 1<br>도시 수: 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]
}
}
@@ -1,40 +0,0 @@
{
"action": "InvaderEnding",
"compactOutput": true,
"setup": {
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"setAllCitiesNation": 1,
"nation": [
{
"id": 1,
"values": {
"name": "기준국",
"capital": 1,
"gennum": 1,
"tech": 100,
"level": 2,
"type": "che_유가"
}
}
],
"gameEnvironment": {
"isunited": 1,
"refreshLimit": 3
},
"event": [
{
"id": 990002,
"action": [["InvaderEnding"]]
}
]
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"currentEventID": 990002
},
"observe": {}
}
@@ -1,108 +0,0 @@
{
"action": "InvaderLifecycle",
"args": [10, 150, 100, 20],
"setup": {
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"resetDiplomacy": true,
"resetEvents": true,
"city": [
{
"id": 1,
"values": {
"name": "기준도시",
"level": 3,
"nation": 1,
"pop": 1000,
"pop_max": 10000,
"agri": 2000,
"agri_max": 20000,
"comm": 3000,
"comm_max": 30000,
"secu": 4000,
"secu_max": 40000,
"def": 5000,
"def_max": 50000,
"wall": 6000,
"wall_max": 60000
}
},
{
"id": 48,
"values": {
"name": "남만",
"level": 4,
"nation": 0,
"pop": 1000,
"pop_max": 10000,
"agri": 2000,
"agri_max": 20000,
"comm": 3000,
"comm_max": 30000,
"secu": 4000,
"secu_max": 40000,
"def": 5000,
"def_max": 50000,
"wall": 6000,
"wall_max": 60000
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "기준국",
"capital": 1,
"gennum": 1,
"tech": 100,
"level": 2,
"type": "che_유가"
}
}
],
"general": [
{
"id": 1,
"values": {
"name": "군주",
"nation": 1,
"city": 1,
"officer_level": 12,
"npc": 0,
"leadership": 50,
"strength": 50,
"intel": 50,
"experience": 1000,
"dedication": 1000,
"dex1": 10,
"dex2": 20,
"dex3": 30,
"dex4": 40,
"dex5": 50,
"turntime": "0200-01-01 00:05:00"
}
}
],
"gameEnvironment": {
"isunited": 0,
"refreshLimit": 3
},
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {}
}
@@ -1,42 +0,0 @@
{
"action": "LostUniqueItem",
"args": [0.2],
"environment": {
"year": 210,
"month": 1,
"startyear": 180
},
"setup": {
"syncEnvironment": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"general": [
{
"id": 1,
"values": {
"name": "갑",
"npc": 0,
"horse": "che_명마_12_옥란백용구",
"weapon": "che_무기_15_의천검",
"book": "che_서적_01_효경전",
"item": "che_저지_삼황내문"
}
},
{
"id": 2,
"values": {
"name": "을",
"npc": 1,
"horse": "che_명마_12_사륜거",
"weapon": "che_무기_01_단도",
"book": "None",
"item": "che_부적_태현청생부"
}
}
]
},
"observe": {
"generalIds": [1, 2]
}
}
@@ -1,74 +0,0 @@
{
"action": "MergeInheritPointRank",
"environment": {
"year": 210,
"month": 1,
"startyear": 180
},
"setup": {
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"clearInheritanceOwnerIds": [100],
"gameEnvironment": {
"isunited": 0
},
"general": [
{
"id": 1,
"values": {
"name": "갑",
"nation": 1,
"owner": 100,
"npc": 0,
"belong": 3,
"dex1": 1000,
"dex2": 0,
"dex3": 0,
"dex4": 0,
"dex5": 0
}
}
],
"inheritancePoints": [
{"ownerId": 100, "key": "previous", "value": 999},
{"ownerId": 100, "key": "lived_month", "value": 2},
{"ownerId": 100, "key": "max_domestic_critical", "value": 4},
{"ownerId": 100, "key": "active_action", "value": 15},
{"ownerId": 100, "key": "unifier", "value": 6},
{"ownerId": 100, "key": "tournament", "value": 7}
],
"rankData": [
{"generalId": 1, "type": "warnum", "value": 2},
{"generalId": 1, "type": "firenum", "value": 1},
{"generalId": 1, "type": "betwin", "value": 2},
{"generalId": 1, "type": "betgold", "value": 4000},
{"generalId": 1, "type": "betwingold", "value": 2000},
{"generalId": 1, "type": "inherit_earned_dyn", "value": 999},
{"generalId": 1, "type": "inherit_earned_act", "value": 11},
{"generalId": 1, "type": "inherit_earned", "value": 999},
{"generalId": 1, "type": "inherit_spent_dyn", "value": 9},
{"generalId": 1, "type": "inherit_spent", "value": 999}
]
},
"observe": {
"generalIds": [1],
"ownerIds": [100],
"inheritanceKeys": [
"previous",
"lived_month",
"max_domestic_critical",
"active_action",
"unifier",
"tournament"
],
"rankGeneralIds": [1],
"rankTypes": [
"inherit_earned",
"inherit_earned_act",
"inherit_earned_dyn",
"inherit_spent",
"inherit_spent_dyn"
]
}
}
@@ -1,75 +0,0 @@
{
"action": "NationBettingLifecycle",
"args": [1, 100],
"bettorGeneralIds": [1, 2],
"bettorOwnerIds": [1, 2],
"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,
"deleteOtherCities": true,
"keepCityIds": [1, 2],
"city": [
{ "id": 1, "values": { "name": "허창", "nation": 1, "level": 3 } },
{ "id": 2, "values": { "name": "성도", "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 } }
],
"inheritancePrevious": {
"1": 1000,
"2": 1000
},
"rankData": [
{ "generalId": 1, "type": "inherit_spent_dyn", "value": 0 },
{ "generalId": 1, "type": "inherit_earned_act", "value": 0 },
{ "generalId": 2, "type": "inherit_spent_dyn", "value": 0 },
{ "generalId": 2, "type": "inherit_earned_act", "value": 0 }
],
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190
},
"observe": {}
}
@@ -1,77 +0,0 @@
{
"action": "NpcNationLifecycle",
"setup": {
"resetCities": true,
"resetGenerals": true,
"resetNations": true,
"city": [
{
"id": 1,
"values": {
"name": "점유도시",
"level": 5,
"nation": 1,
"pop": 10001,
"pop_max": 50001,
"agri": 1001,
"agri_max": 5001,
"comm": 2001,
"comm_max": 6001,
"secu": 3001,
"secu_max": 7001,
"def": 4001,
"def_max": 8001,
"wall": 5001,
"wall_max": 9001
}
},
{
"id": 48,
"values": {
"name": "건국도시",
"level": 5,
"nation": 0,
"pop": 10048,
"pop_max": 50048,
"agri": 1048,
"agri_max": 5048,
"comm": 2048,
"comm_max": 6048,
"secu": 3048,
"secu_max": 7048,
"def": 4048,
"def_max": 8048,
"wall": 5048,
"wall_max": 9048
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "기준국",
"capital": 1,
"gennum": 0,
"tech": 120,
"level": 2,
"type": "che_유가"
}
}
],
"gameEnvironment": {
"lastNPCTroopLeaderID": 40
},
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {}
}
@@ -1,65 +0,0 @@
{
"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]
}
}
@@ -1,178 +0,0 @@
{
"action": "PostUpdateMonthly",
"environment": {
"year": 193,
"month": 2,
"startyear": 193,
"seedYear": 193,
"seedMonth": 1
},
"setup": {
"world": {
"hiddenSeed": "monthly-post-diplomacy-fixture"
},
"syncEnvironment": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2, 3, 4],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2, 3, 4],
"general": [
{
"id": 1,
"values": {
"name": "갑장",
"nation": 1,
"npc": 2,
"crew": 0,
"gold": 0,
"rice": 0,
"leadership": 10,
"strength": 10,
"intel": 10,
"experience": 0,
"dedication": 0
}
},
{
"id": 2,
"values": {
"name": "을장",
"nation": 2,
"npc": 2,
"crew": 0,
"gold": 0,
"rice": 0,
"leadership": 10,
"strength": 10,
"intel": 10,
"experience": 0,
"dedication": 0
}
},
{
"id": 3,
"values": {
"name": "병장",
"nation": 3,
"npc": 2,
"crew": 0,
"gold": 0,
"rice": 0,
"leadership": 10,
"strength": 10,
"intel": 10,
"experience": 0,
"dedication": 0
}
},
{
"id": 4,
"values": {
"name": "정장",
"nation": 4,
"npc": 2,
"crew": 0,
"gold": 0,
"rice": 0,
"leadership": 10,
"strength": 10,
"intel": 10,
"experience": 0,
"dedication": 0
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "갑국",
"gennum": 2,
"level": 1
}
},
{
"id": 2,
"values": {
"name": "을국",
"gennum": 1,
"level": 1
}
},
{
"id": 3,
"values": {
"name": "병국",
"gennum": 1,
"level": 1
}
},
{
"id": 4,
"values": {
"name": "정국",
"gennum": 1,
"level": 1
}
}
],
"resetDiplomacy": true,
"diplomacy": [
{
"fromNationId": 1,
"toNationId": 2,
"state": 1,
"term": 1,
"dead": 777
},
{
"fromNationId": 2,
"toNationId": 1,
"state": 1,
"term": 1,
"dead": 888
},
{
"fromNationId": 1,
"toNationId": 3,
"state": 0,
"term": 5,
"dead": 250
},
{
"fromNationId": 3,
"toNationId": 1,
"state": 0,
"term": 5,
"dead": 50
},
{
"fromNationId": 3,
"toNationId": 4,
"state": 0,
"term": 1,
"dead": 0
},
{
"fromNationId": 4,
"toNationId": 3,
"state": 0,
"term": 1,
"dead": 0
},
{
"fromNationId": 2,
"toNationId": 4,
"state": 7,
"term": 1,
"dead": 999
}
]
},
"observe": {
"generalIds": [1, 2, 3, 4],
"nationIds": [1, 2, 3, 4]
}
}
@@ -1,162 +0,0 @@
{
"action": "PostUpdateMonthly",
"environment": {
"year": 193,
"month": 2,
"startyear": 193,
"seedYear": 193,
"seedMonth": 1
},
"setup": {
"world": {
"hiddenSeed": "monthly-post-nation-stats-fixture"
},
"syncEnvironment": true,
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2],
"resetDiplomacy": true,
"general": [
{
"id": 1,
"values": {
"nation": 1,
"npc": 0,
"gold": 1000,
"rice": 2000,
"crew": 100,
"leadership": 50,
"strength": 40,
"intel": 30,
"experience": 100,
"dedication": 200,
"dex1": 1000,
"dex2": 0,
"dex3": 0,
"dex4": 0,
"dex5": 0
}
},
{
"id": 2,
"values": {
"nation": 2,
"npc": 2,
"gold": 3000,
"rice": 4000,
"crew": 200,
"leadership": 60,
"strength": 50,
"intel": 40,
"experience": 300,
"dedication": 400,
"dex1": 2000,
"dex2": 1000,
"dex3": 0,
"dex4": 0,
"dex5": 0
}
}
],
"rankData": [
{"generalId": 1, "type": "killcrew_person", "value": 100},
{"generalId": 1, "type": "deathcrew_person", "value": 0},
{"generalId": 2, "type": "killcrew_person", "value": 0},
{"generalId": 2, "type": "deathcrew_person", "value": 100}
],
"nation": [
{
"id": 1,
"values": {
"name": "갑국",
"gold": 10000,
"rice": 20000,
"tech": 100,
"level": 2,
"gennum": 9,
"power": 7
}
},
{
"id": 2,
"values": {
"name": "을국",
"gold": 2000,
"rice": 3000,
"tech": 50,
"level": 1,
"gennum": 8,
"power": 8
}
}
],
"city": [
{
"id": 1,
"values": {
"name": "갑성",
"nation": 1,
"level": 4,
"supply": 1,
"pop": 1000,
"pop_max": 2000,
"agri": 100,
"agri_max": 200,
"comm": 100,
"comm_max": 200,
"secu": 100,
"secu_max": 200,
"def": 100,
"def_max": 200,
"wall": 100,
"wall_max": 200
}
},
{
"id": 2,
"values": {
"name": "을성",
"nation": 2,
"level": 4,
"supply": 1,
"pop": 2000,
"pop_max": 4000,
"agri": 200,
"agri_max": 400,
"comm": 200,
"comm_max": 400,
"secu": 200,
"secu_max": 400,
"def": 200,
"def_max": 400,
"wall": 200,
"wall_max": 400
}
}
],
"nationEnvironment": [
{
"nationId": 1,
"key": "max_power",
"value": {
"maxPower": 999,
"maxCrew": 50,
"maxCities": ["옛도시", "옛도시2"]
}
},
{"nationId": 1, "key": "available_war_setting_cnt", "value": 1}
]
},
"observe": {
"generalIds": [1, 2],
"nationIds": [1, 2],
"cityIds": [1, 2],
"rankGeneralIds": [1, 2],
"rankTypes": ["killcrew_person", "deathcrew_person"],
"nationEnvironmentKeys": ["max_power", "available_war_setting_cnt"]
}
}
@@ -1,128 +0,0 @@
{
"action": "PostUpdateMonthly",
"environment": {
"year": 193,
"month": 2,
"startyear": 193,
"seedYear": 193,
"seedMonth": 1
},
"setup": {
"world": {
"hiddenSeed": "monthly-post-tail-2"
},
"syncEnvironment": true,
"clearNeutralAuctions": true,
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2],
"resetDiplomacy": true,
"gameEnvironment": {
"tournament": 0,
"tnmt_trig": true,
"tnmt_pattern": [0, 1, 2, 3],
"prev_winner": null
},
"general": [
{
"id": 1,
"values": {
"name": "갑장",
"nation": 1,
"city": 1,
"officer_level": 12,
"npc": 0,
"gold": 5000,
"rice": 7000,
"crew": 100,
"leadership": 50,
"strength": 40,
"intel": 30,
"experience": 100,
"dedication": 200
}
},
{
"id": 2,
"values": {
"name": "을장",
"nation": 2,
"city": 2,
"officer_level": 12,
"npc": 0,
"gold": 6000,
"rice": 8000,
"crew": 200,
"leadership": 60,
"strength": 50,
"intel": 40,
"experience": 300,
"dedication": 400
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "갑국",
"capital": 1,
"gold": 10000,
"rice": 20000,
"tech": 100,
"level": 2,
"gennum": 1,
"power": 7
}
},
{
"id": 2,
"values": {
"name": "을국",
"capital": 2,
"gold": 2000,
"rice": 3000,
"tech": 50,
"level": 1,
"gennum": 1,
"power": 8
}
}
],
"city": [
{
"id": 1,
"values": {
"name": "갑성",
"nation": 1,
"level": 4,
"supply": 1,
"front": 3,
"pop": 1000,
"pop_max": 2000
}
},
{
"id": 2,
"values": {
"name": "을성",
"nation": 2,
"level": 4,
"supply": 1,
"front": 3,
"pop": 2000,
"pop_max": 4000
}
}
]
},
"observe": {
"generalIds": [1, 2],
"nationIds": [1, 2],
"cityIds": [1, 2]
}
}
@@ -1,143 +0,0 @@
{
"action": "PostUpdateMonthly",
"environment": {
"year": 195,
"month": 2,
"startyear": 193,
"seedYear": 195,
"seedMonth": 1
},
"setup": {
"world": {
"hiddenSeed": "monthly-post-wander-fixture"
},
"syncEnvironment": true,
"resetCities": true,
"city": [
{
"id": 1,
"values": {
"name": "방랑성",
"nation": 4,
"level": 4,
"front": 1
}
},
{
"id": 2,
"values": {
"name": "존속성",
"nation": 1,
"level": 4,
"front": 1
}
}
],
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2, 3],
"general": [
{
"id": 1,
"values": {
"name": "방랑주",
"nation": 4,
"city": 1,
"officer_level": 12,
"npc": 2,
"gold": 2000,
"rice": 3000,
"crew": 100,
"makelimit": 0,
"belong": 7,
"leadership": 50,
"strength": 40,
"intel": 30,
"experience": 100,
"dedication": 200
}
},
{
"id": 2,
"values": {
"name": "방랑객",
"nation": 4,
"city": 1,
"officer_level": 1,
"npc": 0,
"gold": 1500,
"rice": 4000,
"crew": 200,
"makelimit": 0,
"belong": 5,
"leadership": 60,
"strength": 50,
"intel": 40,
"experience": 300,
"dedication": 400
}
},
{
"id": 3,
"values": {
"name": "존속장",
"nation": 1,
"city": 2,
"officer_level": 12,
"npc": 2,
"gold": 500,
"rice": 500,
"crew": 50,
"leadership": 50,
"strength": 50,
"intel": 50,
"experience": 0,
"dedication": 0
}
}
],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 4],
"nation": [
{
"id": 1,
"values": {
"name": "존속국",
"gennum": 1,
"level": 1
}
},
{
"id": 4,
"values": {
"name": "방랑국",
"gennum": 2,
"level": 0
}
}
],
"resetDiplomacy": true,
"diplomacy": [
{
"fromNationId": 1,
"toNationId": 4,
"state": 2,
"term": 0,
"dead": 0
},
{
"fromNationId": 4,
"toNationId": 1,
"state": 2,
"term": 0,
"dead": 0
}
]
},
"observe": {
"generalIds": [1, 2, 3],
"nationIds": [1, 4],
"cityIds": [1, 2]
}
}
@@ -1,49 +0,0 @@
{
"action": "PreUpdateMonthly",
"environment": {
"year": 200,
"month": 12,
"startyear": 190
},
"setup": {
"syncEnvironment": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1, 2],
"general": [
{"id": 1, "values": {"makelimit": 2}},
{"id": 2, "values": {"makelimit": 0}}
],
"generalAccessLog": [
{"generalId": 1, "refreshScoreTotal": 101},
{"generalId": 2, "refreshScoreTotal": 1}
],
"nation": [
{
"id": 1,
"values": {
"rate": 35,
"rate_tmp": 10,
"strategic_cmd_limit": 2,
"surlimit": 1,
"spy": "{\"1\":1,\"2\":2}"
}
}
],
"city": [
{"id": 1, "values": {"state": 31, "term": 1, "conflict": {"1": 10}}},
{"id": 2, "values": {"state": 32, "term": 2, "conflict": {"1": 20}}},
{"id": 3, "values": {"state": 33, "term": 0, "conflict": {"1": 30}}},
{"id": 4, "values": {"state": 34, "term": 3, "conflict": {"1": 40}}},
{"id": 5, "values": {"state": 41, "term": 1, "conflict": {"1": 50}}},
{"id": 6, "values": {"state": 42, "term": 2, "conflict": {"1": 60}}},
{"id": 7, "values": {"state": 43, "term": 3, "conflict": {"1": 70}}}
]
},
"observe": {
"generalIds": [1, 2],
"nationIds": [1],
"cityIds": [1, 2, 3, 4, 5, 6, 7],
"accessGeneralIds": [1, 2]
}
}
@@ -1,76 +0,0 @@
{
"action": "ProcessIncome",
"args": ["gold"],
"environment": {
"year": 190,
"month": 7,
"startyear": 190
},
"setup": {
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"general": [
{
"id": 1,
"values": {
"nation": 1,
"city": 1,
"officer_level": 5,
"npc": 0,
"gold": 1000,
"rice": 1000,
"dedication": 100
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "갑국",
"capital": 1,
"gold": 10000,
"rice": 20000,
"level": 1,
"rate": 35,
"rate_tmp": 10
}
}
],
"city": [
{
"id": 1,
"values": {
"name": "갑성",
"nation": 1,
"level": 4,
"supply": 1,
"pop": 10000,
"pop_max": 20000,
"agri": 1000,
"agri_max": 2000,
"comm": 1000,
"comm_max": 2000,
"secu": 1000,
"secu_max": 2000,
"trust": 80,
"def": 500,
"def_max": 1000,
"wall": 500,
"wall_max": 1000
}
}
]
},
"observe": {
"generalIds": [1],
"nationIds": [1],
"cityIds": [1],
"nationEnvironmentKeys": ["prev_income_gold", "prev_income_rice"]
}
}
@@ -1,106 +0,0 @@
{
"action": "ProcessSemiAnnual",
"resource": "gold",
"environment": {
"year": 193,
"month": 1,
"startyear": 190
},
"setup": {
"resetCities": true,
"resetGenerals": true,
"nation": [
{
"id": 1,
"values": {
"name": "반기검증국",
"gold": 100001,
"rice": 7777,
"rate_tmp": 20,
"type": "che_도가"
}
}
],
"city": [
{
"id": 1,
"values": {
"nation": 1,
"supply": 1,
"pop": 10000,
"pop_max": 50000,
"agri": 1001,
"agri_max": 5000,
"comm": 1001,
"comm_max": 5000,
"secu": 1001,
"secu_max": 2000,
"trust": 55,
"def": 1001,
"def_max": 5000,
"wall": 1001,
"wall_max": 5000,
"dead": 123
}
},
{
"id": 2,
"values": {
"nation": 1,
"supply": 0,
"pop": 10000,
"pop_max": 50000,
"agri": 1001,
"agri_max": 5000,
"comm": 1001,
"comm_max": 5000,
"secu": 1001,
"secu_max": 2000,
"trust": 55,
"def": 1001,
"def_max": 5000,
"wall": 1001,
"wall_max": 5000,
"dead": 123
}
},
{
"id": 3,
"values": {
"nation": 0,
"supply": 1,
"pop": 10000,
"pop_max": 50000,
"agri": 1001,
"agri_max": 5000,
"comm": 1001,
"comm_max": 5000,
"secu": 1001,
"secu_max": 2000,
"trust": 55,
"def": 1001,
"def_max": 5000,
"wall": 1001,
"wall_max": 5000,
"dead": 123
}
}
],
"general": [
{
"id": 1,
"values": {
"nation": 1,
"city": 1,
"gold": 10001,
"rice": 8888
}
}
]
},
"observe": {
"generalIds": [1],
"cityIds": [1, 2, 3],
"nationIds": [1]
}
}
@@ -1,65 +0,0 @@
{
"action": "ProcessSemiAnnual",
"resource": "gold",
"environment": {
"year": 193,
"month": 1,
"startyear": 190
},
"setup": {
"resetCities": true,
"resetGenerals": true,
"nation": [
{
"id": 1,
"values": {
"name": "반기검증국",
"gold": 100001,
"rice": 7777,
"rate": 50,
"rate_tmp": 20,
"type": "che_중립"
}
}
],
"city": [
{
"id": 1,
"values": {
"nation": 1,
"supply": 1,
"pop": 10000,
"pop_max": 50000,
"agri": 1001,
"agri_max": 5000,
"comm": 1001,
"comm_max": 5000,
"secu": 1001,
"secu_max": 2000,
"trust": 55,
"def": 1001,
"def_max": 5000,
"wall": 1001,
"wall_max": 5000,
"dead": 123
}
}
],
"general": [
{
"id": 1,
"values": {
"nation": 1,
"city": 1,
"gold": 10001,
"rice": 8888
}
}
]
},
"observe": {
"generalIds": [1],
"cityIds": [1],
"nationIds": [1]
}
}
@@ -1,96 +0,0 @@
{
"action": "ProcessWarIncome",
"environment": {
"year": 193,
"month": 1,
"startyear": 190
},
"setup": {
"resetCities": true,
"nation": [
{
"id": 1,
"values": {
"name": "전쟁수입국",
"gold": 1000,
"level": 1,
"type": "che_중립"
}
},
{
"id": 2,
"values": {
"name": "비활성국",
"gold": 2000,
"level": 0,
"type": "che_중립"
}
},
{
"id": 3,
"values": {
"name": "법가국",
"gold": 3000,
"level": 1,
"type": "che_법가"
}
}
],
"city": [
{
"id": 1,
"values": {
"nation": 1,
"supply": 1,
"pop": 1000,
"pop_max": 2000,
"dead": 101
}
},
{
"id": 2,
"values": {
"nation": 1,
"supply": 0,
"pop": 1000,
"pop_max": 2000,
"dead": 999
}
},
{
"id": 3,
"values": {
"nation": 2,
"supply": 1,
"pop": 1000,
"pop_max": 2000,
"dead": 1000
}
},
{
"id": 4,
"values": {
"nation": 3,
"supply": 1,
"pop": 1000,
"pop_max": 2000,
"dead": 105
}
},
{
"id": 5,
"values": {
"nation": 0,
"supply": 1,
"pop": 999,
"pop_max": 1000,
"dead": 10
}
}
]
},
"observe": {
"cityIds": [1, 2, 3, 4, 5],
"nationIds": [1, 2, 3]
}
}
@@ -1,105 +0,0 @@
{
"action": "RaiseInvader",
"compactOutput": true,
"args": [10, 150, 100, 20],
"setup": {
"resetCities": true,
"resetGenerals": true,
"deleteOtherGenerals": true,
"keepGeneralIds": [1],
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1],
"city": [
{
"id": 1,
"values": {
"level": 3,
"nation": 1,
"pop": 1000,
"pop_max": 10000,
"agri": 2000,
"agri_max": 20000,
"comm": 3000,
"comm_max": 30000,
"secu": 4000,
"secu_max": 40000,
"def": 5000,
"def_max": 50000,
"wall": 6000,
"wall_max": 60000
}
},
{
"id": 48,
"values": {
"level": 4,
"nation": 0,
"pop": 1000,
"pop_max": 10000,
"agri": 2000,
"agri_max": 20000,
"comm": 3000,
"comm_max": 30000,
"secu": 4000,
"secu_max": 40000,
"def": 5000,
"def_max": 50000,
"wall": 6000,
"wall_max": 60000
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "기준국",
"capital": 1,
"gennum": 1,
"tech": 100,
"level": 2,
"type": "che_유가"
}
}
],
"general": [
{
"id": 1,
"values": {
"name": "군주",
"nation": 1,
"city": 1,
"officer_level": 12,
"npc": 0,
"leadership": 50,
"strength": 50,
"intel": 50,
"experience": 1000,
"dedication": 1000,
"dex1": 10,
"dex2": 20,
"dex3": 30,
"dex4": 40,
"dex5": 50,
"turntime": "0200-01-01 00:05:00"
}
}
],
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {
"generalIds": [1],
"cityIds": [1, 48],
"nationIds": [1]
}
}
@@ -1,77 +0,0 @@
{
"action": "RaiseNPCNation",
"setup": {
"resetCities": true,
"resetGenerals": true,
"resetNations": true,
"city": [
{
"id": 1,
"values": {
"name": "점유도시",
"level": 5,
"nation": 1,
"pop": 10001,
"pop_max": 50001,
"agri": 1001,
"agri_max": 5001,
"comm": 2001,
"comm_max": 6001,
"secu": 3001,
"secu_max": 7001,
"def": 4001,
"def_max": 8001,
"wall": 5001,
"wall_max": 9001
}
},
{
"id": 48,
"values": {
"name": "건국도시",
"level": 5,
"nation": 0,
"pop": 10048,
"pop_max": 50048,
"agri": 1048,
"agri_max": 5048,
"comm": 2048,
"comm_max": 6048,
"secu": 3048,
"secu_max": 7048,
"def": 4048,
"def_max": 8048,
"wall": 5048,
"wall_max": 9048
}
}
],
"nation": [
{
"id": 1,
"values": {
"name": "기준국",
"capital": 1,
"gennum": 1,
"tech": 120,
"level": 2,
"type": "che_유가"
}
}
],
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {
"cityIds": [1, 48],
"nationIds": [1]
}
}
@@ -1,35 +0,0 @@
{
"action": "RegNeutralNPC",
"args": [
0,
"등록재야",
null,
0,
33,
45,
55,
65,
180,
245,
null,
"무쌍",
""
],
"setup": {
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {
"generalIds": [],
"cityIds": [33],
"nationIds": [1]
}
}
-36
View File
@@ -1,36 +0,0 @@
{
"action": "RegNPC",
"args": [
0,
"등록장수",
null,
0,
33,
60,
50,
40,
7,
186,
240,
null,
"인덕",
"등록 대사"
],
"setup": {
"syncEnvironment": true
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190,
"turnterm": 10,
"turntime": "0200-01-01 00:00:00",
"show_img_level": 3,
"fiction": [0]
},
"observe": {
"generalIds": [],
"cityIds": [33],
"nationIds": [1]
}
}
@@ -1,24 +0,0 @@
{
"action": "UnblockScoutAction",
"args": [false],
"setup": {
"resetNations": true,
"deleteOtherNations": true,
"keepNationIds": [1, 2],
"nation": [
{ "id": 1, "values": { "name": "위", "level": 2, "scout": 1 } },
{ "id": 2, "values": { "name": "촉", "level": 2, "scout": 1 } }
],
"gameEnvironment": {
"block_change_scout": true
}
},
"environment": {
"year": 200,
"month": 1,
"startyear": 190
},
"observe": {
"nationIds": [1, 2]
}
}
@@ -1,140 +0,0 @@
{
"action": "UpdateCitySupply",
"environment": {
"year": 193,
"month": 1,
"startyear": 190
},
"setup": {
"nation": [
{
"id": 99,
"values": {
"name": "보급검증국",
"color": "#777777",
"capital": 1,
"level": 1,
"type": "che_중립"
}
},
{
"id": 98,
"values": {
"name": "불일치수도국",
"color": "#888888",
"capital": 2,
"level": 1,
"type": "che_중립"
}
}
],
"city": [
{
"id": 1,
"values": {
"nation": 99,
"supply": 0,
"front": 2,
"pop": 1001,
"agri": 501,
"comm": 499,
"secu": 99,
"trust": 50,
"def": 101,
"wall": 50,
"officer_set": 7,
"term": 2,
"conflict": {"98": 3}
}
},
{
"id": 9,
"values": {
"nation": 99,
"supply": 0,
"front": 2,
"pop": 1001,
"agri": 501,
"comm": 499,
"secu": 99,
"trust": 50,
"def": 101,
"wall": 50,
"officer_set": 7,
"term": 2,
"conflict": {"98": 3}
}
},
{
"id": 2,
"values": {
"nation": 99,
"supply": 1,
"front": 2,
"pop": 1001,
"agri": 501,
"comm": 499,
"secu": 99,
"trust": 33,
"def": 101,
"wall": 50,
"officer_set": 7,
"term": 2,
"conflict": {"98": 3}
}
},
{
"id": 3,
"values": {
"nation": 0,
"supply": 0,
"front": 0,
"trust": 50
}
}
],
"general": [
{
"id": 1,
"values": {
"nation": 99,
"city": 2,
"officer_level": 4,
"officer_city": 2,
"crew": 101,
"train": 51,
"atmos": 99
}
},
{
"id": 2,
"values": {
"nation": 99,
"city": 1,
"officer_level": 4,
"officer_city": 2,
"crew": 101,
"train": 51,
"atmos": 99
}
},
{
"id": 3,
"values": {
"nation": 98,
"city": 2,
"officer_level": 3,
"officer_city": 1,
"crew": 101,
"train": 51,
"atmos": 99
}
}
]
},
"observe": {
"generalIds": [1, 2, 3],
"cityIds": [1, 2, 3, 9],
"nationIds": [98, 99]
}
}
@@ -1,85 +0,0 @@
{
"action": "UpdateNationLevel",
"environment": {
"year": 193,
"month": 2,
"startyear": 190,
"killturn": 1000,
"turnterm": 10
},
"setup": {
"resetCities": true,
"resetGenerals": true,
"resetUniqueOccupancy": true,
"syncEnvironment": true,
"clearNationTurnIds": [1],
"clearInheritanceOwnerIds": [990061],
"nation": [
{
"id": 1,
"values": {
"name": "작위검증국",
"color": "#777777",
"capital": 1,
"gold": 10000,
"rice": 20000,
"level": 0,
"type": "che_중립",
"aux": {}
}
}
],
"city": [
{
"id": 1,
"values": {
"nation": 1,
"level": 4
}
},
{
"id": 2,
"values": {
"nation": 1,
"level": 4
}
}
],
"general": [
{
"id": 1,
"values": {
"name": "조조",
"nation": 1,
"city": 1,
"officer_level": 12,
"owner": 990061,
"npc": 0,
"killturn": 1000,
"belong": 10,
"horse": "None",
"weapon": "None",
"book": "None",
"item": "None",
"aux": {}
}
},
{
"id": 2,
"values": {
"nation": 0,
"city": 2,
"officer_level": 1,
"npc": 0,
"killturn": 0
}
}
]
},
"observe": {
"generalIds": [1],
"cityIds": [1, 2],
"nationIds": [1],
"ownerIds": [990061]
}
}
@@ -1,91 +0,0 @@
{
"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,
"stored_icons": [],
"icon_path": ".",
"fiction": [0]
}
}
-65
View File
@@ -1,65 +0,0 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli' || getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1') {
http_response_code(404);
exit(1);
}
chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';
$request = json_decode(stream_get_contents(STDIN), true, flags: JSON_THROW_ON_ERROR);
$generalIds = $request['generalIds'] ?? null;
if (!is_array($generalIds) || $generalIds === []) {
throw new \InvalidArgumentException('generalIds must be a non-empty array');
}
$db = DB::db();
$gameStorage = KVStorage::getStorage($db, 'game_env');
$env = $gameStorage->getAll(true);
$result = [];
$includeNation = ($request['includeNation'] ?? false) === true;
foreach ($generalIds as $generalId) {
if (!is_int($generalId) || $generalId < 1) {
throw new \InvalidArgumentException('generalIds must contain positive integers');
}
$general = General::createObjFromDB($generalId);
if ($general->getNPCType() < 2) {
throw new \InvalidArgumentException("general {$generalId} is not an NPC");
}
$ai = new GeneralAI($general);
$nationDecision = null;
if ($includeNation && $general->getNationID() !== 0 && $general->getVar('officer_level') >= 5) {
$nationStorage = KVStorage::getStorage($db, $general->getNationID(), 'nation_env');
$lastNationTurn = LastTurn::fromRaw($nationStorage->getValue("turn_last_{$general->getVar('officer_level')}"));
$reservedNation = buildNationCommandClass(null, $general, $env, $lastNationTurn);
$selectedNation = $ai->chooseNationTurn($reservedNation);
$nationDecision = [
'action' => $selectedNation->getRawClassName(),
'args' => $selectedNation->getArg(),
'reason' => $selectedNation->reason,
];
}
$reserved = $general->getReservedTurn(0, $env);
$selected = $ai->chooseGeneralTurn($reserved);
$result[] = [
'generalId' => $generalId,
'name' => $general->getName(),
'action' => $selected->getRawClassName(),
'brief' => $selected->getBrief(),
'args' => $selected->getArg(),
'reason' => $selected->reason,
'nationDecision' => $nationDecision,
];
}
echo json_encode([
'year' => (int) $env['year'],
'month' => (int) $env['month'],
'decisions' => $result,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
-40
View File
@@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';
$input = stream_get_contents(STDIN);
$keys = Json::decode($input);
if (!is_array($keys)) {
fwrite(STDERR, "Expected a JSON array of item keys.\n");
exit(2);
}
$result = [];
foreach ($keys as $key) {
if (!is_string($key)) {
continue;
}
$item = buildItemClass($key);
$result[$key] = [
'rawName' => $item->getRawName(),
'name' => $item->getName(),
'info' => $item->getInfo(),
'cost' => $item->getCost(),
'buyable' => $item->isBuyable(),
'consumable' => $item->isConsumable(),
'reqSecu' => $item->getReqSecu(),
];
}
echo Json::encode($result);
-1839
View File
@@ -1,1839 +0,0 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli' || getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
http_response_code(404);
exit;
}
chdir(dirname(__DIR__));
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
error_reporting(E_ALL & ~E_DEPRECATED);
require_once 'lib.php';
require_once 'func.php';
require_once __DIR__ . '/turn_state_snapshot.php';
function comparisonGameTick(mixed $value, string $label): int
{
if (is_int($value)) {
return GameClock::requireSafeTick($value);
}
if (!is_string($value) || $value === '') {
throw new \InvalidArgumentException("{$label} must be a game tick or projected date string");
}
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
return GameClock::fromStorage($gameStorage)->dateTimeToTick(new \DateTimeImmutable($value));
}
/** @param array<string, mixed> $values */
function comparisonMonthlyPatch(string $table, int $id, array $values): void
{
$definitions = [
'city' => [
'idColumn' => 'city',
'allowed' => [
'name', 'level', 'nation', 'supply', 'front', 'pop', 'pop_max',
'agri', 'agri_max', 'comm', 'comm_max', 'secu', 'secu_max',
'trust', 'trade', 'def', 'def_max', 'wall', 'wall_max',
'officer_set', 'state', 'region', 'term', 'conflict', 'dead',
],
],
'nation' => [
'idColumn' => 'nation',
'allowed' => [
'name', 'color', 'capital', 'gennum', 'gold', 'rice', 'tech',
'power', 'level', 'type', 'bill', 'rate_tmp', 'scout', 'chief_set', 'aux',
'rate', 'strategic_cmd_limit', 'surlimit', 'spy',
],
],
'general' => [
'idColumn' => 'no',
'allowed' => [
'name', 'nation', 'city', 'officer_level', 'officer_city',
'crew', 'train', 'atmos', 'owner', 'npc', 'killturn', 'belong',
'gold', 'rice', 'horse', 'weapon', 'book', 'item', 'aux',
'leadership', 'strength', 'intel', 'experience', 'dedication',
'injury',
'dex1', 'dex2', 'dex3', 'dex4', 'dex5', 'personal', 'special',
'specage', 'special2', 'specage2', 'betray', 'age',
'bornyear', 'deadyear', 'turntime', 'makelimit',
],
],
];
$definition = $definitions[$table] ?? null;
if ($definition === null) {
throw new \InvalidArgumentException("unsupported setup table: {$table}");
}
$patch = [];
foreach ($values as $key => $value) {
if (!is_string($key) || !in_array($key, $definition['allowed'], true)) {
throw new \InvalidArgumentException("unsupported {$table} setup field");
}
if (in_array($key, ['conflict', 'aux'], true) && is_array($value)) {
$value = json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
}
$patch[$key] = $value;
}
if ($table === 'nation') {
$patch += [
'nation' => $id,
'name' => "fixture-nation-{$id}",
'color' => '#777777',
'capital' => 0,
'gold' => 0,
'rice' => 0,
'level' => 1,
'type' => 'che_중립',
'aux' => '{}',
];
DB::db()->insertUpdate('nation', $patch, $patch);
} elseif ($patch !== []) {
DB::db()->update($table, $patch, "`{$definition['idColumn']}` = %i", $id);
}
}
/** @param array<string, mixed> $observe */
function comparisonMonthlyDetails(array $observe, int $worldHistoryAfterId): array
{
$db = DB::db();
$generals = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'no',
'nationId' => 'nation',
'cityId' => 'city',
'officerLevel' => 'officer_level',
'officerCityId' => 'officer_city',
'crew' => 'crew',
'train' => 'train',
'atmos' => 'atmos',
'gold' => 'gold',
'rice' => 'rice',
'injury' => 'injury',
'leadership' => 'leadership',
'strength' => 'strength',
'intelligence' => 'intel',
'age' => 'age',
'specialDomestic' => 'special',
'specAge' => 'specage',
'specialWar' => 'special2',
'specAge2' => 'specage2',
'betray' => 'betray',
'makeLimit' => 'makelimit',
'horse' => 'horse',
'weapon' => 'weapon',
'book' => 'book',
'item' => 'item',
'dex1' => 'dex1',
'dex2' => 'dex2',
'dex3' => 'dex3',
'dex4' => 'dex4',
'dex5' => 'dex5',
'aux' => 'aux',
],
['aux'],
),
comparisonRowsById('general', 'no', comparisonIntegerList($observe['generalIds'] ?? [], 'generalIds')),
);
$cities = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'city',
'nationId' => 'nation',
'state' => 'state',
'supplyState' => 'supply',
'frontState' => 'front',
'officerSet' => 'officer_set',
'term' => 'term',
'dead' => 'dead',
'population' => 'pop',
'populationMax' => 'pop_max',
'agriculture' => 'agri',
'agricultureMax' => 'agri_max',
'commerce' => 'comm',
'commerceMax' => 'comm_max',
'security' => 'secu',
'securityMax' => 'secu_max',
'trust' => 'trust',
'trade' => 'trade',
'defence' => 'def',
'defenceMax' => 'def_max',
'wall' => 'wall',
'wallMax' => 'wall_max',
'conflict' => 'conflict',
],
['conflict'],
),
comparisonRowsById('city', 'city', comparisonIntegerList($observe['cityIds'] ?? [], 'cityIds')),
);
$nations = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'nation',
'gold' => 'gold',
'rice' => 'rice',
'rate' => 'rate_tmp',
'baseRate' => 'rate',
'power' => 'power',
'generalCount' => 'gennum',
'tech' => 'tech',
'typeCode' => 'type',
'scout' => 'scout',
'strategicCommandLimit' => 'strategic_cmd_limit',
'surrenderLimit' => 'surlimit',
'spy' => 'spy',
'chiefSet' => 'chief_set',
],
['spy'],
),
comparisonRowsById('nation', 'nation', comparisonIntegerList($observe['nationIds'] ?? [], 'nationIds')),
);
$worldHistory = array_map(
static fn(array $row): array => comparisonPickRow(
$row,
[
'id' => 'id',
'nationId' => 'nation_id',
'year' => 'year',
'month' => 'month',
'text' => 'text',
],
),
$db->query(
'SELECT id, nation_id, year, month, text FROM world_history WHERE id > %i ORDER BY id',
$worldHistoryAfterId,
),
);
$inheritanceKeys = $observe['inheritanceKeys'] ?? ['unifier'];
if (!is_array($inheritanceKeys) || array_filter($inheritanceKeys, static fn($key): bool => !is_string($key))) {
throw new \InvalidArgumentException('inheritanceKeys must be an array of strings');
}
$inheritancePoints = [];
foreach (comparisonIntegerList($observe['ownerIds'] ?? [], 'ownerIds') as $ownerId) {
$entry = ['ownerId' => $ownerId];
foreach ($inheritanceKeys as $key) {
$value = $db->queryFirstField(
'SELECT value FROM storage WHERE namespace = %s AND `key` = %s',
"inheritance_{$ownerId}",
$key,
);
$entry[$key] = $value === null ? 0 : (comparisonJsonValue($value)[0] ?? 0);
}
$inheritancePoints[] = $entry;
}
$rankGeneralIds = comparisonIntegerList($observe['rankGeneralIds'] ?? [], 'rankGeneralIds');
$rankTypes = $observe['rankTypes'] ?? [];
if (!is_array($rankTypes) || array_filter($rankTypes, static fn($type): bool => !is_string($type))) {
throw new \InvalidArgumentException('rankTypes must be an array of strings');
}
$rankData = (!$rankGeneralIds || !$rankTypes)
? []
: iterator_to_array($db->query(
'SELECT general_id AS generalId, nation_id AS nationId, `type`, `value` FROM rank_data WHERE general_id IN %li AND `type` IN %ls ORDER BY general_id, `type`',
$rankGeneralIds,
$rankTypes,
));
$generalAccessLogs = [];
$accessGeneralIds = comparisonIntegerList($observe['accessGeneralIds'] ?? [], 'accessGeneralIds');
if ($accessGeneralIds) {
$generalAccessLogs = iterator_to_array($db->query(
'SELECT general_id AS generalId, refresh_score_total AS refreshScoreTotal FROM general_access_log WHERE general_id IN %li ORDER BY general_id',
$accessGeneralIds,
));
}
$nationEnvironment = [];
$nationEnvironmentKeys = $observe['nationEnvironmentKeys'] ?? [];
if (
!is_array($nationEnvironmentKeys)
|| array_filter($nationEnvironmentKeys, static fn($key): bool => !is_string($key))
) {
throw new \InvalidArgumentException('nationEnvironmentKeys must be an array of strings');
}
foreach (comparisonIntegerList($observe['nationIds'] ?? [], 'nationIds') as $nationId) {
$entry = ['nationId' => $nationId];
$storage = KVStorage::getStorage($db, $nationId, 'nation_env');
foreach ($nationEnvironmentKeys as $key) {
$entry[$key] = $storage->getValue($key);
}
$nationEnvironment[] = $entry;
}
return [
'generals' => $generals,
'cities' => $cities,
'nations' => $nations,
'worldHistory' => $worldHistory,
'inheritancePoints' => $inheritancePoints,
'rankData' => $rankData,
'generalAccessLogs' => $generalAccessLogs,
'nationEnvironment' => $nationEnvironment,
];
}
/**
* Exercise the dynamically-created invader events against one evolving database.
*
* @param array<string, mixed> $request
* @param array<string, mixed> $environment
* @return array<string, mixed>
*/
function comparisonInvaderLifecycleTrace(
array $request,
array $environment,
int $generalIdBeforeAction,
int $nationIdBeforeAction,
int $eventIdBeforeAction,
int $worldHistoryIdBeforeAction,
): array {
$db = DB::db();
$args = $request['args'] ?? [];
if (!is_array($args) || count($args) > 4) {
throw new \InvalidArgumentException('InvaderLifecycle args must have at most four entries');
}
$year = $environment['year'];
$month = $environment['month'];
$startYear = $environment['startyear'];
$raiseEnvironment = [
'year' => $year,
'month' => $month,
'startyear' => $startYear,
'turnterm' => $environment['turnterm'],
'turntime' => $environment['turntime'],
'show_img_level' => $environment['show_img_level'] ?? 3,
'stored_icons' => $environment['stored_icons'] ?? [],
'icon_path' => $environment['icon_path'] ?? '.',
'fiction' => $environment['fiction'] ?? [0],
];
(new \sammo\Event\Action\RaiseInvader(...$args))->run($raiseEnvironment);
$invaderNationIds = array_map(
'intval',
$db->queryFirstColumn('SELECT nation FROM nation WHERE nation > %i ORDER BY nation', $nationIdBeforeAction),
);
$existingNationId = (int)$db->queryFirstField(
'SELECT nation FROM nation WHERE nation <= %i ORDER BY nation LIMIT 1',
$nationIdBeforeAction,
);
$rulerIds = array_map(
'intval',
$db->queryFirstColumn(
'SELECT no FROM general WHERE nation IN %li AND officer_level = 12 ORDER BY nation',
$invaderNationIds,
),
);
if ($invaderNationIds === [] || $existingNationId <= 0 || count($rulerIds) !== count($invaderNationIds)) {
throw new \RuntimeException('InvaderLifecycle failed to create the expected entities');
}
$autoDeleteEvents = [];
$endingEventId = 0;
foreach ($db->query('SELECT id, action FROM event WHERE id > %i ORDER BY id', $eventIdBeforeAction) as $row) {
$action = comparisonJsonValue($row['action']);
$actionName = $action[0][0] ?? null;
if ($actionName === 'AutoDeleteInvader') {
$autoDeleteEvents[] = [
'eventId' => (int)$row['id'],
'nationId' => (int)($action[0][1] ?? 0),
];
} elseif ($actionName === 'InvaderEnding') {
$endingEventId = (int)$row['id'];
}
}
if (count($autoDeleteEvents) !== count($invaderNationIds) || $endingEventId <= 0) {
throw new \RuntimeException('InvaderLifecycle failed to create follow-up events');
}
$diplomacyRows = iterator_to_array($db->query(
'SELECT me, you, state, term FROM diplomacy WHERE (me IN %li AND you = %i) OR (me = %i AND you IN %li) ORDER BY me, you',
$invaderNationIds,
$existingNationId,
$existingNationId,
$invaderNationIds,
));
$diplomacyStates = array_values(array_unique(array_map(
static fn(array $row): string => "{$row['state']}:{$row['term']}",
$diplomacyRows,
)));
sort($diplomacyStates);
$generalCounts = array_map(
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
'SELECT count(*) FROM general WHERE nation = %i',
$nationId,
),
$invaderNationIds,
);
$diplomacyCounts = array_map(
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
'SELECT count(*) FROM diplomacy WHERE (me = %i AND you = %i) OR (me = %i AND you = %i)',
$nationId,
$existingNationId,
$existingNationId,
$nationId,
),
$invaderNationIds,
);
$gameStorage = KVStorage::getStorage($db, 'game_env');
$afterRaise = [
'createdNationCount' => count($invaderNationIds),
'createdGeneralCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM general WHERE no > %i',
$generalIdBeforeAction,
),
'followUpEventCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id > %i',
$eventIdBeforeAction,
),
'generalCountsPerNation' => array_values(array_unique($generalCounts)),
'diplomacyCountsPerNation' => array_values(array_unique($diplomacyCounts)),
'diplomacyStates' => $diplomacyStates,
'isunited' => (int)$gameStorage->isunited,
'blockChangeScout' => (bool)$gameStorage->block_change_scout,
];
$warResults = [];
foreach ($autoDeleteEvents as $autoDeleteEvent) {
$warResult = (new \sammo\Event\Action\AutoDeleteInvader($autoDeleteEvent['nationId']))->run([
'year' => $year,
'month' => $month + 1,
'startyear' => $startYear,
'currentEventID' => $autoDeleteEvent['eventId'],
]);
$warResults[] = $warResult[1] ?? null;
}
$atWar = [
'results' => array_values(array_unique($warResults)),
'autoDeleteEventCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id IN %li',
array_column($autoDeleteEvents, 'eventId'),
),
];
$db->update(
'diplomacy',
['state' => 2, 'term' => 0],
'me IN %li OR you IN %li',
$invaderNationIds,
$invaderNationIds,
);
$peaceResults = [];
foreach ($autoDeleteEvents as $autoDeleteEvent) {
$peaceResult = (new \sammo\Event\Action\AutoDeleteInvader($autoDeleteEvent['nationId']))->run([
'year' => $year,
'month' => $month + 2,
'startyear' => $startYear,
'currentEventID' => $autoDeleteEvent['eventId'],
]);
$peaceResults[] = $peaceResult[1] ?? null;
}
$atPeace = [
'results' => array_values(array_unique($peaceResults)),
'autoDeleteEventCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id IN %li',
array_column($autoDeleteEvents, 'eventId'),
),
'rulerWanderTurnCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM general_turn WHERE general_id IN %li AND action = %s',
$rulerIds,
'che_방랑',
),
'endingEventPresent' => (bool)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id = %i',
$endingEventId,
),
];
$invaderGeneralIds = array_map('intval', $db->queryFirstColumn(
'SELECT no FROM general WHERE nation IN %li',
$invaderNationIds,
));
if ($invaderGeneralIds !== []) {
$db->delete('general_turn', 'general_id IN %li', $invaderGeneralIds);
$db->delete('rank_data', 'general_id IN %li', $invaderGeneralIds);
$db->delete('troop', 'nation IN %li', $invaderNationIds);
$db->delete('general', 'no IN %li', $invaderGeneralIds);
}
$db->delete('nation_turn', 'nation_id IN %li', $invaderNationIds);
$db->delete('diplomacy', 'me IN %li OR you IN %li', $invaderNationIds, $invaderNationIds);
$db->delete('nation', 'nation IN %li', $invaderNationIds);
$db->update('city', ['nation' => $existingNationId], true);
$endingResult = (new \sammo\Event\Action\InvaderEnding())->run([
'year' => $year,
'month' => $month + 3,
'startyear' => $startYear,
'currentEventID' => $endingEventId,
]);
$gameStorage->resetCache();
$afterUserWin = [
'result' => $endingResult[1] ?? null,
'endingEventPresent' => (bool)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id = %i',
$endingEventId,
),
'isunited' => (int)$gameStorage->isunited,
'refreshLimit' => (int)$gameStorage->refreshLimit,
'logs' => array_values($db->queryFirstColumn(
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 AND year = %i AND month = %i ORDER BY id',
$worldHistoryIdBeforeAction,
$year,
$month + 3,
)),
];
return [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => 'InvaderLifecycle',
'phases' => [
'afterRaise' => $afterRaise,
'atWar' => $atWar,
'atPeace' => $atPeace,
'afterUserWin' => $afterUserWin,
],
];
}
/**
* Exercise NPC nation creation and the same-month troop leader follow-up.
*
* @param array<string, mixed> $environment
* @return array<string, mixed>
*/
function comparisonNpcNationLifecycleTrace(
array $environment,
int $generalIdBeforeAction,
int $nationIdBeforeAction,
int $worldHistoryIdBeforeAction,
): array {
$db = DB::db();
$actionEnvironment = [
'year' => $environment['year'],
'month' => $environment['month'],
'startyear' => $environment['startyear'],
'turnterm' => $environment['turnterm'],
'turntime' => $environment['turntime'],
'show_img_level' => $environment['show_img_level'] ?? 3,
'stored_icons' => $environment['stored_icons'] ?? [],
'icon_path' => $environment['icon_path'] ?? '.',
'fiction' => $environment['fiction'] ?? [0],
];
$gameStorage = KVStorage::getStorage($db, 'game_env');
$leaderCounterBefore = (int)($gameStorage->lastNPCTroopLeaderID ?? 0);
(new \sammo\Event\Action\RaiseNPCNation())->run($actionEnvironment);
$createdNationIds = array_map(
'intval',
$db->queryFirstColumn('SELECT nation FROM nation WHERE nation > %i ORDER BY nation', $nationIdBeforeAction),
);
if ($createdNationIds === []) {
throw new \RuntimeException('NpcNationLifecycle failed to create a nation');
}
$createdNationGeneralCounts = array_map(
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
'SELECT count(*) FROM general WHERE nation = %i',
$nationId,
),
$createdNationIds,
);
$afterRaise = [
'createdNationCount' => count($createdNationIds),
'createdGeneralCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM general WHERE no > %i',
$generalIdBeforeAction,
),
'generalCountsPerCreatedNation' => array_values(array_unique($createdNationGeneralCounts)),
'historyLogs' => array_values($db->queryFirstColumn(
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 ORDER BY id',
$worldHistoryIdBeforeAction,
)),
];
(new \sammo\Event\Action\ProvideNPCTroopLeader())->run($actionEnvironment);
$leaderIds = array_map(
'intval',
$db->queryFirstColumn('SELECT no FROM general WHERE no > %i AND npc = 5 ORDER BY no', $generalIdBeforeAction),
);
if ($leaderIds === []) {
throw new \RuntimeException('NpcNationLifecycle failed to create troop leaders');
}
$leaderNationCounts = array_map(
static fn(int $nationId): int => (int)DB::db()->queryFirstField(
'SELECT count(*) FROM general WHERE nation = %i AND npc = 5',
$nationId,
),
array_map(
'intval',
$db->queryFirstColumn('SELECT nation FROM nation WHERE level > 0 ORDER BY nation'),
),
);
$gatherTurnCounts = array_map(
static fn(int $generalId): int => (int)DB::db()->queryFirstField(
'SELECT count(*) FROM general_turn WHERE general_id = %i AND action = %s',
$generalId,
'che_집합',
),
$leaderIds,
);
$gameStorage->resetCache();
$afterProvide = [
'createdLeaderCount' => count($leaderIds),
'leaderCountsPerActiveNation' => array_values(array_unique($leaderNationCounts)),
'troopCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM troop WHERE troop_leader IN %li',
$leaderIds,
),
'gatherTurnCounts' => array_values(array_unique($gatherTurnCounts)),
'leaderCounterDelta' => (int)$gameStorage->lastNPCTroopLeaderID - $leaderCounterBefore,
];
return [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => 'NpcNationLifecycle',
'phases' => [
'afterRaise' => $afterRaise,
'afterProvide' => $afterProvide,
],
];
}
/**
* Exercise nation betting from monthly open through real user bets and settlement.
*
* @param array<string, mixed> $request
* @param array<string, mixed> $environment
* @return array<string, mixed>
*/
function comparisonNationBettingLifecycleTrace(
array $request,
array $environment,
int $eventIdBeforeAction,
int $worldHistoryIdBeforeAction,
): array {
$db = DB::db();
$args = $request['args'] ?? [];
if (!is_array($args) || count($args) > 2) {
throw new \InvalidArgumentException('NationBettingLifecycle args must have at most two entries');
}
$bettorGeneralIds = comparisonIntegerList(
$request['bettorGeneralIds'] ?? [],
'bettorGeneralIds',
);
$bettorOwnerIds = comparisonIntegerList(
$request['bettorOwnerIds'] ?? [],
'bettorOwnerIds',
);
if (count($bettorGeneralIds) !== 2 || count($bettorOwnerIds) !== 2) {
throw new \InvalidArgumentException(
'NationBettingLifecycle requires exactly two bettorGeneralIds and bettorOwnerIds',
);
}
$year = $environment['year'];
$month = $environment['month'];
$startYear = $environment['startyear'];
(new \sammo\Event\Action\OpenNationBetting(...$args))->run([
'year' => $year,
'month' => $month,
'startyear' => $startYear,
]);
$gameStorage = KVStorage::getStorage($db, 'game_env');
$gameStorage->resetCache();
$bettingId = (int)$gameStorage->last_betting_id;
$betting = new Betting($bettingId);
$bettingInfo = $betting->getInfo();
$candidateNationIds = array_map(
static fn(\sammo\DTO\SelectItem $candidate): int => (int)($candidate->aux['nation'] ?? 0),
$bettingInfo->candidates,
);
if (count($candidateNationIds) !== 2) {
throw new \RuntimeException('NationBettingLifecycle requires exactly two candidates');
}
$finishEventCount = (int)$db->queryFirstField(
'SELECT count(*) FROM event WHERE id > %i AND action LIKE %s',
$eventIdBeforeAction,
"%FinishNationBetting%{$bettingId}%",
);
$afterOpen = [
'bettingId' => $bettingId,
'candidateNationIds' => $candidateNationIds,
'systemBonus' => (int)($db->queryFirstField(
'SELECT amount FROM ng_betting WHERE betting_id = %i AND general_id = 0',
$bettingId,
) ?? 0),
'finishEventCount' => $finishEventCount,
'historyLogs' => array_values($db->queryFirstColumn(
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 ORDER BY id',
$worldHistoryIdBeforeAction,
)),
];
$betting->bet($bettorGeneralIds[0], $bettorOwnerIds[0], [1], 100);
$betting->bet($bettorGeneralIds[1], $bettorOwnerIds[1], [0], 100);
$afterBets = [
'bets' => array_map(
static fn(array $row): array => [
'generalId' => (int)$row['general_id'],
'ownerId' => $row['user_id'] === null ? null : (int)$row['user_id'],
'selectionKey' => $row['betting_type'],
'amount' => (int)$row['amount'],
],
iterator_to_array($db->query(
'SELECT general_id, user_id, betting_type, amount FROM ng_betting WHERE betting_id = %i ORDER BY general_id',
$bettingId,
)),
),
'inheritancePrevious' => array_map(
static function (int $ownerId): int {
$value = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->getValue('previous');
return is_array($value) ? (int)($value[0] ?? 0) : 0;
},
$bettorOwnerIds,
),
'spentRank' => array_map(
static fn(int $generalId): int => (int)(DB::db()->queryFirstField(
'SELECT value FROM rank_data WHERE general_id = %i AND type = %s',
$generalId,
\sammo\Enums\RankColumn::inherit_point_spent_dynamic->value,
) ?? 0),
$bettorGeneralIds,
),
];
$db->update('nation', ['level' => 0], 'nation = %i', $candidateNationIds[1]);
$finishResult = (new \sammo\Event\Action\FinishNationBetting($bettingId))->run([
'year' => $year,
'month' => $month + 1,
'startyear' => $startYear,
]);
$settledInfo = (new Betting($bettingId))->getInfo();
$afterFinish = [
'result' => $finishResult[1] ?? null,
'finished' => $settledInfo->finished,
'winner' => $settledInfo->winner,
'inheritancePrevious' => array_map(
static function (int $ownerId): int {
$value = KVStorage::getStorage(DB::db(), "inheritance_{$ownerId}")->getValue('previous');
return is_array($value) ? (int)($value[0] ?? 0) : 0;
},
$bettorOwnerIds,
),
'earnedRank' => array_map(
static fn(int $generalId): int => (int)(DB::db()->queryFirstField(
'SELECT value FROM rank_data WHERE general_id = %i AND type = %s',
$generalId,
\sammo\Enums\RankColumn::inherit_point_earned_by_action->value,
) ?? 0),
$bettorGeneralIds,
),
'historyLogs' => array_values($db->queryFirstColumn(
'SELECT text FROM world_history WHERE id > %i AND nation_id = 0 ORDER BY id',
$worldHistoryIdBeforeAction,
)),
];
return [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => 'NationBettingLifecycle',
'phases' => [
'afterOpen' => $afterOpen,
'afterBets' => $afterBets,
'afterFinish' => $afterFinish,
],
];
}
/**
* Reproduce the player-visible handoff from a completed unification to the
* invader event and prove that the real turn engine can cross the next monthly
* boundary afterwards. The surrounding runner executes this only against its
* disposable MariaDB clone.
*
* @param array<string, mixed> $request
* @param array<string, mixed> $environment
* @return array<string, mixed>
*/
function comparisonUnificationInvaderResumeTrace(array $request, array $environment): array
{
$db = DB::db();
$gameStorage = KVStorage::getStorage($db, 'game_env');
$winnerGeneralId = $request['winnerGeneralId'] ?? null;
if (!is_int($winnerGeneralId) || $winnerGeneralId <= 0) {
throw new \InvalidArgumentException('UnificationInvaderResume requires a positive winnerGeneralId');
}
$winnerNationId = (int)($db->queryFirstField(
'SELECT nation FROM general WHERE no = %i',
$winnerGeneralId,
) ?? 0);
if ($winnerNationId <= 0) {
throw new \RuntimeException('winner general must belong to a nation');
}
$allCityCount = count(CityConst::all());
$winnerCityCount = (int)$db->queryFirstField(
'SELECT COUNT(*) FROM city WHERE nation = %i',
$winnerNationId,
);
if ($winnerCityCount !== $allCityCount) {
throw new \RuntimeException(
"winner must occupy every scenario city: winner={$winnerCityCount}, scenario={$allCityCount}",
);
}
$year = $environment['year'] ?? null;
$month = $environment['month'] ?? null;
$startYear = $environment['startyear'] ?? null;
if (!is_int($year) || !is_int($month) || !is_int($startYear) || $year - $startYear < 20) {
throw new \InvalidArgumentException('fixture must start at least 20 game years after startyear');
}
$unificationTick = Util::toInt($gameStorage->turntime);
$elapsedMonths = ($year - $startYear) * 12 + $month - 1;
$gameStorage->starttime = GameClock::addTicks(
cutTurn($unificationTick, Util::toInt($gameStorage->turnterm)),
-$elapsedMonths * GameClock::TICKS_PER_TURN,
);
$clock = GameClock::fromStorage($gameStorage);
$clock->persistTick($gameStorage, $unificationTick, GameClock::MODE_MANUAL);
$db->update('general', ['turntime' => $unificationTick], true);
$db->update('plock', ['plock' => 0], true);
$gameStorage->resetCache();
refreshNationStaticInfo();
$before = [
'year' => Util::toInt($gameStorage->year),
'month' => Util::toInt($gameStorage->month),
'isunited' => Util::toInt($gameStorage->isunited),
'clockTick' => GameClock::fromStorage($gameStorage)->nowTick(),
'turntime' => Util::toInt($gameStorage->turntime),
'nationCount' => (int)$db->queryFirstField('SELECT COUNT(*) FROM nation WHERE level > 0'),
'winnerCityCount' => $winnerCityCount,
];
checkEmperior();
$gameStorage->resetCache();
$invaderMessages = [];
foreach ($db->query(
'SELECT id, mailbox, message FROM message WHERE mailbox = %i ORDER BY id',
$winnerGeneralId,
) as $row) {
$message = Json::decode($row['message']);
if (($message['option']['action'] ?? null) !== 'raiseInvader') {
continue;
}
$invaderMessages[] = [
'id' => (int)$row['id'],
'mailbox' => (int)$row['mailbox'],
'text' => (string)($message['text'] ?? ''),
'args' => $message['option']['args'] ?? null,
'used' => (bool)($message['option']['used'] ?? false),
];
}
if (count($invaderMessages) !== 3) {
throw new \RuntimeException('unification must create exactly three invader choices for the ruler');
}
$buttonSnapshot = [
'year' => Util::toInt($gameStorage->year),
'month' => Util::toInt($gameStorage->month),
'isunited' => Util::toInt($gameStorage->isunited),
'clockMode' => GameClock::fromStorage($gameStorage)->getMode(),
'clockTick' => GameClock::fromStorage($gameStorage)->nowTick(),
'turntime' => Util::toInt($gameStorage->turntime),
'plock' => (int)$db->queryFirstField("SELECT plock FROM plock WHERE type = 'GAME' LIMIT 1"),
'messages' => $invaderMessages,
];
$stopAfterButton = $request['stopAfterButton'] ?? false;
if (!is_bool($stopAfterButton)) {
throw new \InvalidArgumentException('stopAfterButton must be a boolean');
}
if ($stopAfterButton) {
return [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => 'UnificationInvaderResume',
'phase' => 'button',
'before' => $before,
'buttonSnapshot' => $buttonSnapshot,
];
}
// Simulate an autoreset poll that recorded the original unification pause.
// A successful event start must replace this stale operational wall anchor.
$pausedAnchor = '2000-01-01 00:00:00.000000';
$gameStorage->autoreset_united_wall_anchor = $pausedAnchor;
$selectedMessageId = $invaderMessages[array_key_last($invaderMessages)]['id'];
$message = Message::getMessageByID($selectedMessageId);
if (!$message instanceof RaiseInvaderMessage) {
throw new \RuntimeException('selected invader message did not hydrate as RaiseInvaderMessage');
}
$reason = '';
$decisionResult = $message->agreeMessage($winnerGeneralId, $reason);
$gameStorage->resetCache();
$afterSummon = [
'result' => $decisionResult,
'reason' => $reason,
'isunited' => Util::toInt($gameStorage->isunited),
'clockMode' => GameClock::fromStorage($gameStorage)->getMode(),
'clockTick' => GameClock::fromStorage($gameStorage)->nowTick(),
'turntime' => Util::toInt($gameStorage->turntime),
'turnterm' => Util::toInt($gameStorage->turnterm),
'plock' => (int)$db->queryFirstField("SELECT plock FROM plock WHERE type = 'GAME' LIMIT 1"),
'nationCount' => (int)$db->queryFirstField('SELECT COUNT(*) FROM nation WHERE level > 0'),
'invaderNationCount' => (int)$db->queryFirstField("SELECT COUNT(*) FROM nation WHERE name LIKE 'ⓞ%족'"),
'eventCount' => (int)$db->queryFirstField(
"SELECT COUNT(*) FROM event WHERE action LIKE '%Invader%'",
),
'autoresetAnchor' => $gameStorage->autoreset_united_wall_anchor,
'autoresetAnchorRefreshed' => $gameStorage->autoreset_united_wall_anchor !== $pausedAnchor,
];
if (
$decisionResult !== RaiseInvaderMessage::ACCEPTED
|| $afterSummon['isunited'] !== 1
|| $afterSummon['plock'] !== 0
|| $afterSummon['invaderNationCount'] !== 7
|| $afterSummon['autoresetAnchorRefreshed'] !== true
) {
throw new \RuntimeException('invader choice did not start an unlocked, watchdog-active event game');
}
$resumeBoundary = addTurn(
cutTurn(Util::toInt($gameStorage->turntime), Util::toInt($gameStorage->turnterm)),
Util::toInt($gameStorage->turnterm),
);
GameClock::fromStorage($gameStorage)->persistTick(
$gameStorage,
GameClock::addTicks($resumeBoundary, 1),
GameClock::MODE_MANUAL,
);
$gameStorage->resetCache();
// Likewise, active event progress must move the inactivity anchor. Leaving
// this stale makes j_autoreset close a healthy invader game on the original
// unification timeline.
$stalledAnchor = '2000-01-02 00:00:00.000000';
$gameStorage->autoreset_united_wall_anchor = $stalledAnchor;
$engineCalls = 0;
$executedCalls = 0;
$lockedCalls = 0;
while ($engineCalls < 20 && Util::toInt($gameStorage->turntime) < $resumeBoundary) {
$executed = false;
$locked = false;
TurnExecutionHelper::executeAllCommand($executed, $locked);
$engineCalls++;
if ($executed) {
$executedCalls++;
}
if ($locked) {
$lockedCalls++;
}
$gameStorage->resetCache();
}
$afterResume = [
'year' => Util::toInt($gameStorage->year),
'month' => Util::toInt($gameStorage->month),
'isunited' => Util::toInt($gameStorage->isunited),
'clockMode' => GameClock::fromStorage($gameStorage)->getMode(),
'clockTick' => GameClock::fromStorage($gameStorage)->nowTick(),
'turntime' => Util::toInt($gameStorage->turntime),
'resumeBoundary' => $resumeBoundary,
'engineCalls' => $engineCalls,
'executedCalls' => $executedCalls,
'lockedCalls' => $lockedCalls,
'plock' => (int)$db->queryFirstField("SELECT plock FROM plock WHERE type = 'GAME' LIMIT 1"),
'worldHistoryCount' => (int)$db->queryFirstField('SELECT COUNT(*) FROM world_history'),
'generalRecordCount' => (int)$db->queryFirstField('SELECT COUNT(*) FROM general_record'),
'autoresetAnchor' => $gameStorage->autoreset_united_wall_anchor,
'autoresetAnchorRefreshed' => $gameStorage->autoreset_united_wall_anchor !== $stalledAnchor,
];
$expectedYear = $month === 12 ? $year + 1 : $year;
$expectedMonth = $month === 12 ? 1 : $month + 1;
if (
$afterResume['turntime'] < $resumeBoundary
|| $afterResume['year'] !== $expectedYear
|| $afterResume['month'] !== $expectedMonth
|| $afterResume['isunited'] !== 1
|| $afterResume['lockedCalls'] !== 0
|| $afterResume['plock'] !== 0
|| $afterResume['autoresetAnchorRefreshed'] !== true
) {
throw new \RuntimeException('invader event game did not advance through the next monthly boundary');
}
return [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => 'UnificationInvaderResume',
'before' => $before,
'buttonSnapshot' => $buttonSnapshot,
'afterSummon' => $afterSummon,
'afterResume' => $afterResume,
];
}
function comparisonMonthlyEventTraceMain(): void
{
try {
$input = stream_get_contents(STDIN);
$request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($request)) {
throw new \InvalidArgumentException('request must be an object');
}
$actionName = $request['action'] ?? null;
$supportedActions = [
'UpdateCitySupply',
'UpdateNationLevel',
'RaiseDisaster',
'ProcessSemiAnnual',
'ProcessWarIncome',
'CreateAdminNPC',
'CreateManyNPC',
'RegNPC',
'RegNeutralNPC',
'RaiseNPCNation',
'NpcNationLifecycle',
'RaiseInvader',
'InvaderLifecycle',
'UnificationInvaderResume',
'AutoDeleteInvader',
'InvaderEnding',
'ChangeCity',
'ProvideNPCTroopLeader',
'NationBettingLifecycle',
'OpenNationBetting',
'FinishNationBetting',
'BlockScoutAction',
'UnblockScoutAction',
'AssignGeneralSpeciality',
'AddGlobalBetray',
'LostUniqueItem',
'MergeInheritPointRank',
'ProcessIncome',
'NoticeToHistoryLog',
'NewYear',
'ResetOfficerLock',
'PreUpdateMonthly',
'PostUpdateMonthly',
'MonthlyBoundary',
];
if (!in_array($actionName, $supportedActions, true)) {
throw new \InvalidArgumentException('unsupported monthly action');
}
$setup = $request['setup'] ?? [];
if (!is_array($setup)) {
throw new \InvalidArgumentException('setup must be an object');
}
foreach (['city', 'nation', 'general'] as $table) {
$rows = $setup[$table] ?? [];
if (!is_array($rows)) {
throw new \InvalidArgumentException("setup.{$table} must be an array");
}
foreach ($rows as $row) {
if (!is_array($row) || !is_int($row['id'] ?? null) || !is_array($row['values'] ?? null)) {
throw new \InvalidArgumentException("invalid setup.{$table} row");
}
comparisonMonthlyPatch($table, $row['id'], $row['values']);
}
}
if (($setup['resetCities'] ?? false) === true) {
DB::db()->query('UPDATE city SET nation = 0, level = 1');
foreach ($setup['city'] ?? [] as $row) {
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',
'None',
'None',
'None',
'None',
);
foreach ($setup['general'] ?? [] as $row) {
comparisonMonthlyPatch('general', $row['id'], $row['values']);
}
}
if (($setup['deleteOtherGenerals'] ?? false) === true) {
$keepGeneralIds = comparisonIntegerList($setup['keepGeneralIds'] ?? [], 'keepGeneralIds');
if (!$keepGeneralIds) {
throw new \InvalidArgumentException('deleteOtherGenerals requires keepGeneralIds');
}
DB::db()->query('DELETE FROM general_turn WHERE general_id NOT IN %li', $keepGeneralIds);
DB::db()->query('DELETE FROM rank_data WHERE general_id NOT IN %li', $keepGeneralIds);
DB::db()->query('DELETE FROM general WHERE no NOT IN %li', $keepGeneralIds);
}
if (($setup['resetNations'] ?? false) === true) {
DB::db()->query('UPDATE nation SET level = 0, gennum = 0, tech = 0');
foreach ($setup['nation'] ?? [] as $row) {
comparisonMonthlyPatch('nation', $row['id'], $row['values']);
}
}
if (($setup['deleteOtherNations'] ?? false) === true) {
$keepNationIds = comparisonIntegerList($setup['keepNationIds'] ?? [], 'keepNationIds');
if (!$keepNationIds) {
throw new \InvalidArgumentException('deleteOtherNations requires keepNationIds');
}
DB::db()->query(
'DELETE FROM diplomacy WHERE me NOT IN %li OR you NOT IN %li',
$keepNationIds,
$keepNationIds,
);
DB::db()->query('DELETE FROM nation_turn WHERE nation_id NOT IN %li', $keepNationIds);
DB::db()->query('DELETE FROM nation WHERE nation NOT IN %li', $keepNationIds);
}
if (($setup['resetUniqueOccupancy'] ?? false) === true) {
DB::db()->delete('ng_auction', '`type` = %s AND finished = 0', 'uniqueItem');
DB::db()->query('DELETE FROM storage WHERE namespace LIKE %s', 'ut\\_%');
}
if (($setup['clearNeutralAuctions'] ?? false) === true) {
DB::db()->delete(
'ng_auction',
'`type` IN %ls AND host_general_id = 0',
['buyRice', 'sellRice'],
);
}
foreach (comparisonIntegerList($setup['clearNationTurnIds'] ?? [], 'clearNationTurnIds') as $nationId) {
DB::db()->delete('nation_turn', 'nation_id = %i', $nationId);
}
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]);
}
foreach ($setup['inheritancePoints'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['ownerId'] ?? null)
|| !is_string($row['key'] ?? null)
|| !is_numeric($row['value'] ?? null)
) {
throw new \InvalidArgumentException('invalid setup.inheritancePoints row');
}
KVStorage::getStorage(DB::db(), "inheritance_{$row['ownerId']}")->setValue(
$row['key'],
[$row['value'], null],
);
}
foreach ($setup['rankData'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['generalId'] ?? null)
|| !is_string($row['type'] ?? null)
|| !is_int($row['value'] ?? null)
) {
throw new \InvalidArgumentException('invalid setup.rankData row');
}
$nationId = (int)(DB::db()->queryFirstField(
'SELECT nation FROM general WHERE no = %i',
$row['generalId'],
) ?? 0);
DB::db()->insertUpdate('rank_data', [
'general_id' => $row['generalId'],
'nation_id' => $nationId,
'type' => $row['type'],
'value' => $row['value'],
], [
'nation_id' => $nationId,
'value' => $row['value'],
]);
}
foreach ($setup['generalAccessLog'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['generalId'] ?? null)
|| !is_int($row['refreshScoreTotal'] ?? null)
) {
throw new \InvalidArgumentException('invalid setup.generalAccessLog row');
}
DB::db()->insertUpdate('general_access_log', [
'general_id' => $row['generalId'],
'refresh_score_total' => $row['refreshScoreTotal'],
], [
'refresh_score_total' => $row['refreshScoreTotal'],
]);
}
foreach ($setup['nationEnvironment'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['nationId'] ?? null)
|| !is_string($row['key'] ?? null)
|| !array_key_exists('value', $row)
) {
throw new \InvalidArgumentException('invalid setup.nationEnvironment row');
}
KVStorage::getStorage(DB::db(), $row['nationId'], 'nation_env')->setValue(
$row['key'],
$row['value'],
);
}
if (($setup['resetDiplomacy'] ?? false) === true) {
DB::db()->query('DELETE FROM diplomacy');
}
foreach ($setup['diplomacy'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['fromNationId'] ?? null)
|| !is_int($row['toNationId'] ?? null)
|| !is_int($row['state'] ?? null)
|| !is_int($row['term'] ?? null)
) {
throw new \InvalidArgumentException('invalid setup.diplomacy row');
}
$diplomacyPatch = [
'me' => $row['fromNationId'],
'you' => $row['toNationId'],
'state' => $row['state'],
'term' => $row['term'],
'dead' => $row['dead'] ?? 0,
];
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');
}
$eventPatch = [
'id' => $row['id'],
'target' => $row['target'] ?? 'month',
'priority' => $row['priority'] ?? 1000,
'condition' => Json::encode($row['condition'] ?? true),
'action' => Json::encode($row['action']),
];
DB::db()->insertUpdate('event', $eventPatch, $eventPatch);
}
$gameEnvironmentPatch = $setup['gameEnvironment'] ?? [];
if (!is_array($gameEnvironmentPatch)) {
throw new \InvalidArgumentException('setup.gameEnvironment must be an object');
}
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
$worldPatch = $setup['world'] ?? [];
if (!is_array($worldPatch)) {
throw new \InvalidArgumentException('setup.world must be an object');
}
if (array_key_exists('hiddenSeed', $worldPatch)) {
if (!is_string($worldPatch['hiddenSeed']) || $worldPatch['hiddenSeed'] === '') {
throw new \InvalidArgumentException('setup.world.hiddenSeed must be a non-empty string');
}
UniqueConst::$hiddenSeed = $worldPatch['hiddenSeed'];
}
foreach ([
'isunited',
'refreshLimit',
'block_change_scout',
'lastNPCTroopLeaderID',
'tournament',
'tnmt_trig',
'tnmt_pattern',
'prev_winner',
] as $key) {
if (array_key_exists($key, $gameEnvironmentPatch)) {
$gameStorage->setValue($key, $gameEnvironmentPatch[$key]);
}
}
if (($setup['setAllCitiesNation'] ?? null) !== null) {
if (!is_int($setup['setAllCitiesNation'])) {
throw new \InvalidArgumentException('setup.setAllCitiesNation must be an integer');
}
DB::db()->update('city', ['nation' => $setup['setAllCitiesNation']], true);
}
$db = DB::db();
$generalIdBeforeAction =
(int)($db->queryFirstField('SELECT COALESCE(MAX(no), 0) FROM general') ?? 0);
$nationIdBeforeAction =
(int)($db->queryFirstField('SELECT COALESCE(MAX(nation), 0) FROM nation') ?? 0);
$eventIdBeforeAction =
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM event') ?? 0);
$auctionIdBeforeAction =
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM ng_auction') ?? 0);
$snapshotRequest = ['observe' => $request['observe'] ?? []];
$snapshotRequest['observe']['logAfterId'] =
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM general_record') ?? 0);
$worldHistoryAfterId =
(int)($db->queryFirstField('SELECT COALESCE(MAX(id), 0) FROM world_history') ?? 0);
$before = comparisonTurnStateSnapshot($snapshotRequest);
$beforeDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId);
$environment = $request['environment'] ?? [];
if (!is_array($environment)) {
throw new \InvalidArgumentException('environment must be an object');
}
foreach (['starttime', 'turntime'] as $timeKey) {
if (array_key_exists($timeKey, $environment)) {
$environment[$timeKey] = comparisonGameTick(
$environment[$timeKey],
"environment.{$timeKey}",
);
}
}
$year = $environment['year'] ?? null;
$month = $environment['month'] ?? null;
$startYear = $environment['startyear'] ?? null;
if (!is_int($year) || !is_int($month) || !is_int($startYear)) {
throw new \InvalidArgumentException('environment year, month, and startyear must be integers');
}
if (($setup['syncEnvironment'] ?? false) === true) {
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
$environmentValues = [
'year' => $year,
'month' => $month,
'startyear' => $startYear,
'killturn' => $environment['killturn'] ?? $gameStorage->killturn,
'turnterm' => $environment['turnterm'] ?? $gameStorage->turnterm,
];
if (isset($environment['starttime'])) {
$environmentValues['starttime'] = $environment['starttime'];
}
foreach ($environmentValues as $key => $value) {
$gameStorage->setValue($key, $value);
}
if ($actionName === 'MonthlyBoundary') {
$before = comparisonTurnStateSnapshot($snapshotRequest);
$beforeDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId);
}
}
if ($actionName === 'InvaderLifecycle') {
foreach (['turnterm', 'turntime'] as $requiredEnvironmentKey) {
if (!array_key_exists($requiredEnvironmentKey, $environment)) {
throw new \InvalidArgumentException(
"InvaderLifecycle requires environment.{$requiredEnvironmentKey}",
);
}
}
echo json_encode(
comparisonInvaderLifecycleTrace(
$request,
$environment,
$generalIdBeforeAction,
$nationIdBeforeAction,
$eventIdBeforeAction,
$worldHistoryAfterId,
),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
return;
}
if ($actionName === 'UnificationInvaderResume') {
echo json_encode(
comparisonUnificationInvaderResumeTrace($request, $environment),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
);
return;
}
if ($actionName === 'NpcNationLifecycle') {
foreach (['turnterm', 'turntime'] as $requiredEnvironmentKey) {
if (!array_key_exists($requiredEnvironmentKey, $environment)) {
throw new \InvalidArgumentException(
"NpcNationLifecycle requires environment.{$requiredEnvironmentKey}",
);
}
}
echo json_encode(
comparisonNpcNationLifecycleTrace(
$environment,
$generalIdBeforeAction,
$nationIdBeforeAction,
$worldHistoryAfterId,
),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
return;
}
if ($actionName === 'NationBettingLifecycle') {
echo json_encode(
comparisonNationBettingLifecycleTrace(
$request,
$environment,
$eventIdBeforeAction,
$worldHistoryAfterId,
),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
return;
}
$actionClass = "\\sammo\\Event\\Action\\{$actionName}";
if (in_array($actionName, ['PreUpdateMonthly', 'PostUpdateMonthly', 'MonthlyBoundary'], true)) {
$action = null;
} elseif ($actionName === 'ProcessSemiAnnual') {
$resource = $request['resource'] ?? null;
if (!is_string($resource)) {
throw new \InvalidArgumentException('ProcessSemiAnnual requires resource');
}
$action = new $actionClass($resource);
} elseif (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseInvader', 'AutoDeleteInvader', 'ChangeCity', 'OpenNationBetting', 'FinishNationBetting', 'BlockScoutAction', 'UnblockScoutAction', 'AddGlobalBetray', 'LostUniqueItem', 'ProcessIncome', 'NoticeToHistoryLog'], true)) {
$args = $request['args'] ?? [];
if (!is_array($args)) {
throw new \InvalidArgumentException("{$actionName} args must be an array");
}
if ($actionName === 'CreateManyNPC' && count($args) > 2) {
throw new \InvalidArgumentException('CreateManyNPC args must have at most two entries');
}
if ($actionName === 'RaiseInvader' && count($args) > 4) {
throw new \InvalidArgumentException('RaiseInvader args must have at most four entries');
}
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');
}
if (in_array($actionName, ['BlockScoutAction', 'UnblockScoutAction'], true) && count($args) > 1) {
throw new \InvalidArgumentException("{$actionName} args must have at most one entry");
}
if ($actionName === 'AddGlobalBetray' && count($args) > 2) {
throw new \InvalidArgumentException('AddGlobalBetray args must have at most two entries');
}
if ($actionName === 'LostUniqueItem' && count($args) > 1) {
throw new \InvalidArgumentException('LostUniqueItem args must have at most one entry');
}
if ($actionName === 'ProcessIncome' && count($args) !== 1) {
throw new \InvalidArgumentException('ProcessIncome args must have exactly one entry');
}
if ($actionName === 'NoticeToHistoryLog' && (count($args) < 1 || count($args) > 2)) {
throw new \InvalidArgumentException('NoticeToHistoryLog args must have one or two entries');
}
$action = new $actionClass(...$args);
} else {
$action = new $actionClass();
}
$actionEnvironment = [
'year' => $year,
'month' => $month,
'startyear' => $startYear,
];
if (isset($environment['currentEventID'])) {
if (!is_int($environment['currentEventID'])) {
throw new \InvalidArgumentException('currentEventID must be an integer');
}
$actionEnvironment['currentEventID'] = $environment['currentEventID'];
}
if ($actionName === 'UpdateNationLevel') {
$killturn = $environment['killturn'] ?? null;
$turnterm = $environment['turnterm'] ?? null;
if (!is_int($killturn) || !is_int($turnterm) || $turnterm <= 0) {
throw new \InvalidArgumentException('UpdateNationLevel requires integer killturn and positive turnterm');
}
$actionEnvironment['killturn'] = $killturn;
$actionEnvironment['turnterm'] = $turnterm;
}
if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseNPCNation', 'RaiseInvader', 'ProvideNPCTroopLeader'], true)) {
$turnterm = $environment['turnterm'] ?? null;
$turntime = $environment['turntime'] ?? null;
$showImgLevel = $environment['show_img_level'] ?? 3;
$fiction = $environment['fiction'] ?? [0];
if (
!is_int($turnterm)
|| $turnterm <= 0
|| !is_int($turntime)
|| !is_int($showImgLevel)
|| !is_array($fiction)
) {
throw new \InvalidArgumentException(
"{$actionName} requires positive turnterm, game-tick turntime, integer show_img_level, and array fiction",
);
}
$actionEnvironment += [
'turnterm' => $turnterm,
'turntime' => $turntime,
'show_img_level' => $showImgLevel,
'stored_icons' => $environment['stored_icons'] ?? [],
'icon_path' => $environment['icon_path'] ?? '.',
'fiction' => $fiction,
];
}
if ($actionName === 'PreUpdateMonthly') {
if (!preUpdateMonthly()) {
throw new \RuntimeException('preUpdateMonthly failed');
}
} elseif ($actionName === 'PostUpdateMonthly') {
$seedYear = $environment['seedYear'] ?? null;
$seedMonth = $environment['seedMonth'] ?? null;
if (!is_int($seedYear) || !is_int($seedMonth)) {
throw new \InvalidArgumentException('PostUpdateMonthly requires seedYear and seedMonth');
}
postUpdateMonthly(new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
UniqueConst::$hiddenSeed,
'monthly',
$seedYear,
$seedMonth,
))));
} elseif ($actionName === 'MonthlyBoundary') {
$turntime = $environment['turntime'] ?? null;
if (!is_int($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);
}
unset($action);
gc_collect_cycles();
$after = comparisonTurnStateSnapshot($snapshotRequest);
$afterDetails = comparisonMonthlyDetails($snapshotRequest['observe'], $worldHistoryAfterId);
$createdGenerals = [];
if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC', 'RaiseNPCNation', 'RaiseInvader', 'ProvideNPCTroopLeader'], true)) {
foreach ($db->query('SELECT * FROM general WHERE no > %i ORDER BY no', $generalIdBeforeAction) as $row) {
$generalId = (int)$row['no'];
$turnRows = $db->query(
'SELECT turn_idx, action, arg FROM general_turn WHERE general_id = %i ORDER BY turn_idx',
$generalId,
);
$createdGenerals[] = comparisonPickRow(
$row,
[
'id' => 'no',
'name' => 'name',
'nationId' => 'nation',
'cityId' => 'city',
'leadership' => 'leadership',
'strength' => 'strength',
'intelligence' => 'intel',
'experience' => 'experience',
'dedication' => 'dedication',
'officerLevel' => 'officer_level',
'gold' => 'gold',
'rice' => 'rice',
'crew' => 'crew',
'crewTypeId' => 'crewtype',
'train' => 'train',
'atmos' => 'atmos',
'turnTime' => 'turntime',
'killturn' => 'killturn',
'age' => 'age',
'npcState' => 'npc',
'npcOriginalState' => 'npc_org',
'affinity' => 'affinity',
'personality' => 'personal',
'specialDomestic' => 'special',
'specialWar' => 'special2',
'specAge' => 'specage',
'specAge2' => 'specage2',
'bornYear' => 'bornyear',
'deadYear' => 'deadyear',
'picture' => 'picture',
'dex1' => 'dex1',
'dex2' => 'dex2',
'dex3' => 'dex3',
'dex4' => 'dex4',
'dex5' => 'dex5',
],
) + [
'turnCount' => count($turnRows),
'turnActions' => array_values(array_unique(array_column($turnRows, 'action'))),
'rankCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM rank_data WHERE general_id = %i',
$generalId,
),
'nonZeroRankCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM rank_data WHERE general_id = %i AND value != 0',
$generalId,
),
'troop' => $db->queryFirstRow(
'SELECT troop_leader AS id, nation AS nationId, name FROM troop WHERE troop_leader = %i',
$generalId,
) ?: null,
];
}
}
$createdNations = [];
if (in_array($actionName, ['RaiseNPCNation', 'RaiseInvader'], true)) {
foreach ($db->query('SELECT * FROM nation WHERE nation > %i ORDER BY nation', $nationIdBeforeAction) as $row) {
$nationId = (int)$row['nation'];
$createdNations[] = comparisonPickRow(
$row,
[
'id' => 'nation',
'name' => 'name',
'color' => 'color',
'capitalCityId' => 'capital',
'generalCount' => 'gennum',
'gold' => 'gold',
'rice' => 'rice',
'tech' => 'tech',
'level' => 'level',
'typeCode' => 'type',
'bill' => 'bill',
'rate' => 'rate',
'scout' => 'scout',
'war' => 'war',
'strategicCommandLimit' => 'strategic_cmd_limit',
'surrenderLimit' => 'surlimit',
'aux' => 'aux',
],
['aux'],
) + [
'nationTurnCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM nation_turn WHERE nation_id = %i',
$nationId,
),
'diplomacyCount' => (int)$db->queryFirstField(
'SELECT count(*) FROM diplomacy WHERE me = %i OR you = %i',
$nationId,
$nationId,
),
];
}
}
$response = [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => $actionName,
'before' => $before,
'beforeDetails' => $beforeDetails,
'after' => $after,
'afterDetails' => $afterDetails,
];
if (in_array($actionName, ['CreateManyNPC', 'RegNPC', 'RegNeutralNPC'], true)) {
$response['createdGenerals'] = $createdGenerals;
}
if (in_array($actionName, ['RaiseNPCNation', 'RaiseInvader'], true)) {
$response['createdGenerals'] = $createdGenerals;
$response['createdNations'] = $createdNations;
}
if ($actionName === 'ProvideNPCTroopLeader') {
$response['createdGenerals'] = $createdGenerals;
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
'lastNPCTroopLeaderID',
]);
}
if ($actionName === 'PreUpdateMonthly') {
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
'develcost',
]);
}
if ($actionName === 'PostUpdateMonthly') {
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
'tournament',
'tnmt_trig',
'tnmt_pattern',
'tnmt_type',
'tnmt_auto',
'phase',
]);
$response['createdNeutralAuctions'] = array_map(
static fn(array $row): array => [
'id' => (int)$row['id'],
'type' => $row['type'],
'target' => $row['target'],
'detail' => Json::decode($row['detail']),
],
iterator_to_array(
$db->query(
'SELECT id, `type`, target, detail FROM ng_auction WHERE id > %i AND host_general_id = 0 ORDER BY id',
$auctionIdBeforeAction,
),
),
);
}
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 (in_array($actionName, ['BlockScoutAction', 'UnblockScoutAction'], true)) {
$response['gameEnvironment'] = KVStorage::getStorage(DB::db(), 'game_env')->getValues([
'block_change_scout',
]);
}
if ($actionName === 'RaiseInvader') {
$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['createdDiplomacy'] = iterator_to_array(
$db->query(
'SELECT me AS fromNationId, you AS toNationId, state, term FROM diplomacy WHERE me > %i OR you > %i ORDER BY me, you',
$nationIdBeforeAction,
$nationIdBeforeAction,
),
);
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
$response['gameEnvironment'] = $gameStorage->getValues([
'isunited',
'turnterm',
'block_change_scout',
'refreshLimit',
]);
}
if (in_array($actionName, ['AutoDeleteInvader', 'InvaderEnding'], true)) {
$gameStorage = KVStorage::getStorage(DB::db(), 'game_env');
$response['gameEnvironment'] = $gameStorage->getValues([
'isunited',
'turnterm',
'block_change_scout',
'refreshLimit',
]);
}
if (($request['compactOutput'] ?? false) === true) {
$observedGeneralTurns = [];
foreach (comparisonIntegerList($request['observe']['generalIds'] ?? [], 'generalIds') as $generalId) {
$observedGeneralTurns[(string)$generalId] = iterator_to_array(
$db->query(
'SELECT turn_idx AS turnIndex, action, arg FROM general_turn WHERE general_id = %i ORDER BY turn_idx',
$generalId,
),
);
}
$response = [
'schemaVersion' => 1,
'engine' => 'ref',
'action' => $actionName,
'afterDetails' => $afterDetails,
'createdGeneralCount' => count($createdGenerals),
'firstCreatedGenerals' => array_slice($createdGenerals, 0, 3),
'createdNations' => $response['createdNations'] ?? [],
'createdEvents' => $response['createdEvents'] ?? [],
'createdDiplomacy' => $response['createdDiplomacy'] ?? [],
'gameEnvironment' => $response['gameEnvironment'] ?? [],
'logs' => $after['logs'],
'observedGeneralTurns' => $observedGeneralTurns,
'remainingEventIds' => array_map(
'intval',
$db->queryFirstColumn('SELECT id FROM event ORDER BY id'),
),
];
}
echo json_encode(
$response,
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
} catch (\Throwable $throwable) {
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
exit(1);
}
}
comparisonMonthlyEventTraceMain();
-153
View File
@@ -1,153 +0,0 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli' || getenv('REF_DETERMINISTIC_INSTALL_ENABLED') !== '1') {
http_response_code(404);
exit(1);
}
chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';
/** @return mixed */
function monthlySnapshotJson(mixed $value): mixed
{
if (!is_string($value) || $value === '') {
return $value;
}
try {
return json_decode($value, true, flags: JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return $value;
}
}
/** @return array<string, mixed> */
function monthlySnapshotPick(array $row, array $mapping, array $jsonColumns = []): array
{
$result = [];
foreach ($mapping as $target => $source) {
$value = $row[$source] ?? null;
$result[$target] = in_array($source, $jsonColumns, true)
? monthlySnapshotJson($value)
: $value;
}
return $result;
}
$db = DB::db();
$game = KVStorage::getStorage($db, 'game_env');
$game->resetCache();
$clock = GameClock::fromStorage($game);
$world = $game->getValues([
'scenario', 'year', 'month', 'turnterm', 'turntime', 'starttime',
'init_year', 'init_month', 'develcost', 'killturn', 'isunited',
]);
$world['turntime'] = $clock->formatTick(Util::toInt($world['turntime']), true);
$world['starttime'] = $clock->formatTick(Util::toInt($world['starttime']), true);
$generals = [];
foreach ($db->query('SELECT * FROM general ORDER BY no') as $row) {
$general = monthlySnapshotPick($row, [
'id' => 'no', 'name' => 'name', 'nationId' => 'nation', 'cityId' => 'city',
'troopId' => 'troop', 'leadership' => 'leadership', 'strength' => 'strength',
'intelligence' => 'intel', 'experience' => 'experience', 'experienceLevel' => 'explevel',
'dedication' => 'dedication', 'dedicationLevel' => 'dedlevel',
'leadershipExp' => 'leadership_exp', 'strengthExp' => 'strength_exp', 'intelligenceExp' => 'intel_exp',
'officerLevel' => 'officer_level', 'injury' => 'injury', 'gold' => 'gold',
'rice' => 'rice', 'crew' => 'crew', 'crewTypeId' => 'crewtype', 'train' => 'train',
'atmos' => 'atmos', 'age' => 'age', 'npcState' => 'npc', 'turnTime' => 'turntime',
'recentWarTime' => 'recent_war', 'lastTurn' => 'last_turn', 'meta' => 'aux',
'affinity' => 'affinity', 'birthYear' => 'bornyear', 'deathYear' => 'deadyear',
'personality' => 'personal', 'specialDomestic' => 'special', 'specialWar' => 'special2',
'horse' => 'horse', 'weapon' => 'weapon', 'book' => 'book', 'item' => 'item',
'specAge' => 'specage', 'specAge2' => 'specage2',
'dex1' => 'dex1', 'dex2' => 'dex2', 'dex3' => 'dex3', 'dex4' => 'dex4', 'dex5' => 'dex5',
'killTurn' => 'killturn',
], ['last_turn', 'aux']);
foreach (['id', 'nationId', 'cityId', 'troopId', 'leadership', 'strength', 'intelligence',
'experience', 'experienceLevel', 'dedication', 'dedicationLevel', 'leadershipExp',
'strengthExp', 'intelligenceExp', 'officerLevel', 'injury', 'gold', 'rice', 'crew',
'crewTypeId', 'train', 'atmos', 'age', 'npcState', 'dex1', 'dex2', 'dex3',
'dex4', 'dex5', 'killTurn', 'affinity', 'birthYear', 'deathYear', 'specAge', 'specAge2'] as $key) {
$general[$key] = (int)$general[$key];
}
$general['turnTick'] = Util::toInt($general['turnTime']);
$general['turnTime'] = $clock->formatTick($general['turnTick'], true);
$general['recentWarTick'] = $general['recentWarTime'] === null
? null
: Util::toInt($general['recentWarTime']);
$general['recentWarTime'] = $general['recentWarTime'] === null
? null
: $clock->formatTick(Util::toInt($general['recentWarTime']), true);
$generals[] = $general;
}
$cities = [];
foreach ($db->query('SELECT * FROM city ORDER BY city') as $row) {
$city = monthlySnapshotPick($row, [
'id' => 'city', 'name' => 'name', 'nationId' => 'nation', 'level' => 'level',
'population' => 'pop', 'populationMax' => 'pop_max', 'agriculture' => 'agri',
'agricultureMax' => 'agri_max', 'commerce' => 'comm', 'commerceMax' => 'comm_max',
'security' => 'secu', 'securityMax' => 'secu_max', 'supplyState' => 'supply',
'frontState' => 'front', 'defence' => 'def', 'defenceMax' => 'def_max',
'wall' => 'wall', 'wallMax' => 'wall_max', 'state' => 'state', 'term' => 'term',
'trust' => 'trust', 'trade' => 'trade', 'conflict' => 'conflict',
], ['conflict']);
foreach (array_keys($city) as $key) {
if ($key !== 'name' && $key !== 'conflict' && $key !== 'trust' && $key !== 'trade') {
$city[$key] = (int)$city[$key];
}
}
$city['trade'] = $city['trade'] === null ? null : (int)$city['trade'];
$cities[] = $city;
}
$nations = [];
$lastAttackableByNation = KVStorage::getValuesFromInterNamespace($db, 'nation_env', 'last_attackable');
$lastCapitalMoveTrialByNation = KVStorage::getValuesFromInterNamespace($db, 'nation_env', 'last천도Trial');
foreach ($db->query('SELECT *, CAST(tech AS DOUBLE) AS tech_raw FROM nation ORDER BY nation') as $row) {
$nation = monthlySnapshotPick($row, [
'id' => 'nation', 'name' => 'name', 'color' => 'color', 'capitalCityId' => 'capital',
'gold' => 'gold', 'rice' => 'rice', 'tech' => 'tech', 'level' => 'level',
'typeCode' => 'type', 'generalCount' => 'gennum', 'power' => 'power',
'war' => 'war', 'diplomacyLimit' => 'surlimit', 'capitalRevision' => 'capset',
'strategicCommandLimit' => 'strategic_cmd_limit', 'rate' => 'rate',
'rateTmp' => 'rate_tmp', 'bill' => 'bill', 'meta' => 'aux',
], ['aux']);
foreach (['id', 'capitalCityId', 'gold', 'rice', 'level', 'generalCount',
'power', 'war', 'diplomacyLimit', 'capitalRevision', 'strategicCommandLimit',
'rate', 'rateTmp', 'bill'] as $key) {
$nation[$key] = (int)$nation[$key];
}
$nation['tech'] = (float)$nation['tech'];
$nation['techRaw'] = (float)$row['tech_raw'];
$nation['lastAttackable'] = (int)($lastAttackableByNation[$nation['id']] ?? 0);
$lastCapitalMoveTrial = $lastCapitalMoveTrialByNation[$nation['id']] ?? null;
$nation['lastCapitalMoveTrial'] = is_array($lastCapitalMoveTrial)
? [(int)($lastCapitalMoveTrial[0] ?? 0), (int)($lastCapitalMoveTrial[1] ?? 0)]
: null;
$nations[] = $nation;
}
$diplomacy = [];
foreach ($db->query('SELECT me, you, state, term, dead FROM diplomacy ORDER BY me, you') as $row) {
$diplomacy[] = [
'fromNationId' => (int)$row['me'], 'toNationId' => (int)$row['you'],
'state' => (int)$row['state'], 'term' => (int)$row['term'], 'dead' => (int)$row['dead'],
];
}
echo json_encode([
'schemaVersion' => 1,
'engine' => 'ref',
'world' => $world,
'generals' => $generals,
'cities' => $cities,
'nations' => $nations,
'diplomacy' => $diplomacy,
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION), PHP_EOL;
@@ -1,312 +0,0 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli' || getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
http_response_code(404);
fwrite(STDERR, "NPC possession comparison is disabled.\n");
exit(64);
}
chdir(dirname(__DIR__));
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
error_reporting(E_ALL & ~E_DEPRECATED);
require_once 'lib.php';
require_once 'func.php';
final class ComparisonNpcTracingRandUtil extends RandUtil
{
/** @var list<float> */
public array $floatDraws = [];
public function nextFloat1(): float
{
$value = parent::nextFloat1();
$this->floatDraws[] = $value;
return $value;
}
}
/** @return array<string, mixed> */
function comparisonNpcRequest(): array
{
$request = json_decode(stream_get_contents(STDIN), true, flags: JSON_THROW_ON_ERROR);
if (!is_array($request)) {
throw new \InvalidArgumentException('fixture must be a JSON object');
}
return $request;
}
/** @return list<int> */
function comparisonNpcIntegerList(mixed $value, string $name): array
{
if (!is_array($value)) {
throw new \InvalidArgumentException("{$name} must be an array");
}
$result = [];
foreach ($value as $item) {
if (!is_int($item) || $item <= 0) {
throw new \InvalidArgumentException("{$name} must contain positive integers");
}
$result[] = $item;
}
return $result;
}
/** @return array<int, int> */
function comparisonNpcPreviousPick(mixed $value): array
{
if ($value === null) {
return [];
}
if (!is_array($value)) {
throw new \InvalidArgumentException('previousPick must be an array');
}
$result = [];
foreach ($value as $item) {
if (
!is_array($item)
|| !isset($item['id'], $item['keepCount'])
|| !is_int($item['id'])
|| $item['id'] <= 0
|| !is_int($item['keepCount'])
|| $item['keepCount'] < 0
) {
throw new \InvalidArgumentException('previousPick entries require positive id and non-negative keepCount');
}
$result[$item['id']] = $item['keepCount'];
}
return $result;
}
/** @return list<array{id: int, leadership: int, strength: int, intel: int}> */
function comparisonNpcCandidateRows(): array
{
return array_map(
static fn(array $row): array => [
'id' => (int)$row['no'],
'leadership' => (int)$row['leadership'],
'strength' => (int)$row['strength'],
'intel' => (int)$row['intel'],
],
DB::db()->query('SELECT `no`, leadership, strength, intel FROM general WHERE npc=2'),
);
}
/** @return array<string, mixed> */
function comparisonNpcState(): array
{
return [
'candidates' => comparisonNpcCandidateRows(),
'tokens' => DB::db()->query(
'SELECT owner, valid_until, pick_more_from, pick_result, nonce FROM select_npc_token ORDER BY id',
),
];
}
/**
* @param array<string, mixed> $case
* @return array<string, mixed>
*/
function comparisonNpcRunCase(array $case, string|int $hiddenSeed, int $owner, int $now, GameClock $clock): array
{
$db = DB::db();
$db->delete('select_npc_token', '1=1');
$insertReserved = static function (int $reservedOwner, array $reservedIds, int $validUntil) use ($db, $now): void {
if ($reservedIds === []) {
return;
}
$reservedPick = [];
foreach ($reservedIds as $id) {
$reservedPick[$id] = ['keepCnt' => 3];
}
$db->insert('select_npc_token', [
'owner' => $reservedOwner,
'valid_until' => $validUntil,
'pick_more_from' => $now,
'pick_result' => json_encode((object)$reservedPick, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
'nonce' => 1,
]);
};
$reservedIds = comparisonNpcIntegerList($case['reservedIds'] ?? [], 'reservedIds');
$boundaryReservedIds = comparisonNpcIntegerList(
$case['boundaryReservedIds'] ?? [],
'boundaryReservedIds',
);
$expiredReservedIds = comparisonNpcIntegerList(
$case['expiredReservedIds'] ?? [],
'expiredReservedIds',
);
$expiredAt = $now - $clock->ticksFromSeconds(1);
$insertReserved($owner + 1, $reservedIds, GameClock::MAX_SAFE_TICK);
$insertReserved($owner + 2, $boundaryReservedIds, $now);
$insertReserved($owner + 3, $expiredReservedIds, $expiredAt);
$candidates = [];
$weights = [];
foreach (comparisonNpcCandidateRows() as $row) {
$id = $row['id'];
$candidates[$id] = $row + ['keepCnt' => 3];
$weights[$id] = NpcPossessionSelector::weight($row);
}
NpcPossessionSelector::removeReserved(
$candidates,
$weights,
$db->queryFirstColumn(
'SELECT pick_result FROM select_npc_token WHERE `owner`!=%i AND valid_until >=%i',
$owner,
$now,
),
);
$previousPick = comparisonNpcPreviousPick($case['previousPick'] ?? null);
$keepIds = comparisonNpcIntegerList($case['keepIds'] ?? [], 'keepIds');
$oldPick = [];
foreach ($previousPick as $id => $keepCount) {
if (!isset($candidates[$id])) {
throw new \InvalidArgumentException("previous candidate {$id} is not selectable");
}
$oldPick[$id] = $candidates[$id];
$oldPick[$id]['keepCnt'] = $keepCount;
}
$hasPreviousToken = ($case['hasPreviousToken'] ?? false) === true || $oldPick !== [];
if ($hasPreviousToken) {
$db->insert('select_npc_token', [
'owner' => $owner,
'valid_until' => GameClock::MAX_SAFE_TICK,
'pick_more_from' => $now,
'pick_result' => json_encode((object)$oldPick, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
'nonce' => 2,
]);
}
$before = comparisonNpcState();
$keepSelection = NpcPossessionSelector::applyKeep($oldPick, $keepIds);
$picked = $keepSelection['picked'];
if ($hasPreviousToken && $keepSelection['cancelled']) {
return [
'name' => (string)($case['name'] ?? ''),
'cancelled' => true,
'seed' => null,
'candidateOrder' => array_keys($candidates),
'randomDraws' => [],
'draws' => [],
'pick' => array_map(
static fn(int $id, array $candidate): array => [
'id' => $id,
'keepCount' => (int)$candidate['keepCnt'],
],
array_keys($oldPick),
array_values($oldPick),
),
'selectionStateUnchanged' => comparisonNpcState() === $before,
];
}
$seed = NpcPossessionSelector::buildSeed($hiddenSeed, $owner, $now);
$rng = new ComparisonNpcTracingRandUtil(new LiteHashDRBG($seed));
$draws = [];
$picked = NpcPossessionSelector::select(
$candidates,
$weights,
$picked,
$rng,
static function (int|string $id) use (&$draws): void {
$draws[] = (int)$id;
},
);
return [
'name' => (string)($case['name'] ?? ''),
'cancelled' => false,
'seed' => $seed,
'candidateOrder' => array_keys($candidates),
'randomDraws' => $rng->floatDraws,
'draws' => $draws,
'pick' => array_map(
static fn(int $id, array $candidate): array => [
'id' => $id,
'keepCount' => (int)$candidate['keepCnt'],
],
array_keys($picked),
array_values($picked),
),
'selectionStateUnchanged' => comparisonNpcState() === $before,
];
}
$request = comparisonNpcRequest();
$hiddenSeed = $request['hiddenSeed'] ?? null;
$owner = $request['owner'] ?? null;
$now = $request['now'] ?? null;
$candidateFixtures = $request['candidates'] ?? null;
$cases = $request['cases'] ?? null;
if (
(!is_string($hiddenSeed) && !is_int($hiddenSeed))
|| !is_int($owner)
|| $owner <= 0
|| !is_string($now)
|| !is_array($candidateFixtures)
|| !is_array($cases)
) {
throw new \InvalidArgumentException('fixture requires hiddenSeed, positive owner, now, candidates and cases');
}
$date = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', $now);
if ($date === false || $date->format('Y-m-d H:i:s') !== $now) {
throw new \InvalidArgumentException('now must use Y-m-d H:i:s');
}
$db = DB::db();
$gameStorage = KVStorage::getStorage($db, 'game_env');
$clock = GameClock::fromStorage($gameStorage);
$nowTick = $clock->dateTimeToTick($date);
$db->update('general', ['npc' => 1], '1=1');
$fixtureIds = [];
foreach ($candidateFixtures as $candidate) {
if (
!is_array($candidate)
|| !isset($candidate['id'], $candidate['leadership'], $candidate['strength'], $candidate['intel'])
|| !is_int($candidate['id'])
|| $candidate['id'] <= 0
|| !is_int($candidate['leadership'])
|| !is_int($candidate['strength'])
|| !is_int($candidate['intel'])
) {
throw new \InvalidArgumentException('candidate entries require integer id and stats');
}
if ($db->queryFirstField('SELECT `no` FROM general WHERE `no`=%i', $candidate['id']) === null) {
throw new \RuntimeException("fixture general {$candidate['id']} does not exist");
}
$db->update(
'general',
[
'npc' => 2,
'leadership' => $candidate['leadership'],
'strength' => $candidate['strength'],
'intel' => $candidate['intel'],
],
'`no`=%i',
$candidate['id'],
);
$fixtureIds[] = $candidate['id'];
}
$results = [];
foreach ($cases as $case) {
if (!is_array($case)) {
throw new \InvalidArgumentException('case must be an object');
}
$results[] = comparisonNpcRunCase($case, $hiddenSeed, $owner, $nowTick, $clock);
}
echo json_encode(
[
'fixtureGeneralIds' => $fixtureIds,
'observedSqlOrder' => array_column(comparisonNpcCandidateRows(), 'id'),
'cases' => $results,
],
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT,
);
echo PHP_EOL;
@@ -1,95 +0,0 @@
<?php
declare(strict_types=1);
use sammo\DB;
use sammo\Event\Action\OpenNationBetting;
use sammo\GameClock;
use sammo\Json;
use sammo\KVStorage;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit(1);
}
chdir(dirname(__DIR__));
require 'lib.php';
require 'func.php';
$db = DB::db();
$game = KVStorage::getStorage($db, 'game_env')->getAll();
$contract = [
'scenario' => (int)($game['scenario'] ?? 0),
'year' => (int)($game['year'] ?? 0),
'month' => (int)($game['month'] ?? 0),
'turnterm' => (int)($game['turnterm'] ?? 0),
'npcmode' => (int)($game['npcmode'] ?? 0),
];
if ($contract !== ['scenario' => 2601, 'year' => 186, 'month' => 1, 'turnterm' => 10, 'npcmode' => 2]) {
throw new RuntimeException('Ref GUI parity conditional fixture requires scenario2601 at 186-01.');
}
$nationBettingCount = (int)$db->queryFirstField(
"SELECT COUNT(*) FROM storage
WHERE namespace = 'betting'
AND JSON_UNQUOTE(JSON_EXTRACT(value, '$.type')) = 'bettingNation'"
);
if ($nationBettingCount === 0) {
(new OpenNationBetting(4, 5000))->run($game);
}
$uniqueCount = (int)$db->queryFirstField(
"SELECT COUNT(*) FROM ng_auction WHERE type = 'uniqueItem' AND finished = 0"
);
if ($uniqueCount === 0) {
$generalID = (int)$db->queryFirstField('SELECT no FROM general WHERE npc <= 2 ORDER BY no LIMIT 1');
if ($generalID <= 0) {
throw new RuntimeException('Ref GUI parity fixture requires at least one general.');
}
$gameStorage = KVStorage::getStorage($db, 'game_env');
$clock = GameClock::fromStorage($gameStorage);
$openTick = $clock->nowTick();
$closeTick = GameClock::addTicks($openTick, $clock->ticksFromMinutes(24 * 60));
$latestBidCloseTick = GameClock::addTicks($openTick, $clock->ticksFromMinutes(2 * 24 * 60));
$openDate = $clock->tickToDateTime($openTick);
$detail = [
'title' => '손자병법 경매',
'hostName' => '청룡',
'amount' => 1,
'isReverse' => false,
'startBidAmount' => 1000,
'finishBidAmount' => null,
'remainCloseDateExtensionCnt' => 1,
'availableLatestBidCloseTick' => $latestBidCloseTick,
];
$db->insert('ng_auction', [
'type' => 'uniqueItem',
'finished' => 0,
'target' => 'che_서적_15_손자병법',
'host_general_id' => $generalID,
'req_resource' => 'inheritPoint',
'open_tick' => $openTick,
'close_tick' => $closeTick,
'detail' => Json::encode($detail),
]);
$auctionID = (int)$db->insertId();
$db->insert('ng_auction_bid', [
'auction_id' => $auctionID,
'owner' => null,
'general_id' => $generalID,
'amount' => 1000,
'date' => $openDate->format('Y-m-d H:i:s'),
'aux' => Json::encode(['generalName' => '청룡']),
]);
}
printf(
"Ref conditional capture fixtures: nation_betting=%d unique_auction=%d\n",
(int)$db->queryFirstField(
"SELECT COUNT(*) FROM storage
WHERE namespace = 'betting'
AND JSON_UNQUOTE(JSON_EXTRACT(value, '$.type')) = 'bettingNation'"
),
(int)$db->queryFirstField("SELECT COUNT(*) FROM ng_auction WHERE type = 'uniqueItem'")
);
-40
View File
@@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace sammo;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
chdir(dirname(__DIR__));
require_once 'lib.php';
require_once 'func.php';
$groups = [
'nation' => ['ActionNationType', 'ActionNationType'],
'domestic' => ['ActionSpecialDomestic', 'ActionSpecialDomestic'],
'war' => ['ActionSpecialWar', 'ActionSpecialWar'],
'personality' => ['ActionPersonality', 'ActionPersonality'],
];
$result = [];
foreach ($groups as $kind => [$directory, $namespace]) {
foreach (glob(__DIR__ . "/../sammo/{$directory}/*.php") ?: [] as $path) {
$key = basename($path, '.php');
if ($key === 'None' || str_starts_with($key, 'che_event_') || $key === 'che_거상') {
continue;
}
$className = "\\sammo\\{$namespace}\\{$key}";
$trait = new $className();
$result[$kind][$key] = [
'name' => $trait->getName(),
'info' => $trait->getInfo(),
];
}
ksort($result[$kind]);
}
echo Json::encode($result);
-887
View File
@@ -1,887 +0,0 @@
<?php
declare(strict_types=1);
namespace sammo;
use sammo\Enums\RankColumn;
if (PHP_SAPI !== 'cli') {
http_response_code(404);
exit;
}
chdir(dirname(__DIR__));
$_SERVER['REMOTE_ADDR'] ??= '127.0.0.1';
error_reporting(E_ALL & ~E_DEPRECATED);
require_once 'lib.php';
require_once 'func.php';
require_once __DIR__ . '/turn_state_snapshot.php';
final class TurnComparisonTracingRNG implements RNG
{
private int $sequence = 0;
/** @var list<array<string, mixed>> */
public array $calls = [];
public function __construct(private readonly RNG $inner)
{
}
public static function getMaxInt(): int
{
return LiteHashDRBG::getMaxInt();
}
public function nextBytes(int $bytes): string
{
$value = $this->inner->nextBytes($bytes);
$this->record('nextBytes', ['bytes' => $bytes], bin2hex($value));
return $value;
}
public function nextBits(int $bits): string
{
$value = $this->inner->nextBits($bits);
$this->record('nextBits', ['bits' => $bits], bin2hex($value));
return $value;
}
public function nextInt(?int $max = null): int
{
$value = $this->inner->nextInt($max);
$this->record('nextInt', ['maxInclusive' => $max], $value);
return $value;
}
public function nextFloat1(): float
{
$value = $this->inner->nextFloat1();
$this->record('nextFloat1', [], $value);
return $value;
}
private function record(string $operation, array $arguments, mixed $result): void
{
$this->calls[] = [
'seq' => $this->sequence++,
'operation' => $operation,
'arguments' => $arguments === [] ? (object)[] : $arguments,
'result' => $result,
];
}
}
/** @return array<string, mixed> */
function comparisonMappedPatch(array $row, array $mapping): array
{
$patch = [];
foreach ($mapping as $canonical => $legacy) {
if (array_key_exists($canonical, $row)) {
$patch[$legacy] = $row[$canonical];
}
}
return $patch;
}
function comparisonApplyTurnFixtureSetup(mixed $setup): void
{
if ($setup === null) {
return;
}
if (!is_array($setup)) {
throw new \InvalidArgumentException('setup must be an object');
}
$db = DB::db();
$world = $setup['world'] ?? null;
if ($world !== null) {
if (!is_array($world)) {
throw new \InvalidArgumentException('setup.world must be an object');
}
$game = KVStorage::getStorage($db, 'game_env');
foreach (['year', 'month', 'startyear', 'init_year', 'init_month'] as $key) {
$fixtureKey = match ($key) {
'startyear' => 'startYear',
'init_year' => 'initYear',
'init_month' => 'initMonth',
default => $key,
};
if (!array_key_exists($fixtureKey, $world)) {
continue;
}
$value = $world[$fixtureKey];
if (
!is_int($value)
|| $value < 1
|| (in_array($key, ['month', 'init_month'], true) && $value > 12)
) {
throw new \InvalidArgumentException("setup.world.{$fixtureKey} is invalid");
}
$game->setValue($key, $value);
}
$game->resetCache();
if (array_key_exists('hiddenSeed', $world)) {
$hiddenSeed = $world['hiddenSeed'];
if (!is_string($hiddenSeed) || $hiddenSeed === '' || strlen($hiddenSeed) > 256) {
throw new \InvalidArgumentException('setup.world.hiddenSeed is invalid');
}
UniqueConst::$hiddenSeed = $hiddenSeed;
}
// 비교 fixture는 전역 상태에 의존하지 않도록 effect 기본값을 null로 고정하고,
// 명시된 경우에만 해당 effect를 활성화합니다.
GameConst::$scenarioEffect = null;
if (array_key_exists('scenarioEffect', $world)) {
$scenarioEffect = $world['scenarioEffect'];
if ($scenarioEffect === '' || $scenarioEffect === 'None') {
$scenarioEffect = null;
}
if ($scenarioEffect !== null) {
if (!is_string($scenarioEffect) || getScenarioEffectClass($scenarioEffect) === null) {
throw new \InvalidArgumentException('setup.world.scenarioEffect is invalid');
}
}
GameConst::$scenarioEffect = $scenarioEffect;
}
if (array_key_exists('staticEventHandlers', $world)) {
$handlersByEvent = $world['staticEventHandlers'];
if (!is_array($handlersByEvent)) {
throw new \InvalidArgumentException('setup.world.staticEventHandlers must be an object');
}
foreach ($handlersByEvent as $eventType => $handlers) {
if (!is_string($eventType) || $eventType === '' || !is_array($handlers)) {
throw new \InvalidArgumentException('setup.world.staticEventHandlers is invalid');
}
foreach ($handlers as $handler) {
if (!is_string($handler) || $handler === '') {
throw new \InvalidArgumentException('setup.world.staticEventHandlers is invalid');
}
}
}
GameConst::$staticEventHandlers = $handlersByEvent;
}
}
foreach (['nations', 'cities', 'generals', 'troops', 'diplomacy', 'generalTurns', 'nationTurns'] as $collection) {
if (isset($setup[$collection]) && !is_array($setup[$collection])) {
throw new \InvalidArgumentException("setup.{$collection} must be an array");
}
}
$generalCooldowns = comparisonGeneralCooldownSelectors(
$setup['generalCooldowns'] ?? [],
'setup.generalCooldowns',
);
if ($generalCooldowns !== []) {
$nextExecuteStorage = KVStorage::getStorage($db, 'next_execute');
foreach ($setup['generalCooldowns'] as $entry) {
$nextAvailableTurn = $entry['nextAvailableTurn'] ?? null;
if (!is_int($nextAvailableTurn) || $nextAvailableTurn < 0) {
throw new \InvalidArgumentException(
'setup.generalCooldowns entries require a non-negative nextAvailableTurn',
);
}
$key = "next_execute_{$entry['generalId']}_{$entry['actionName']}";
$nextExecuteStorage->setValue($key, $nextAvailableTurn);
}
$nextExecuteStorage->resetCache();
}
if (isset($setup['randomFoundingCandidateCityIds'])) {
$candidateCityIds = comparisonIntegerList(
$setup['randomFoundingCandidateCityIds'],
'randomFoundingCandidateCityIds',
);
$db->update('city', ['level' => 4], '1 = 1');
if ($candidateCityIds !== []) {
$db->update('city', ['level' => 5], 'city IN %li', $candidateCityIds);
}
}
if (($setup['isolateWorld'] ?? false) === true) {
$generalIds = array_values(array_map(
static fn(array $row): int => (int)$row['id'],
$setup['generals'] ?? [],
));
$nationIds = array_values(array_map(
static fn(array $row): int => (int)$row['id'],
$setup['nations'] ?? [],
));
if ($generalIds === [] || $nationIds === []) {
throw new \InvalidArgumentException('isolateWorld requires generals and nations');
}
$generalTemplate = $db->queryFirstRow(
'SELECT * FROM general WHERE no IN %li ORDER BY no LIMIT 1',
$generalIds,
);
if ($generalTemplate === null) {
throw new \InvalidArgumentException('isolateWorld requires at least one existing fixture general');
}
$generalTemplateId = (int)$generalTemplate['no'];
$rankDataTemplate = $db->query(
'SELECT `type`, `value` FROM rank_data WHERE general_id = %i',
$generalTemplateId,
);
$db->delete('general_turn', 'general_id NOT IN %li', $generalIds);
$db->delete('rank_data', 'general_id NOT IN %li', $generalIds);
$db->delete('nation_turn', 'nation_id NOT IN %li', $nationIds);
$db->delete('troop', '1 = 1');
$db->delete('diplomacy', '1 = 1');
$db->delete('general', 'no NOT IN %li', $generalIds);
foreach ($generalIds as $generalId) {
if ($db->queryFirstField('SELECT no FROM general WHERE no = %i', $generalId) !== null) {
continue;
}
$newGeneral = $generalTemplate;
$newGeneral['no'] = $generalId;
$newGeneral['owner'] = 0;
$newGeneral['name'] = "fixture-general-{$generalId}";
$db->insert('general', $newGeneral);
}
foreach ($generalIds as $generalId) {
foreach ($rankDataTemplate as $rankRow) {
$rankData = [
'general_id' => $generalId,
'type' => $rankRow['type'],
'value' => $rankRow['value'],
];
$db->insertUpdate('rank_data', $rankData, $rankData);
}
}
$db->delete('nation', 'nation NOT IN %li', $nationIds);
$db->update('city', [
'nation' => 0,
'supply' => 0,
'front' => 0,
'state' => 0,
'term' => 0,
'conflict' => '{}',
'officer_set' => 0,
], '1 = 1');
}
foreach ($setup['nations'] ?? [] as $row) {
if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
throw new \InvalidArgumentException('setup.nations requires positive integer ids');
}
$data = comparisonMappedPatch($row, [
'id' => 'nation',
'name' => 'name',
'color' => 'color',
'capitalCityId' => 'capital',
'gold' => 'gold',
'rice' => 'rice',
'tech' => 'tech',
'level' => 'level',
'typeCode' => 'type',
'war' => 'war',
'diplomacyLimit' => 'surlimit',
'generalCount' => 'gennum',
'power' => 'power',
'capitalRevision' => 'capset',
'strategicCommandLimit' => 'strategic_cmd_limit',
]);
if (isset($row['meta'])) {
$data['aux'] = Json::encode($row['meta']);
}
$data += [
'name' => "fixture-nation-{$row['id']}",
'color' => '#777777',
'capital' => 0,
'gold' => 0,
'rice' => 0,
'level' => 1,
'type' => 'che_중립',
'aux' => '{}',
];
$db->insertUpdate('nation', $data, $data);
$nationStor = KVStorage::getStorage($db, $row['id'], 'nation_env');
if (($setup['isolateWorld'] ?? false) === true) {
$nationStor->deleteValue('recv_assist');
$nationStor->deleteValue('resp_assist');
}
if (array_key_exists('nationEnv', $row)) {
if (!is_array($row['nationEnv'])) {
throw new \InvalidArgumentException('setup.nations.nationEnv must be an object');
}
foreach ($row['nationEnv'] as $key => $value) {
if (!is_string($key) || $key === '') {
throw new \InvalidArgumentException('setup.nations.nationEnv keys must be non-empty strings');
}
$nationStor->setValue($key, $value);
}
$nationStor->resetCache();
}
if (array_key_exists('turnLastByOfficerLevel', $row)) {
$turnLastByOfficerLevel = $row['turnLastByOfficerLevel'];
if (!is_array($turnLastByOfficerLevel)) {
throw new \InvalidArgumentException('setup.nations.turnLastByOfficerLevel must be an object');
}
foreach ($turnLastByOfficerLevel as $officerLevel => $lastTurn) {
$officerLevel = filter_var($officerLevel, FILTER_VALIDATE_INT);
if (
$officerLevel === false
|| $officerLevel < 5
|| $officerLevel > 12
|| !is_array($lastTurn)
) {
throw new \InvalidArgumentException(
'setup.nations.turnLastByOfficerLevel requires officer levels 5 through 12'
);
}
$nationStor->setValue("turn_last_{$officerLevel}", $lastTurn);
}
$nationStor->resetCache();
}
}
if (($setup['isolateWorld'] ?? false) === true && ($setup['nations'] ?? []) !== []) {
$maxNationId = max(array_map(
static fn(array $row): int => (int)$row['id'],
$setup['nations'],
));
$db->query('ALTER TABLE nation AUTO_INCREMENT = %i', $maxNationId + 1);
}
foreach ($setup['cities'] ?? [] as $row) {
if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
throw new \InvalidArgumentException('setup.cities requires positive integer ids');
}
$patch = comparisonMappedPatch($row, [
'nationId' => 'nation',
'level' => 'level',
'population' => 'pop',
'populationMax' => 'pop_max',
'agriculture' => 'agri',
'agricultureMax' => 'agri_max',
'commerce' => 'comm',
'commerceMax' => 'comm_max',
'security' => 'secu',
'securityMax' => 'secu_max',
'supplyState' => 'supply',
'frontState' => 'front',
'defence' => 'def',
'defenceMax' => 'def_max',
'wall' => 'wall',
'wallMax' => 'wall_max',
'state' => 'state',
'term' => 'term',
'trust' => 'trust',
'trade' => 'trade',
'officerSet' => 'officer_set',
]);
if (array_key_exists('conflictEntries', $row)) {
if (!is_array($row['conflictEntries'])) {
throw new \InvalidArgumentException('setup.cities.conflictEntries must be an array');
}
$orderedConflict = [];
foreach ($row['conflictEntries'] as $entry) {
if (
!is_array($entry)
|| count($entry) !== 2
|| !is_int($entry[0] ?? null)
|| $entry[0] < 1
|| !is_numeric($entry[1] ?? null)
) {
throw new \InvalidArgumentException(
'setup.cities.conflictEntries requires [positive nation id, numeric value] pairs'
);
}
$orderedConflict[$entry[0]] = $entry[1];
}
$patch['conflict'] = Json::encode($orderedConflict);
} elseif (array_key_exists('conflict', $row)) {
if (!is_array($row['conflict'])) {
throw new \InvalidArgumentException('setup.cities.conflict must be an object');
}
$patch['conflict'] = Json::encode($row['conflict']);
}
if ($patch !== []) {
$db->update('city', $patch, 'city = %i', $row['id']);
}
}
foreach ($setup['generals'] ?? [] as $row) {
if (!is_array($row) || !is_int($row['id'] ?? null) || $row['id'] < 1) {
throw new \InvalidArgumentException('setup.generals requires positive integer ids');
}
$patch = comparisonMappedPatch($row, [
'name' => 'name',
'nationId' => 'nation',
'cityId' => 'city',
'troopId' => 'troop',
'leadership' => 'leadership',
'strength' => 'strength',
'intelligence' => 'intel',
'leadershipExp' => 'leadership_exp',
'strengthExp' => 'strength_exp',
'intelExp' => 'intel_exp',
'experience' => 'experience',
'dedication' => 'dedication',
'expLevel' => 'explevel',
'officerLevel' => 'officer_level',
'officerCityId' => 'officer_city',
'belong' => 'belong',
'permission' => 'permission',
'betray' => 'betray',
'makeLimit' => 'makelimit',
'injury' => 'injury',
'age' => 'age',
'gold' => 'gold',
'rice' => 'rice',
'crew' => 'crew',
'crewTypeId' => 'crewtype',
'train' => 'train',
'atmos' => 'atmos',
'dex1' => 'dex1',
'dex2' => 'dex2',
'dex3' => 'dex3',
'dex4' => 'dex4',
'dex5' => 'dex5',
'specAge' => 'specage',
'specAge2' => 'specage2',
'killTurn' => 'killturn',
'npcState' => 'npc',
'blockState' => 'block',
'specialDomestic' => 'special',
'specialWar' => 'special2',
'personality' => 'personal',
'itemHorse' => 'horse',
'itemWeapon' => 'weapon',
'itemBook' => 'book',
'itemExtra' => 'item',
]);
if (isset($row['meta'])) {
$patch['aux'] = Json::encode($row['meta']);
}
if (array_key_exists('penalty', $row)) {
if (!is_array($row['penalty'])) {
throw new \InvalidArgumentException('setup.generals.penalty must be an object');
}
$patch['penalty'] = Json::encode($row['penalty']);
}
if (isset($row['lastTurn']) && is_array($row['lastTurn'])) {
$patch['last_turn'] = Json::encode($row['lastTurn']);
}
if ($patch !== []) {
$db->update('general', $patch, 'no = %i', $row['id']);
}
if (array_key_exists('nationId', $row)) {
$db->update('rank_data', [
'nation_id' => $row['nationId'],
], 'general_id = %i', $row['id']);
}
}
foreach ($setup['rankData'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['generalId'] ?? null)
|| !is_string($row['type'] ?? null)
|| RankColumn::tryFrom($row['type']) === null
|| !is_int($row['value'] ?? null)
) {
throw new \InvalidArgumentException('invalid setup.rankData row');
}
$nationId = (int)($db->queryFirstField(
'SELECT nation FROM general WHERE no = %i',
$row['generalId'],
) ?? 0);
$db->insertUpdate('rank_data', [
'general_id' => $row['generalId'],
'nation_id' => $nationId,
'type' => $row['type'],
'value' => $row['value'],
], [
'nation_id' => $nationId,
'value' => $row['value'],
]);
}
if (($setup['isolateWorld'] ?? false) === true && ($setup['generals'] ?? []) !== []) {
$maxGeneralId = max(array_map(
static fn(array $row): int => (int)$row['id'],
$setup['generals'],
));
$db->query('ALTER TABLE general AUTO_INCREMENT = %i', $maxGeneralId + 1);
}
foreach ($setup['troops'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['id'] ?? null)
|| !is_int($row['nationId'] ?? null)
|| !is_string($row['name'] ?? null)
|| $row['id'] < 1
|| $row['nationId'] < 1
|| $row['name'] === ''
) {
throw new \InvalidArgumentException('setup.troops requires positive id/nationId and non-empty name');
}
$data = [
'troop_leader' => $row['id'],
'nation' => $row['nationId'],
'name' => $row['name'],
];
$db->insertUpdate('troop', $data, $data);
}
foreach ($setup['diplomacy'] ?? [] as $row) {
if (
!is_array($row) ||
!is_int($row['fromNationId'] ?? null) ||
!is_int($row['toNationId'] ?? null) ||
$row['fromNationId'] < 1 ||
$row['toNationId'] < 1
) {
throw new \InvalidArgumentException('setup.diplomacy requires positive integer nation ids');
}
$data = comparisonMappedPatch($row, [
'fromNationId' => 'me',
'toNationId' => 'you',
'state' => 'state',
'term' => 'term',
'dead' => 'dead',
]);
$data += ['state' => 3, 'term' => 0, 'dead' => 0];
$db->insertUpdate('diplomacy', $data, $data);
}
foreach ($setup['generalTurns'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['generalId'] ?? null)
|| !is_int($row['turnIndex'] ?? null)
|| !is_string($row['action'] ?? null)
|| $row['generalId'] < 1
|| $row['turnIndex'] < 0
|| $row['turnIndex'] >= GameConst::$maxTurn
|| $row['action'] === ''
) {
throw new \InvalidArgumentException('setup.generalTurns entries are invalid');
}
$data = [
'general_id' => $row['generalId'],
'turn_idx' => $row['turnIndex'],
'action' => $row['action'],
'arg' => Json::encode($row['args'] ?? []),
'brief' => $row['action'],
];
$db->insertUpdate('general_turn', $data, $data);
}
foreach ($setup['nationTurns'] ?? [] as $row) {
if (
!is_array($row)
|| !is_int($row['nationId'] ?? null)
|| !is_int($row['officerLevel'] ?? null)
|| !is_int($row['turnIndex'] ?? null)
|| !is_string($row['action'] ?? null)
|| $row['nationId'] < 1
|| $row['officerLevel'] < 5
|| $row['turnIndex'] < 0
|| $row['turnIndex'] >= GameConst::$maxChiefTurn
|| $row['action'] === ''
) {
throw new \InvalidArgumentException('setup.nationTurns entries are invalid');
}
$data = [
'nation_id' => $row['nationId'],
'officer_level' => $row['officerLevel'],
'turn_idx' => $row['turnIndex'],
'action' => $row['action'],
'arg' => Json::encode($row['args'] ?? []),
'brief' => $row['action'],
];
$db->insertUpdate('nation_turn', $data, $data);
}
}
function comparisonRunTurnCommand(array $request): array
{
if (getenv('TURN_DIFFERENTIAL_ENABLED') !== '1') {
throw new \RuntimeException('TURN_DIFFERENTIAL_ENABLED=1 is required');
}
$kind = $request['kind'] ?? null;
if (!in_array($kind, ['general', 'nation', 'instantNation', 'troopJoinStaticEvent'], true)) {
throw new \InvalidArgumentException(
'kind must be general, nation, instantNation, or troopJoinStaticEvent',
);
}
$actorGeneralId = $request['actorGeneralId'] ?? null;
$action = $request['action'] ?? null;
$args = $request['args'] ?? null;
$includeLifecycle = $request['includeLifecycle'] ?? false;
if (!is_int($actorGeneralId) || $actorGeneralId < 1) {
throw new \InvalidArgumentException('actorGeneralId must be a positive integer');
}
if (!is_string($action) || $action === '') {
throw new \InvalidArgumentException('action must be a non-empty string');
}
if (!is_bool($includeLifecycle)) {
throw new \InvalidArgumentException('includeLifecycle must be a boolean');
}
if ($includeLifecycle && !in_array($kind, ['general', 'nation'], true)) {
throw new \InvalidArgumentException('includeLifecycle supports general and nation commands only');
}
comparisonApplyTurnFixtureSetup($request['setup'] ?? null);
if ($includeLifecycle) {
if ($kind === 'general') {
DB::db()->insertUpdate('general_turn', [
'general_id' => $actorGeneralId,
'turn_idx' => 0,
'action' => $action,
'arg' => Json::encode($args),
'brief' => $action,
], [
'action' => $action,
'arg' => Json::encode($args),
'brief' => $action,
]);
} else {
$actorOfficer = DB::db()->queryFirstRow(
'SELECT nation, officer_level FROM general WHERE no = %i',
$actorGeneralId,
);
if ($actorOfficer === null) {
throw new \RuntimeException('장수 정보를 불러올 수 없습니다.');
}
$actorNationId = (int)$actorOfficer['nation'];
$actorOfficerLevel = (int)$actorOfficer['officer_level'];
if ($actorNationId < 1 || $actorOfficerLevel < 5) {
throw new \InvalidArgumentException('nation lifecycle requires a nation officer');
}
DB::db()->insertUpdate('nation_turn', [
'nation_id' => $actorNationId,
'officer_level' => $actorOfficerLevel,
'turn_idx' => 0,
'action' => $action,
'arg' => Json::encode($args),
'brief' => $action,
], [
'action' => $action,
'arg' => Json::encode($args),
'brief' => $action,
]);
}
}
$snapshotRequest = ['observe' => $request['observe'] ?? []];
$before = comparisonTurnStateSnapshot($snapshotRequest);
$db = DB::db();
$gameStorage = KVStorage::getStorage($db, 'game_env');
$gameStorage->resetCache();
$environment = $gameStorage->getAll();
if ($kind === 'troopJoinStaticEvent') {
if ($action !== \sammo\API\Troop\JoinTroop::class) {
throw new \InvalidArgumentException('troopJoinStaticEvent requires the JoinTroop API action');
}
$troopId = is_array($args) ? ($args['troopID'] ?? null) : null;
if (!is_int($troopId) || $troopId < 1) {
throw new \InvalidArgumentException('troopJoinStaticEvent requires a positive integer troopID');
}
$general = GeneralLite::createObjFromDB(
$actorGeneralId,
['troop'],
Enums\GeneralLiteQueryMode::Lite,
);
if ($general === null) {
throw new \RuntimeException('장수 정보를 불러올 수 없습니다.');
}
if ($general->getVar('troop') != 0) {
throw new \RuntimeException('이미 부대에 소속되어 있습니다.');
}
$nationId = $general->getNationID();
if ($nationId == 0) {
throw new \RuntimeException('국가에 소속되어 있지 않습니다.');
}
$troopExists = $db->queryFirstField(
'SELECT `troop_leader` FROM `troop` WHERE `troop_leader` = %i AND `nation` = %i',
$troopId,
$nationId,
);
if (!$troopExists) {
throw new \RuntimeException('부대가 올바르지 않습니다.');
}
GameConst::$staticEventHandlers = [
\sammo\API\Troop\JoinTroop::class => ['event_부대탑승즉시이동'],
];
$general->setVar('troop', $troopId);
StaticEventHandler::handleEvent($general, null, \sammo\API\Troop\JoinTroop::class, [], $args);
$general->applyDB($db);
$after = comparisonTurnStateSnapshot($snapshotRequest);
return [
'schemaVersion' => 1,
'engine' => 'ref',
'execution' => [
'kind' => $kind,
'actorGeneralId' => $actorGeneralId,
'action' => $action,
'args' => $args,
'seedDomain' => 'none',
'outcome' => ['completed' => true],
],
'before' => $before,
'after' => $after,
'rng' => [],
];
}
$general = General::createObjFromDB($actorGeneralId);
if ($kind === 'instantNation') {
$command = buildNationCommandClass($action, $general, $environment, new LastTurn(), $args);
if (!$command->hasFullConditionMet()) {
throw new \RuntimeException($command->getFailString());
}
$completed = $command->run(NoRNG::rngInstance());
$command->setNextAvailable();
$general->getLogger()->flush();
$after = comparisonTurnStateSnapshot($snapshotRequest);
return [
'schemaVersion' => 1,
'engine' => 'ref',
'execution' => [
'kind' => $kind,
'actorGeneralId' => $actorGeneralId,
'action' => $action,
'args' => $args,
'seedDomain' => 'none',
'outcome' => [
'lastTurn' => (object)[],
'commandName' => $command->getName(),
'completed' => $completed,
],
],
'before' => $before,
'after' => $after,
'rng' => [],
];
}
$turn = new TurnExecutionHelper($general);
$general->increaseInheritancePoint(Enums\InheritanceKey::lived_month, 1);
$preprocessRng = new RandUtil(new LiteHashDRBG(Util::simpleSerialize(
UniqueConst::$hiddenSeed,
'preprocess',
(int)$environment['year'],
(int)$environment['month'],
$actorGeneralId,
)));
$turn->preprocessCommand($preprocessRng, $environment);
$seedDomain = $kind === 'general' ? 'generalCommand' : 'nationCommand';
$seed = Util::simpleSerialize(
UniqueConst::$hiddenSeed,
$seedDomain,
(int)$environment['year'],
(int)$environment['month'],
$actorGeneralId,
$action,
);
$tracingRng = new TurnComparisonTracingRNG(new LiteHashDRBG($seed));
$rng = new RandUtil($tracingRng);
if ($kind === 'general' && $action === 'che_견문') {
\sammo\TextDecoration\SightseeingMessage::setComparisonRng($rng);
}
if ($kind === 'general') {
$previousLastTurn = $general->getLastTurn();
$previousLastTurnRaw = $previousLastTurn->toRaw();
$command = buildGeneralCommandClass($action, $general, $environment, $args);
$commandFullConditionMet = $command->hasFullConditionMet();
$resultTurn = $turn->processCommand($rng, $command, false);
} else {
$nationStor = KVStorage::getStorage($db, $general->getNationID(), 'nation_env');
$lastNationTurnKey = "turn_last_{$general->getVar('officer_level')}";
$previousLastTurn = LastTurn::fromRaw($nationStor->getValue($lastNationTurnKey));
$previousLastTurnRaw = $previousLastTurn->toRaw();
$command = buildNationCommandClass($action, $general, $environment, $previousLastTurn, $args);
$commandFullConditionMet = $command->hasFullConditionMet();
$resultTurn = $turn->processNationCommand($rng, $command);
$nationStor->setValue($lastNationTurnKey, $resultTurn->toRaw());
}
if ($includeLifecycle) {
if ($kind === 'general') {
pullGeneralCommand($general->getID());
} else {
pullNationCommand(
$general->getNationID(),
(int)$general->getVar('officer_level'),
);
}
$general->increaseVarWithLimit(
'myset',
GameConst::$incDefSettingChange,
null,
GameConst::$maxDefSettingChange,
);
$turn->updateTurnTime();
}
$general->getLogger()->flush();
$turn->applyDB();
unset($turn);
$after = comparisonTurnStateSnapshot($snapshotRequest);
$resultTurnRaw = $resultTurn->toRaw();
$resultTerm = (int)($resultTurnRaw['term'] ?? 0);
$preReqTurn = $command->getPreReqTurn();
$acceptScoutCompleted = (
$action === 'che_등용수락'
&& is_array($args)
&& is_int($args['destNationID'] ?? null)
&& $general->getNationID() === $args['destNationID']
);
$foundNationCompleted = (
in_array($action, ['che_건국', 'cr_건국', 'che_무작위건국'], true)
&& is_array($args)
&& is_string($args['nationName'] ?? null)
&& DB::db()->queryFirstField(
'SELECT name FROM nation WHERE nation = %i',
$general->getNationID(),
) === $args['nationName']
);
$completedByState = (
$action === 'che_접경귀환'
&& $tracingRng->calls !== []
) || $acceptScoutCompleted || $foundNationCompleted || (($resultTurnRaw['command'] ?? null) === $command->getName()
&& (
($preReqTurn === 0 && $resultTerm === 0)
|| (
$preReqTurn > 0
&& ($previousLastTurnRaw['command'] ?? null) === $command->getName()
&& ($previousLastTurnRaw['arg'] ?? null) === $command->getArg()
&& (int)($previousLastTurnRaw['term'] ?? 0) === $preReqTurn
)
));
$completed = $commandFullConditionMet && $completedByState;
return [
'schemaVersion' => 1,
'engine' => 'ref',
'execution' => [
'kind' => $kind,
'actorGeneralId' => $actorGeneralId,
'action' => $action,
'args' => $args,
'seedDomain' => $seedDomain,
'outcome' => [
'lastTurn' => $resultTurnRaw,
'commandName' => $command->getName(),
'completed' => $completed,
],
],
'before' => $before,
'after' => $after,
'rng' => $tracingRng->calls,
];
}
try {
$input = stream_get_contents(STDIN);
$request = json_decode($input === '' ? '{}' : $input, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($request)) {
throw new \InvalidArgumentException('request must be an object');
}
echo json_encode(
comparisonRunTurnCommand($request),
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION,
), PHP_EOL;
} catch (\Throwable $throwable) {
fwrite(STDERR, $throwable::class . ': ' . $throwable->getMessage() . PHP_EOL);
exit(1);
}

Some files were not shown because too many files have changed in this diff Show More