Compare commits

...
18 Commits
Author SHA1 Message Date
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
류화영 3e06304dc7 fix: 토너먼트 분노 공격 확률 오류 수정 2025-08-23 09:23:00 +09:00
40 changed files with 575 additions and 110 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
@@ -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);
+7 -3
View File
@@ -1153,21 +1153,25 @@ function fight($tnmt_type, $tnmt, $phs, $group, $g1, $g2, $type)
];
//막판 분노
$factor1 = 1.0;
$factor2 = 1.0;
$ratio = rand() % 300;
if ($e1 / 5 > $energy1 && $damage1 > $damage2 && $gen1[$tp] >= $ratio) {
$damage2 *= Util::round((rand() % 301 + 200) / 100); // 200 ~ 500%
$factor2 = Util::round((rand() % 301 + 200) / 100); // 200 ~ 500%
$critical1 = 1;
$str = Util::choiceRandom($crticialSkillMap[$tnmt_type]);
$log[] = "<S>●</> <Y>{$gen1['name']}</>의 분노의 <M>{$str}</> 공격!";
}
$ratio = rand() % 300;
if ($e2 / 5 > $energy2 && $damage2 > $damage1 && $gen2[$tp] >= $ratio) {
$damage1 *= Util::round((rand() % 301 + 200) / 100); // 200 ~ 500%
$factor1 = Util::round((rand() % 301 + 200) / 100); // 200 ~ 500%
$critical2 = 1;
$str = Util::choiceRandom($crticialSkillMap[$tnmt_type]);
$log[] = "<S>●</> <Y>{$gen2['name']}</>의 분노의 <M>{$str}</> 공격!";
}
$damage1 *= $factor1;
$damage2 *= $factor2;
//1합 승부
if ($phase == 1) {
+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];
+8 -2
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,9 +3129,15 @@ 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])) {
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;
$lordCities = $db->queryFirstColumn('SELECT city.city as city FROM general LEFT JOIN city ON general.city = city.city WHERE general.officer_level = 12 AND city.nation = 0');
+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;
}
}
+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
;
##############################
##
+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();
+375
View File
@@ -0,0 +1,375 @@
#!/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;
};
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): 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 res = await fetch(webBase, { method: "GET", signal: composite });
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): 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 res = await fetch(url, { signal, cache: "no-store" });
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;
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);
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);
// 같은 분에 중복 호출 방지: 0초 직후 잠깐 쉬고 다음 루프는 자연스럽게 다음 분으로 정렬됨
await delay(200, undefined, { signal: this.#stopCtrl.signal }).catch(() => { /* empty */ });
continue;
}
// 열림: proc
const data = await httpGetJson<ProcResult>(this.#entry.procUrl, this.#stopCtrl.signal);
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();
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;
});
+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);
}
+107 -1
View File
@@ -3,7 +3,7 @@ namespace sammo;
use Iterator;
class Util extends \utilphp\util
class Util
{
/**
@@ -889,4 +889,110 @@ 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;
}
/**
* Returns the IP address of the client.
*
* @param boolean $trust_proxy_headers Whether or not to trust the
* proxy headers HTTP_CLIENT_IP
* and HTTP_X_FORWARDED_FOR. ONLY
* use if your server is behind a
* proxy that sets these values
* @return string
*/
public static function get_client_ip($trust_proxy_headers = false)
{
if (!$trust_proxy_headers) {
return $_SERVER['REMOTE_ADDR'];
}
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
return $ip;
}
/**
* 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"