Compare commits

...
Author SHA1 Message Date
Hide_D 4de7ebec17 요양 깃수 추가 2026-03-04 14:16:08 +00:00
Hide_D d43dff35fb fix: 디버그 코드 복구 2026-02-18 18:54:46 +00:00
Hide_D 5b13f8bd30 ip 기록 2026-02-17 22:31:08 +09:00
Hide_D e51bc6f711 fix: 턴 데몬에 cookie jar 적용 2026-02-17 20:35:15 +09:00
Hide_D 1b08aa0c58 wip: 디버그 코드 추가 2026-02-17 19:12:28 +09:00
Hide_D f2547fe207 dbg 2026-02-17 18:32:04 +09:00
류화영 cec7726731 fix: 방랑군이 빈번하게 해산하는 문제 해결 시도 2026-01-19 22:50:11 +09:00
Hide_D d679a6fed8 fix: InheritancePointManager 버그 수정 2025-11-01 17:38:02 +00:00
Hide_D 6fe156eb7c fix: composer uninstall 누락 2025-10-29 13:00:46 +00:00
Hide_D 663ddbe76a fix: inheritance_result 작성 문제 2025-10-28 20:24:25 +00:00
Hide_D 30297454f7 refac: inheritance_result로 데이터 이관 2025-10-28 20:15:05 +00:00
Hide_D bc602722bf feat: schema에 inheritance_result 추가
- storage에 의미없이 쌓이는 데이터 이관
2025-10-28 20:09:30 +00:00
Hide_D be8f147216 dep: brandonwamboldt/utilphp 제거 2025-10-28 19:56:02 +00:00
Hide_D b9495fb64a refac: 턴 데몬 전체를 async로 변경경 2025-10-07 20:05:55 +09:00
Hide_D 07b217b290 fix: 서버 오픈 핸들러를 00초에 맞추도록 변경경 2025-10-07 19:54:24 +09:00
Hide_D 42fac46062 refac: brandonwamboldt/utilphp 의존 코드 수정
- 실제 의존성 제거는 이후에에
2025-10-07 19:26:22 +09:00
Hide_D 24bc8985d0 __filename 2025-10-04 11:39:35 +00:00
Hide_D 76a2d5ff63 daemon typescript 버전 추가 2025-10-04 11:29:03 +00:00
Hide_D 148f32f78b fix: execute engine updated 2025-10-04 11:04:36 +00:00
Hide_D 62f5b6597e proc.php 일부 수정
- api.php 미사용 경로 대비
2025-10-04 10:59:56 +00:00
Hide_D 7e810b0279 LogText 끄기 2025-09-07 06:01:04 +00:00
Hide_D 2336b29b65 fix: ?? 2025-08-30 06:15:25 +00:00
Hide_D 185c0e16a0 fix: AI 일부 key 접근 문제 수정 2025-08-30 06:11:37 +00:00
Hide_D 656baeb9ee fix: 토너먼트 분노 공격 확률 오류 수정 (#263)
Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/263
2025-08-23 10:08:13 +09:00
45 changed files with 784 additions and 167 deletions
-1
View File
@@ -12,7 +12,6 @@
}
},
"require": {
"brandonwamboldt/utilphp": "^1.1",
"sergeytsalkov/meekrodb": "^2.5",
"league/plates": "^3.5",
"vlucas/valitron": "^1.4",
Generated
+2 -52
View File
@@ -4,58 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "deed9afdc95bc42f84784df67d295223",
"content-hash": "7edae9b6be35e66ffd0d180f5fac6679",
"packages": [
{
"name": "brandonwamboldt/utilphp",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/brandonwamboldt/utilphp.git",
"reference": "36c32efc4f0679c05163464a550f45c8d83fe683"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/brandonwamboldt/utilphp/zipball/36c32efc4f0679c05163464a550f45c8d83fe683",
"reference": "36c32efc4f0679c05163464a550f45c8d83fe683",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"satooshi/php-coveralls": "dev-master"
},
"type": "library",
"autoload": {
"psr-0": {
"utilphp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brandon Wamboldt",
"email": "brandon.wamboldt@gmail.com"
}
],
"description": "util.php is a collection of useful functions and snippets that you need or could use every day, designed to avoid conflicts with existing projects",
"homepage": "https://github.com/brandonwamboldt/utilphp",
"keywords": [
"collection",
"helpers",
"php",
"utility"
],
"support": {
"issues": "https://github.com/brandonwamboldt/utilphp/issues",
"source": "https://github.com/brandonwamboldt/utilphp/tree/master"
},
"time": "2015-02-02T17:56:14+00:00"
},
{
"name": "brick/math",
"version": "0.12.1",
@@ -5721,5 +5671,5 @@
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}
+1 -1
View File
@@ -31,7 +31,7 @@ $queryMap = [
];
if ($reqQueryType === null || !key_exists($reqQueryType, $queryMap)) {
$reqQueryType = Util::array_first_key($queryMap);
$reqQueryType = array_key_first($queryMap);
}
//로그인 검사
+1 -1
View File
@@ -1047,7 +1047,7 @@ function increaseRefresh($type = "", $cnt = 1)
$logDB = FileDB::db($logPath, $serverPath . '/../f_install/sql/api_log.sql');
$ip = $_SERVER['REMOTE_ADDR'] ?? 'local';
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'local';
$date = date('Y-m-d H:i:s');
$logDB->insert('api_log', [
+1 -1
View File
@@ -559,7 +559,7 @@ function checkStatistic()
$nationHist = '';
foreach (GameConst::$availableNationType as $nationType) {
if (!Util::array_get($nationHists[$nationType])) {
if (!($nationHists[$nationType] ?? null)) {
$nationHists[$nationType] = '-';
}
$nationHist .= getNationType($nationType) . "({$nationHists[$nationType]}), ";
+2 -2
View File
@@ -138,7 +138,7 @@ function JSCitiesBasedOnDistance(int $cityNo, int $maxDistance = 1): array{
$distanceList = searchDistance($cityNo, $maxDistance, true);
$result = [];
for ($dist = 1; $dist <= $maxDistance; $dist++) {
$result[$dist] = Util::array_get($distanceList[$dist], []);
$result[$dist] = $distanceList[$dist] ?? [];
}
return $result;
}
@@ -152,7 +152,7 @@ function printCitiesBasedOnDistance(int $cityNo, int $maxDistance = 1): string
for ($dist = 1; $dist <= $maxDistance; $dist++) {
$cityList = array_map(function ($cityID) {
return CityConst::byID($cityID)->name;
}, Util::array_get($distanceList[$dist], []));
}, $distanceList[$dist] ?? []);
$cityStr = join(', ', $cityList);
+1 -1
View File
@@ -144,7 +144,7 @@ function do도시임명(General $general, int $cityID, int $targetOfficerLevel):
return '올바르지 않은 도시입니다';
}
if ($general->getVar(GeneralColumn::officer_level) >= 4 && $myPenalty[PenaltyKey::NoChiefChange->value] ?? false) {
if ($general->getVar(GeneralColumn::officer_level) >= 4 && ($myPenalty[PenaltyKey::NoChiefChange->value] ?? false)) {
return '수뇌인 장수를 변경할 수 없는 상태입니다.';
}
+3 -2
View File
@@ -51,8 +51,9 @@ function MessageBox($str)
function LogText($prefix, $variable)
{
$text = sprintf('%s : %s'."\r\n", $prefix, TVarDumper::dump($variable));
file_put_contents(ROOT.'/d_log/'.UniqueConst::$serverName.'_dbg_logs.txt', $text, FILE_APPEND);
return;
//$text = sprintf('%s : %s'."\r\n", $prefix, TVarDumper::dump($variable));
//file_put_contents(ROOT.'/d_log/'.UniqueConst::$serverName.'_dbg_logs.txt', $text, FILE_APPEND);
}
function prepareDir(string $dirPath, bool $forceCreate=true):bool{
+9 -1
View File
@@ -8,4 +8,12 @@ $session = Session::getInstance()->setReadOnly();
$db = DB::db();
TurnExecutionHelper::executeAllCommand();
$updated = false;
$locked = false;
$lastExecuted = TurnExecutionHelper::executeAllCommand($updated, $locked);
Json::die([
'result' => true,
'updated' => $updated,
'locked' => $locked,
'lastExecuted' => $lastExecuted,
]);
+1 -1
View File
@@ -526,7 +526,7 @@ function DeleteConflict($nation)
function getConquerNation($city): int
{
$conflict = Json::decode($city['conflict']);
return Util::array_first_key($conflict);
return array_key_first($conflict);
}
function ConquerCity(array $admin, General $general, array $city, array $defenderCityGeneralList)
+1 -1
View File
@@ -99,7 +99,7 @@ class Join extends \sammo\BaseAPI
}
$gameID = UniqueConst::$serverID;
$alreadyJoined = $db->queryFirstField('SELECT count(*) FROM `storage` WHERE `namespace` = %s AND `key` LIKE %s', 'inheritance_result', "{$gameID}_{$userID}_%");
$alreadyJoined = $db->queryFirstField('SELECT COUNT(*) FROM `inheritance_result` WHERE `server_id` = %s AND `owner` = %i', $gameID, $userID);
if ($alreadyJoined) {
//이미 받았음
return 0;
@@ -33,7 +33,7 @@ class event_전투특기_공성 extends \sammo\BaseItem{
}
public function onCalcStat(General $general, string $statName, $value, $aux=null){
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_SIEGE;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -28,7 +28,7 @@ class event_전투특기_궁병 extends \sammo\BaseItem{
if($statName === 'warAvoidRatio'){
return $value + 0.2;
}
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_ARCHER;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -28,7 +28,7 @@ class event_전투특기_귀병 extends \sammo\BaseItem{
if($statName === 'warMagicSuccessProb'){
return $value + 0.2;
}
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_WIZARD;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -32,7 +32,7 @@ class event_전투특기_기병 extends \sammo\BaseItem{
}
public function onCalcStat(General $general, string $statName, $value, $aux=null){
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_CAVALRY;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -33,7 +33,7 @@ class event_전투특기_보병 extends \sammo\BaseItem{
}
public function onCalcStat(General $general, string $statName, $value, $aux=null){
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_FOOTMAN;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -35,7 +35,7 @@ class che_event_공성 extends \sammo\BaseSpecial{
}
public function onCalcStat(General $general, string $statName, $value, $aux=null){
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_SIEGE;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -31,7 +31,7 @@ class che_event_궁병 extends \sammo\BaseSpecial{
if($statName === 'warAvoidRatio'){
return $value + 0.2;
}
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_ARCHER;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -30,7 +30,7 @@ class che_event_귀병 extends \sammo\BaseSpecial{
if($statName === 'warMagicSuccessProb'){
return $value + 0.2;
}
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_WIZARD;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -35,7 +35,7 @@ class che_event_기병 extends \sammo\BaseSpecial{
}
public function onCalcStat(General $general, string $statName, $value, $aux=null){
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_CAVALRY;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -35,7 +35,7 @@ class che_event_보병 extends \sammo\BaseSpecial{
}
public function onCalcStat(General $general, string $statName, $value, $aux=null){
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_FOOTMAN;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
+1 -1
View File
@@ -35,7 +35,7 @@ class che_공성 extends \sammo\BaseSpecial{
}
public function onCalcStat(General $general, string $statName, $value, $aux=null){
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_SIEGE;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
+1 -1
View File
@@ -31,7 +31,7 @@ class che_궁병 extends \sammo\BaseSpecial{
if($statName === 'warAvoidRatio'){
return $value + 0.2;
}
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_ARCHER;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
+1 -1
View File
@@ -30,7 +30,7 @@ class che_귀병 extends \sammo\BaseSpecial{
if($statName === 'warMagicSuccessProb'){
return $value + 0.2;
}
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_WIZARD;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
+1 -1
View File
@@ -35,7 +35,7 @@ class che_기병 extends \sammo\BaseSpecial{
}
public function onCalcStat(General $general, string $statName, $value, $aux=null){
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_CAVALRY;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
+1 -1
View File
@@ -35,7 +35,7 @@ class che_보병 extends \sammo\BaseSpecial{
}
public function onCalcStat(General $general, string $statName, $value, $aux=null){
if(\sammo\Util::starts_with($statName, 'dex')){
if(str_starts_with($statName, 'dex')){
$myArmType = 'dex'.GameUnitConst::T_FOOTMAN;
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
if($aux['isAttacker'] && $opposeArmType === $statName){
@@ -208,7 +208,7 @@ class che_랜덤임관 extends Command\GeneralCommand
foreach ($rawGeneralsCnt as $nation) {
$calcCnt = $nation['warpower'] + $nation['develpower'];
if ($general->getNPCType() < 2 && Util::starts_with($nation['name'], 'ⓤ')) {
if ($general->getNPCType() < 2 && str_starts_with($nation['name'], 'ⓤ')) {
$calcCnt *= 100;
}
+1 -1
View File
@@ -164,7 +164,7 @@ class che_출병 extends Command\GeneralCommand
$currDist = 999;
$minDist = Util::array_first_key($distanceList);
$minDist = array_key_first($distanceList);
do {
//1: 최단 거리 도시 중 공격 대상이 있는가 확인
//2: 최단 거리 + 1 도시 중 공격 대상이 있는가 확인
+1 -1
View File
@@ -127,7 +127,7 @@ class ChangeCity extends \sammo\Event\Action{
return DB::db()->sqleval('ROUND(%b * %d, 0)', $keyMax, $value/100);
}
if(Util::ends_with($key, '_max') && preg_match(self::REGEXP_MATH, $value, $matches)){
if(str_ends_with($key, '_max') && preg_match(self::REGEXP_MATH, $value, $matches)){
//key가 아예 최대치에 대한 값이면 연산은 결과값이 0보다만 높게.
$op = $matches[1];
$value = $matches[2];
+9 -3
View File
@@ -2084,7 +2084,7 @@ class GeneralAI
}
}
$finalCityID = Util::array_first_key($cityScoreList);
$finalCityID = array_key_first($cityScoreList);
$dist = $distanceList[$capital][$finalCityID];
$targetCityID = $finalCityID;
if ($dist > 1) {
@@ -3129,8 +3129,14 @@ class GeneralAI
$db = DB::db();
$dupLord = $db->queryFirstField('SELECT COUNT(*) FROM general WHERE officer_level = 12 AND city = %i', $this->general->getCityID());
if($dupLord <= 1 && !in_array($this->general->getRawCity()['level'], [5, 6])) {
return null;
if($dupLord <= 1) {
if ($this->general->getRawCity() === null) {
$rawCity = $db->queryFirstRow('SELECT * FROM city WHERE city = %i', $this->general->getCityID());
$this->general->setRawCity($rawCity);
}
if (in_array($this->general->getRawCity()['level'], [5, 6])) {
return null;
}
}
$general = $this->general;
+10 -2
View File
@@ -334,11 +334,19 @@ class InheritancePointManager
$inheritStor->setValue($key, [$point, $aux]);
}
$oldInheritStor = KVStorage::getStorage(DB::db(), "inheritance_result");
$db = DB::db();
$serverID = UniqueConst::$serverID;
$year = $gameStor->year;
$month = $gameStor->month;
$oldInheritStor->setValue("{$serverID}_{$ownerID}_{$general->getID()}_{$year}_{$month}", $inheritStor->getAll(true));
$db->insert('inheritance_result', [
'server_id'=>$serverID,
'owner'=>$ownerID,
'general_id'=>$general->getID(),
'year'=>$year,
'month'=>$month,
'value'=>Json::encode($inheritStor->getAll(true))
]);
}
+1 -1
View File
@@ -62,7 +62,7 @@ abstract class TriggerCaller{
return $this;
}
$lastKey = Util::array_last_key($this->triggerListByPriority);
$lastKey = array_key_last($this->triggerListByPriority);
if($lastKey < $priority){
$this->triggerListByPriority[$priority] = [$uniqueID=>$trigger];
return $this;
+1 -8
View File
@@ -422,7 +422,7 @@ class TurnExecutionHelper
$date = TimeUtil::now(true);
// 최종 처리 월턴의 다음 월턴시간 구함
$lastExecuted = $gameStor->turntime;
//$lastExecuted = $gameStor->turntime;
$prevTurn = cutTurn($gameStor->turntime, $gameStor->turnterm);
$nextTurn = addTurn($prevTurn, $gameStor->turnterm);
@@ -455,9 +455,6 @@ class TurnExecutionHelper
$gameStor->turntime = $currentTurn;
}
unlock();
if($lastExecuted !== $currentTurn){
$executed = true;
}
return $gameStor->turntime;
}
@@ -516,10 +513,6 @@ class TurnExecutionHelper
$gameStor->resetCache();
unlock();
if($lastExecuted !== $currentTurn){
$executed = true;
}
return $turntime;
}
}
+92
View File
@@ -0,0 +1,92 @@
{
"title":"【공백지】 요양 ",
"startYear":180,
"map":{
"mapName":"miniche"
},
"history":[
"<C>●</>180년 1월:<L><b>【이벤트】</b></>쉬었다 가는 깃수입니다."
],
"const": {
"joinRuinedNPCProp":0,
"npcBanMessageProb":0.005,
"defaultMaxGeneral": 800,
"allItems":{
"horse":{
"che_명마_01_노기":0, "che_명마_02_조랑":0, "che_명마_03_노새":0,
"che_명마_04_나귀":0, "che_명마_05_갈색마":0, "che_명마_06_흑색마":0,
"che_명마_07_백마" : 2, "che_명마_07_기주마" : 2, "che_명마_07_오환마" : 2, "che_명마_07_백상" : 2,
"che_명마_08_양주마" : 2, "che_명마_08_흉노마" : 2, "che_명마_09_과하마" : 2, "che_명마_09_의남백마" : 2,
"che_명마_10_대완마" : 2, "che_명마_10_옥추마" : 2, "che_명마_11_서량마" : 2, "che_명마_11_화종마" : 2,
"che_명마_12_사륜거" : 2, "che_명마_12_옥란백용구": 2, "che_명마_13_절영" : 2, "che_명마_13_적로" : 2,
"che_명마_14_적란마" : 2, "che_명마_14_조황비전" : 2, "che_명마_15_한혈마" : 2, "che_명마_15_적토마" : 2
},
"weapon":{
"che_무기_01_단도":0, "che_무기_02_단궁":0, "che_무기_03_단극":0,
"che_무기_04_목검":0, "che_무기_05_죽창":0, "che_무기_06_소부":0,
"che_무기_07_동추":2, "che_무기_07_철편":2, "che_무기_07_철쇄":2, "che_무기_07_맥궁":2,
"che_무기_08_유성추":2, "che_무기_08_철질여골":2, "che_무기_09_쌍철극":2, "che_무기_09_동호비궁":2,
"che_무기_10_삼첨도":2, "che_무기_10_대부":2, "che_무기_11_고정도":2, "che_무기_11_이광궁":2,
"che_무기_12_철척사모":2, "che_무기_12_칠성검":2, "che_무기_13_사모":2, "che_무기_13_양유기궁":2,
"che_무기_14_언월도":2, "che_무기_14_방천화극":2, "che_무기_15_청홍검":2, "che_무기_15_의천검":2
},
"book":{
"che_서적_01_효경전":0, "che_서적_02_회남자":0, "che_서적_03_변도론":0,
"che_서적_04_건상역주":0, "che_서적_05_여씨춘추":0, "che_서적_06_사민월령":0,
"che_서적_07_위료자":2, "che_서적_07_사마법":2, "che_서적_07_한서":2, "che_서적_07_논어":2,
"che_서적_08_전론":2, "che_서적_08_사기":2, "che_서적_09_장자":2, "che_서적_09_역경":2,
"che_서적_10_시경":2, "che_서적_10_구국론":2, "che_서적_11_상군서":2, "che_서적_11_춘추전":2,
"che_서적_12_산해경":2, "che_서적_12_맹덕신서":2, "che_서적_13_관자":2, "che_서적_13_병법24편":2,
"che_서적_14_한비자":2, "che_서적_14_오자병법":2, "che_서적_15_노자":2, "che_서적_15_손자병법":2
},
"item":{
"che_치료_환약":0, "che_저격_수극":0, "che_사기_탁주":0,
"che_훈련_청주":0, "che_계략_이추":0, "che_계략_향낭":0,
"event_전투특기_격노":0, "event_전투특기_견고":0, "event_전투특기_공성":0, "event_전투특기_궁병":0,
"event_전투특기_귀병":0, "event_전투특기_기병":0, "event_전투특기_돌격":0, "event_전투특기_무쌍":0,
"event_전투특기_반계":0, "event_전투특기_보병":0, "event_전투특기_신산":0, "event_전투특기_신중":0,
"event_전투특기_위압":0, "event_전투특기_의술":0, "event_전투특기_저격":0, "event_전투특기_집중":0,
"event_전투특기_징병":0, "event_전투특기_척사":0, "event_전투특기_필살":0, "event_전투특기_환술":0
}
}
},
"events":[
[
"month", 1000,
["or", ["Date", "==", null, 12], ["Date", "==", null, 6]],
["CreateManyNPC", 15, 10],
["DeleteEvent"]
],
[
"month", 1000,
["Date", "==", 181, 1],
["RaiseNPCNation"],
["DeleteEvent"]
],
[
"month", 999,
["Date", "==", 181, 1],
["OpenNationBetting", 4, 5000],
["OpenNationBetting", 1, 2000],
["DeleteEvent"]
],
[
"month", 1000,
["Date", "==", null, 1],
["ChangeCity", "all", {
"trade":100
}]
],
[
"month", 1000,
["Date", "==", null, 7],
["ChangeCity", "all", {
"trade":100
}]
]
]
}
+14
View File
@@ -583,6 +583,20 @@ CREATE TABLE IF NOT EXISTS `nation_env` (
UNIQUE INDEX `key` (`namespace`, `key`),
CONSTRAINT `json` CHECK (json_valid(`value`))
) COLLATE = 'utf8mb4_bin' ENGINE = Aria;
CREATE TABLE IF NOT EXISTS `inheritance_result` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`server_id` CHAR(20) NOT NULL COLLATE 'utf8mb4_general_ci',
`owner` INT(11) NOT NULL,
`general_id` INT(11) NOT NULL,
`year` INT(11) NOT NULL,
`month` INT(11) NOT NULL,
`value` LONGTEXT NOT NULL COLLATE 'utf8mb4_bin',
PRIMARY KEY (`id`),
INDEX `by_owner` (`server_id`, `owner`, `general_id`, `year`, `month`)
)
COLLATE='utf8mb4_general_ci'
ENGINE=Aria
;
##############################
##
+3 -3
View File
@@ -1,6 +1,6 @@
import type { AutoLoginFailed, AutoLoginNonceResponse, AutoLoginResponse, LoginFailed, LoginResponse } from "./defs/API/Login";
import { APIPathGen } from "./util/APIPathGen";
import { callSammoAPI, extractHttpMethod, GET, POST, type APICallT, type APITail, type InvalidResponse, type RawArgType, type ValidResponse } from "./util/callSammoAPI";
import { callSammoAPI, extractHttpMethod, POST, type APICallT, type APITail, type InvalidResponse, type RawArgType, type ValidResponse } from "./util/callSammoAPI";
export type { ValidResponse, InvalidResponse };
const apiRealPath = {
@@ -18,7 +18,7 @@ const apiRealPath = {
hashedToken: string,
token_id: number,
}, AutoLoginResponse, AutoLoginFailed>,
ReqNonce: GET as APICallT<undefined, AutoLoginNonceResponse, AutoLoginFailed>
ReqNonce: POST as APICallT<undefined, AutoLoginNonceResponse, AutoLoginFailed>
},
} as const;
@@ -30,4 +30,4 @@ export const SammoRootAPI = APIPathGen(apiRealPath, (path: string[], tail: APITa
}
return callSammoAPI(method, path.join('/'), args, pathParam);
};
});
});
+70 -55
View File
@@ -37,14 +37,16 @@ function regNextToken(tokenInfo: [number, string]) {
function getToken(): [number, string] | undefined {
const trialToken = localStorage.getItem(LOGIN_TOKEN_KEY);
if (!trialToken) {
console.log('no token');
return;
}
const tokenItems = JSON.parse(trialToken) as [number, [number, string], string];
const tokenItems = JSON.parse(trialToken) as [number, [number, string], number];
if (tokenItems[0] != TOKEN_VERSION) {
console.log(tokenItems);
resetToken();
return;
}
console.debug(localStorage.getItem(LOGIN_TOKEN_KEY));
const [, token,] = tokenItems;
return token;
}
@@ -53,60 +55,73 @@ function resetToken() {
localStorage.removeItem(LOGIN_TOKEN_KEY);
}
async function tryAutoLogin() {
try {
const tokenInfo = getToken();
if (!tokenInfo) {
return;
}
const [tokenID, token] = tokenInfo;
const result = await SammoRootAPI.Login.ReqNonce(undefined, true);
if (!result) {
//api 에러.
return;
}
if (!result.result) {
resetToken();
return;
}
const nonce = result.loginNonce;
const hashedToken = sha512(token + nonce);
const loginResult = await SammoRootAPI.Login.LoginByToken({
'hashedToken': hashedToken,
'token_id': tokenID,
}, true);
if (!loginResult.result) {
if (!loginResult.silent) {
alert(loginResult.reason);
}
console.error(loginResult.reason);
return;
}
if (loginResult.nextToken) {
regNextToken(loginResult.nextToken);
}
window.location.href = "./";
}
catch (e) {
if (isString(e)) {
alert(e);
}
console.error(e);
return;
}
}
async function tryAutoLogin() {
try {
const tokenInfo = getToken();
if (!tokenInfo) {
return;
}
const [tokenID, token] = tokenInfo;
for (let attempt = 0; attempt < 2; attempt++) {
const reqNonceStartAt = Date.now();
const nonceResult = await SammoRootAPI.Login.ReqNonce(undefined, true);
if (!nonceResult) {
//api 에러.
return;
}
if (!nonceResult.result) {
resetToken();
return;
}
const nonce = nonceResult.loginNonce;
console.debug(
'try auto login with token',
tokenID,
`attempt:${attempt + 1}`,
`reqNonceElapsed:${Date.now() - reqNonceStartAt}ms`
);
const hashedToken = sha512(token + nonce);
const loginResult = await SammoRootAPI.Login.LoginByToken({
'hashedToken': hashedToken,
'token_id': tokenID,
}, true);
if (!loginResult.result) {
if (loginResult.reason === '자동 로그인: 절차 오류' && attempt === 0) {
console.warn('auto login failed by procedure error. retrying once.');
await delay(150);
continue;
}
if (!loginResult.silent) {
alert(loginResult.reason);
}
console.error(loginResult.reason);
return;
}
if (loginResult.nextToken) {
regNextToken(loginResult.nextToken);
}
window.location.href = "./";
return;
}
}
catch (e) {
if (isString(e)) {
alert(e);
}
console.error(e);
return;
}
}
function getOAuthToken(mode: string, scope_list?: string[] | string) {
if (mode === undefined) {
mode = 'login';
+2 -2
View File
@@ -174,7 +174,7 @@ function tryNpmInstall()
tryComposerInstall();
if (tryNpmInstall()) {
genJS(Util::array_last_key(ServConfig::getServerList()));
genJS(array_key_last(ServConfig::getServerList()));
}
$session = Session::requireLogin(null)->setReadOnly();
@@ -227,7 +227,7 @@ if (!$allowFullUpdate || !$target) {
$target = $request['target'];
}
$baseServerName = Util::array_last_key(ServConfig::getServerList());
$baseServerName = array_key_last(ServConfig::getServerList());
if (!$target && $server != $baseServerName) {
Json::die([
+4 -4
View File
@@ -109,16 +109,16 @@ if($expires < $nowDate){
$access_token = $result['access_token'];
$expires = TimeUtil::nowAddSeconds($result['expires_in']);
if(isset($result['refresh_token'])){
$refresh_token = Util::array_get($result['refresh_token']);
$refresh_token = $result['refresh_token'] ?? null;
$refresh_token_expires = TimeUtil::nowAddSeconds($result['refresh_token_expires_in']);
}
}
$signupResult = $restAPI->signup();
$kakaoID = Util::array_get($signupResult['id']);
$kakaoID = $signupResult['id'] ?? null;
if(!$kakaoID && Util::array_get($signupResult['msg'])!='already registered'){
if(!$kakaoID && ($signupResult['msg'] ?? null) !='already registered'){
Json::die([
'result'=>false,
'reason'=>'카카오 로그인 과정 중 앱 연결 절차를 실패했습니다'.Json::encode($signupResult)
@@ -126,7 +126,7 @@ if(!$kakaoID && Util::array_get($signupResult['msg'])!='already registered'){
}
$me = $restAPI->meWithEmail();
$me['code'] = Util::array_get($me['code'], 0);
$me['code'] = $me['code'] ?? 0;
$kakao_account = $me['kakao_account']??[];
if ($me['code']< 0) {
$restAPI->unlink();
+440
View File
@@ -0,0 +1,440 @@
#!/usr/bin/env node
/* eslint-disable no-console */
import fs from "node:fs/promises";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
//const __dirname = path.dirname(__filename);
// ============ 환경설정 ============
const REQUIRE_PUBLIC = (process.env.REQUIRE_PUBLIC ?? "true").toLowerCase() === "true";
const FETCH_TIMEOUT_MS = nenv("FETCH_TIMEOUT_MS", 60_000);
const IF_LOCKED_WAIT_MS = nenv("IF_LOCKED_WAIT_MS", 10_000);
const IF_UPDATED_WAIT_MS = nenv("IF_UPDATED_WAIT_MS", 1_000);
const DEFAULT_WAIT_MS = nenv("DEFAULT_WAIT_MS", 8_000);
const BASE_SCAN_DEPTH_ONE = (process.env.BASE_SCAN_DEPTH_ONE ?? "true").toLowerCase() === "true";
const RESCAN_INTERVAL_MS = nenv("RESCAN_INTERVAL_MS", 300_000); // 5분
const SHUTDOWN_TIMEOUT_MS = nenv("SHUTDOWN_TIMEOUT_MS", 15_000);
const AUTO_RESET_JITTER_MS = nenv("AUTO_RESET_JITTER_MS", 0); // 0초 직후 지연(밀리초)
function nenv(key: string, def: number): number {
const v = process.env[key];
if (!v) return def;
const num = Number(v);
return Number.isFinite(num) ? num : def;
}
// ============ 유틸 ============
type Json = Record<string, unknown>;
type ProcResult = {
result: boolean;
updated: boolean;
locked: boolean;
lastExecuted?: string;
};
class CookieJar {
#cookies = new Map<string, string>();
getCookieHeader(): string | undefined {
if (this.#cookies.size === 0) {
return undefined;
}
return [...this.#cookies.entries()].map(([key, value]) => `${key}=${value}`).join("; ");
}
ingestSetCookie(setCookieHeaders: string[]): void {
for (const setCookie of setCookieHeaders) {
const cookiePart = setCookie.split(";", 1)[0]?.trim();
if (!cookiePart) {
continue;
}
const eqPos = cookiePart.indexOf("=");
if (eqPos <= 0) {
continue;
}
const key = cookiePart.slice(0, eqPos).trim();
const value = cookiePart.slice(eqPos + 1).trim();
if (!key) {
continue;
}
if (value === "") {
this.#cookies.delete(key);
continue;
}
this.#cookies.set(key, value);
}
}
}
function getSetCookieHeaders(headers: Headers): string[] {
const headerObj = headers as unknown as { getSetCookie?: () => string[] };
if (typeof headerObj.getSetCookie === "function") {
return headerObj.getSetCookie();
}
const fallback = headers.get("set-cookie");
if (!fallback) {
return [];
}
return [fallback];
}
function nowStr() {
const d = new Date();
const pad = (n: number) => n.toString().padStart(2, "0");
const ms = d.getMilliseconds().toString().padStart(3, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${ms}`;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function log(...args: any[]) { console.log(nowStr(), ...args); }
async function fileExists(fp: string): Promise<boolean> {
try { await fs.access(fp, fs.constants.F_OK); return true; } catch { return false; }
}
async function readFileSafe(fp: string): Promise<string | null> {
try { return await fs.readFile(fp, "utf8"); } catch { return null; }
}
async function listDirsOnceAsync(base: string): Promise<string[]> {
const out: string[] = [];
const names = await fs.readdir(base);
for (const name of names) {
const p = path.join(base, name);
try { if ((await fs.stat(p)).isDirectory()) out.push(p); } catch { /* empty */ }
}
return out;
}
function joinUrl(base: string, suffix: string): string {
if (base.endsWith("/") && suffix.startsWith("/")) return base + suffix.slice(1);
if (!base.endsWith("/") && !suffix.startsWith("/")) return base + "/" + suffix;
return base + suffix;
}
// 분 0초 정렬 대기 시간 계산: 지금 시각에서 다음 분의 0초(+지터)까지 남은 ms
function msUntilNextMinuteZero(jitterMs = 0): number {
const now = Date.now();
const nextMinuteStart = Math.ceil(now / 60_000) * 60_000;
const target = nextMinuteStart + Math.max(0, jitterMs);
return Math.max(0, target - now);
}
// ============ 경로/루트 URL ============
const basepath = path.dirname(path.dirname(path.resolve(__filename)));
async function parseWebBaseFromCommonPath(base: string): Promise<string> {
const jsPath = path.join(base, "d_shared", "common_path.js");
const text = await readFileSafe(jsPath);
if (!text) { log("[WARN] common_path.js 읽기 실패 → fallback=http://127.0.0.1"); return "http://127.0.0.1"; }
for (const line of text.split(/\r?\n/)) {
if (line.includes("root:")) {
let v = line.slice(line.indexOf("root:") + 5).trim();
v = v.replace(/^[\s,'"`]+/, "").replace(/[\s,'"`,]+$/, "");
if (v) return v;
}
}
log("[WARN] root: 항목 없음 → fallback=http://127.0.0.1");
return "http://127.0.0.1";
}
const webBase = process.env.PUBLIC_CHECK_URL || await parseWebBaseFromCommonPath(basepath);
log("BASEPATH =", basepath);
log("WEBBASE =", webBase);
// ============ 서버 모델 ============
type ServerEntry = {
name: string; // servRelPath
abs: string; // 절대 경로
isHidden: () => Promise<boolean>; // .htaccess 존재 여부
procUrl: string;
autoresetUrl: string;
};
async function scanOnce(): Promise<ServerEntry[]> {
const dirs = await listDirsOnceAsync(basepath); // 깊이는 1단계만
const out: ServerEntry[] = [];
for (const dir of dirs) {
if (BASE_SCAN_DEPTH_ONE === false) {
// 필요하면 확장 로직 배치
}
const dbphp = path.join(dir, "d_setting", "DB.php");
if (!await fileExists(dbphp)) continue;
const name = path.relative(basepath, dir);
const isHidden = () => fileExists(path.join(dir, ".htaccess"));
out.push({
name,
abs: dir,
isHidden,
procUrl: joinUrl(webBase, `/${name}/proc.php`),
autoresetUrl:joinUrl(webBase, `/${name}/j_autoreset.php`),
});
}
return out;
}
// ============ 공개 접근 체크 ============
async function isPublicReachable(signal?: AbortSignal, cookieJar?: CookieJar): Promise<boolean> {
if (!REQUIRE_PUBLIC) return true;
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), Math.min(5_000, FETCH_TIMEOUT_MS));
const composite = anySignal([ctrl.signal, signal].filter(Boolean) as AbortSignal[]);
try {
const cookieHeader = cookieJar?.getCookieHeader();
const res = await fetch(webBase, {
method: "GET",
signal: composite,
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
});
cookieJar?.ingestSetCookie(getSetCookieHeaders(res.headers));
if (!res.ok) { log(`[PUBLIC] 응답 ${res.status} → 비공개로 간주`); return false; }
return true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
if (e?.name !== "AbortError") log(`[PUBLIC] 체크 실패: ${e?.name || e}`);
return false;
} finally {
clearTimeout(timer);
}
}
// ============ HTTP ============
async function httpGetJson<T extends Json>(url: string, outerSignal?: AbortSignal, cookieJar?: CookieJar): Promise<T | null> {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
const signal = anySignal([ctrl.signal, outerSignal].filter(Boolean) as AbortSignal[]);
const t0 = performance.now();
try {
const cookieHeader = cookieJar?.getCookieHeader();
const res = await fetch(url, {
signal,
cache: "no-store",
headers: cookieHeader ? { cookie: cookieHeader } : undefined,
});
cookieJar?.ingestSetCookie(getSetCookieHeaders(res.headers));
const dt = Math.round(performance.now() - t0);
if (!res.ok) { log("HTTPError:", res.status, url); return null; }
try {
const data = (await res.json()) as T;
log(url, `${dt}ms`);
return data;
} catch {
await res.arrayBuffer().catch(() => { /* empty */ });
log("ParseError: JSON 변환 실패", url);
return null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
if (e?.name !== "AbortError") log(e?.name || "Exception", e?.message || String(e), url);
return null;
} finally {
clearTimeout(timer);
}
}
function anySignal(signals: AbortSignal[]): AbortSignal {
if (signals.length === 1) return signals[0];
const ctrl = new AbortController();
for (const s of signals) {
if (s.aborted) { ctrl.abort(); break; }
s.addEventListener("abort", () => ctrl.abort(), { once: true });
}
return ctrl.signal;
}
// ============ 서버 러너 ============
class ServerRunner {
#entry: ServerEntry;
#task?: Promise<void>;
#stopCtrl = new AbortController();
#stopped = false;
#lastAutoResetAt = 0;
#cookieJar = new CookieJar();
constructor(entry: ServerEntry) {
this.#entry = entry;
}
name() { return this.#entry.name; }
start() {
if (!this.#task) {
log(`[${this.name()}] 루프 시작`);
this.#task = this.#run();
}
return this.#task;
}
async stop(): Promise<void> {
if (this.#stopped) return;
this.#stopped = true;
this.#stopCtrl.abort();
try { await this.#task; } catch { /* empty */ }
log(`[${this.name()}] 루프 종료`);
}
async #run() {
while (!this.#stopCtrl.signal.aborted) {
try {
// 공개 접근 체크
const pub = await isPublicReachable(this.#stopCtrl.signal, publicCookieJar);
if (!pub) { await delay(15_000, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ }); continue; }
const hidden = await this.#entry.isHidden();
if (hidden) {
// ★ 매 분 0초에 맞춰 호출
const waitMs = msUntilNextMinuteZero(AUTO_RESET_JITTER_MS);
if (waitMs > 0) {
// 신호 안전하게 대기
await delay(waitMs, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ });
if (this.#stopCtrl.signal.aborted) break;
}
log(`[${this.name()}] 닫힘 - (정렬) autoreset 호출 @ mm:00${AUTO_RESET_JITTER_MS ? `+${AUTO_RESET_JITTER_MS}ms` : ""}`);
await httpGetJson<Json>(this.#entry.autoresetUrl, this.#stopCtrl.signal, this.#cookieJar);
// 같은 분에 중복 호출 방지: 0초 직후 잠깐 쉬고 다음 루프는 자연스럽게 다음 분으로 정렬됨
await delay(200, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ });
continue;
}
// 열림: proc
const data = await httpGetJson<ProcResult>(this.#entry.procUrl, this.#stopCtrl.signal, this.#cookieJar);
if (!data) {
await delay(IF_LOCKED_WAIT_MS, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ });
continue;
}
if (data.result === false || data.locked === true) {
log(`[${this.name()}] result=${data.result} locked=${data.locked}${IF_LOCKED_WAIT_MS}ms 대기`);
await delay(IF_LOCKED_WAIT_MS, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ });
continue;
}
if (data.updated === true) {
log(`[${this.name()}] updated=true → ${IF_UPDATED_WAIT_MS}ms 후 재호출 (last=${data.lastExecuted ?? "-"})`);
await delay(IF_UPDATED_WAIT_MS, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ });
continue;
}
await delay(DEFAULT_WAIT_MS, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
if (this.#stopCtrl.signal.aborted) break;
log(`[${this.name()}] 루프 예외:`, e?.message || String(e));
await delay(5_000, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ });
}
}
}
}
// ============ 매니저: 재스캔 & 종료 ============
class DaemonManager {
#runners = new Map<string, ServerRunner>();
#rescanCtrl = new AbortController();
async start() {
log("턴 데몬 시작");
await this.#rescanOnce(); // 초기 스캔
this.#scheduleRescan(); // 주기적 재스캔
}
async shutdown(timeoutMs = SHUTDOWN_TIMEOUT_MS) {
log("[SHUTDOWN] 신호 수신 → 종료 절차 시작");
this.#rescanCtrl.abort();
const stops: Promise<void>[] = [];
for (const r of this.#runners.values()) stops.push(r.stop());
// 타임아웃 대기
const done = Promise.allSettled(stops);
const timeout = delay(timeoutMs).then(() => { throw new Error("Shutdown timeout"); });
try { await Promise.race([done, timeout]); }
catch { log("[SHUTDOWN] 타임아웃 도달 → 강제 종료"); }
log("[SHUTDOWN] 종료 완료");
}
#scheduleRescan() {
// 반복 타이머(AbortSignal과 함께)
const loop = async () => {
while (!this.#rescanCtrl.signal.aborted) {
await delay(RESCAN_INTERVAL_MS, undefined, { signal: this.#rescanCtrl.signal }).catch(() => { /* empty */ });
if (this.#rescanCtrl.signal.aborted) break;
try { await this.#rescanOnce(); }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
catch (e: any) { log("[RESCAN] 예외:", e?.message || String(e)); }
}
};
loop().catch(() => { /* empty */ });
}
async #rescanOnce() {
const entries = await scanOnce();
log(`[RESCAN] 서버 ${entries.length}개 발견`);
// 추가/갱신/삭제 판정
const seen = new Set<string>();
for (const e of entries) {
seen.add(e.name);
if (!this.#runners.has(e.name)) {
// 신규 서버
const runner = new ServerRunner(e);
this.#runners.set(e.name, runner);
runner.start().catch(() => { /* empty */ });
log(`[RESCAN] 추가: ${e.name}`);
} else {
// 이미 러너 있음 → 경로/URL 변경 가능성 거의 없지만, 필요시 갱신 로직 여기에
}
}
// 사라진 서버 중지
for (const name of Array.from(this.#runners.keys())) {
if (!seen.has(name)) {
log(`[RESCAN] 제거: ${name}`);
const r = this.#runners.get(name);
if (!r) continue;
this.#runners.delete(name);
r.stop().catch(() => { /* empty */ });
}
}
}
}
// ============ 메인 ============
const manager = new DaemonManager();
const publicCookieJar = new CookieJar();
async function main() {
// 신호 처리: SIGINT/SIGTERM → graceful shutdown
const onSignal = async (sig: NodeJS.Signals) => {
log(`[SIGNAL] ${sig} 수신`);
// 중복 호출 방지
// eslint-disable-next-line @typescript-eslint/no-explicit-any
process.off("SIGINT", onSignal as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
process.off("SIGTERM", onSignal as any);
await manager.shutdown().catch(() => { /* empty */ });
process.exit(0);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
process.on("SIGINT", onSignal as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
process.on("SIGTERM", onSignal as any);
await manager.start();
}
main().catch((e) => {
log("치명적 예외:", e);
process.exitCode = 1;
});
+2
View File
@@ -13,6 +13,8 @@ use sammo\TimeUtil;
use sammo\Util;
use sammo\Validator;
use function sammo\logError;
class LoginByToken extends LoginByID
{
static array $sensitiveArgs = ['hashedToken'];
+1 -1
View File
@@ -73,7 +73,7 @@ class APIHelper
$logPath = "{$rootPath}/d_log/{$realYearMonth}_api_log.db";
}
$logDB = FileDB::db($logPath, __DIR__ . '/../../f_install/sql/api_log.sql');
$ip = $_SERVER['REMOTE_ADDR'] ?? 'local';
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'local';
$date = date('Y-m-d H:i:s');
//NOTE: array_merge([], {})의 상황이 가능함.
+4 -4
View File
@@ -88,9 +88,9 @@ class Setting
$tail = new FileTail($this->versionFile);
$version = 'noVersionJson';
foreach ($tail->smart(5, 100, true) as $line) {
if (Util::starts_with($line, '//{')) {
if (str_starts_with($line, '//{')) {
$version = Json::decode(substr($line, 2));
$version = Util::array_get($version['version'], 'noVersionValue');
$version = $version['version'] ?? 'noVersionValue';
break;
}
}
@@ -106,8 +106,8 @@ class Setting
$templates = new \League\Plates\Engine(__DIR__.'/templates');
//TODO: .htaccess가 서버 오픈에도 사용될 수 있으니 별도의 방법이 필요함
$allow_ip = Util::get_client_ip(false);
if (Util::starts_with($allow_ip, '192.168.') ||
Util::starts_with($allow_ip, '10.')) {
if (str_starts_with($allow_ip, '192.168.') ||
str_starts_with($allow_ip, '10.')) {
//172.16~172.31은 코딩하기 귀찮으니까 안할거다
$allow_ip = Util::get_client_ip(true);
}
+89 -1
View File
@@ -3,7 +3,7 @@ namespace sammo;
use Iterator;
class Util extends \utilphp\util
class Util
{
/**
@@ -889,4 +889,92 @@ class Util extends \utilphp\util
}
return join('|', $result);
}
/**
* http://github.com/brandonwamboldt/utilphp/ 의 구버전 코드에서 이식함
* 일부 코드는 수정
* 해당 라이선스: MIT
*/
/**
* Returns the first element in an array.
* PHP 8.5부터 array_first 사용 가능
*
* @param array $array
* @return mixed
*/
public static function array_first(array $array)
{
return $array[array_key_first($array)];
}
/**
* Returns the last element in an array.
* PHP 8.5부터 array_last 사용 가능
*
* @param array $array
* @return mixed
*/
public static function array_last(array $array)
{
return $array[array_key_last($array)];
}
/**
* Flatten a multi-dimensional array into a one dimensional array.
*
* Contributed by Theodore R. Smith of PHP Experts, Inc. <http://www.phpexperts.pro/>
*
* @param array $array The array to flatten
* @param boolean $preserve_keys Whether or not to preserve array keys.
* Keys from deeply nested arrays will
* overwrite keys from shallowy nested arrays
* @return array
*/
public static function array_flatten(array $array, $preserve_keys = true)
{
$flattened = array();
array_walk_recursive($array, function($value, $key) use (&$flattened, $preserve_keys) {
if ($preserve_keys && !is_int($key)) {
$flattened[$key] = $value;
} else {
$flattened[] = $value;
}
});
return $flattened;
}
public static function get_client_ip($trust_proxy_headers = false)
{
if (!$trust_proxy_headers) {
return $_SERVER['REMOTE_ADDR'];
}
return $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? 'local';
}
/**
* Access an array index, retrieving the value stored there if it
* exists or a default if it does not. This function allows you to
* concisely access an index which may or may not exist without
* raising a warning.
*
* @deprecated PHP 7.0 이상에서는 null 병합 연산자 사용을 권장
*
* @param array $var Array value to access
* @param mixed $default Default value to return if the key is not
* present in the array
* @return mixed
*/
public static function array_get(&$var, $default = null)
{
if (isset($var)) {
return $var;
}
return $default;
}
};
+2 -1
View File
@@ -81,7 +81,8 @@
},
"include": [
"hwe/ts/**/*",
"hwe/ts/**/*.vue"
"hwe/ts/**/*.vue",
"src/*.ts",
],
"exclude": [
"**/*.test.ts"