forked from devsam/core
Compare commits
18
Commits
4bb940c8c5
...
d679a6fed8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d679a6fed8 | ||
|
|
6fe156eb7c | ||
|
|
663ddbe76a | ||
|
|
30297454f7 | ||
|
|
bc602722bf | ||
|
|
be8f147216 | ||
|
|
b9495fb64a | ||
|
|
07b217b290 | ||
|
|
42fac46062 | ||
|
|
24bc8985d0 | ||
|
|
76a2d5ff63 | ||
|
|
148f32f78b | ||
|
|
62f5b6597e | ||
|
|
7e810b0279 | ||
|
|
2336b29b65 | ||
|
|
185c0e16a0 | ||
|
|
656baeb9ee | ||
|
|
3e06304dc7 |
@@ -12,7 +12,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"brandonwamboldt/utilphp": "^1.1",
|
|
||||||
"sergeytsalkov/meekrodb": "^2.5",
|
"sergeytsalkov/meekrodb": "^2.5",
|
||||||
"league/plates": "^3.5",
|
"league/plates": "^3.5",
|
||||||
"vlucas/valitron": "^1.4",
|
"vlucas/valitron": "^1.4",
|
||||||
|
|||||||
Generated
+2
-52
@@ -4,58 +4,8 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "deed9afdc95bc42f84784df67d295223",
|
"content-hash": "7edae9b6be35e66ffd0d180f5fac6679",
|
||||||
"packages": [
|
"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",
|
"name": "brick/math",
|
||||||
"version": "0.12.1",
|
"version": "0.12.1",
|
||||||
@@ -5721,5 +5671,5 @@
|
|||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
"php": "8.3"
|
"php": "8.3"
|
||||||
},
|
},
|
||||||
"plugin-api-version": "2.3.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ $queryMap = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ($reqQueryType === null || !key_exists($reqQueryType, $queryMap)) {
|
if ($reqQueryType === null || !key_exists($reqQueryType, $queryMap)) {
|
||||||
$reqQueryType = Util::array_first_key($queryMap);
|
$reqQueryType = array_key_first($queryMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
//로그인 검사
|
//로그인 검사
|
||||||
|
|||||||
@@ -559,7 +559,7 @@ function checkStatistic()
|
|||||||
|
|
||||||
$nationHist = '';
|
$nationHist = '';
|
||||||
foreach (GameConst::$availableNationType as $nationType) {
|
foreach (GameConst::$availableNationType as $nationType) {
|
||||||
if (!Util::array_get($nationHists[$nationType])) {
|
if (!($nationHists[$nationType] ?? null)) {
|
||||||
$nationHists[$nationType] = '-';
|
$nationHists[$nationType] = '-';
|
||||||
}
|
}
|
||||||
$nationHist .= getNationType($nationType) . "({$nationHists[$nationType]}), ";
|
$nationHist .= getNationType($nationType) . "({$nationHists[$nationType]}), ";
|
||||||
|
|||||||
+2
-2
@@ -138,7 +138,7 @@ function JSCitiesBasedOnDistance(int $cityNo, int $maxDistance = 1): array{
|
|||||||
$distanceList = searchDistance($cityNo, $maxDistance, true);
|
$distanceList = searchDistance($cityNo, $maxDistance, true);
|
||||||
$result = [];
|
$result = [];
|
||||||
for ($dist = 1; $dist <= $maxDistance; $dist++) {
|
for ($dist = 1; $dist <= $maxDistance; $dist++) {
|
||||||
$result[$dist] = Util::array_get($distanceList[$dist], []);
|
$result[$dist] = $distanceList[$dist] ?? [];
|
||||||
}
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ function printCitiesBasedOnDistance(int $cityNo, int $maxDistance = 1): string
|
|||||||
for ($dist = 1; $dist <= $maxDistance; $dist++) {
|
for ($dist = 1; $dist <= $maxDistance; $dist++) {
|
||||||
$cityList = array_map(function ($cityID) {
|
$cityList = array_map(function ($cityID) {
|
||||||
return CityConst::byID($cityID)->name;
|
return CityConst::byID($cityID)->name;
|
||||||
}, Util::array_get($distanceList[$dist], []));
|
}, $distanceList[$dist] ?? []);
|
||||||
|
|
||||||
$cityStr = join(', ', $cityList);
|
$cityStr = join(', ', $cityList);
|
||||||
|
|
||||||
|
|||||||
@@ -1153,21 +1153,25 @@ function fight($tnmt_type, $tnmt, $phs, $group, $g1, $g2, $type)
|
|||||||
];
|
];
|
||||||
|
|
||||||
//막판 분노
|
//막판 분노
|
||||||
|
$factor1 = 1.0;
|
||||||
|
$factor2 = 1.0;
|
||||||
$ratio = rand() % 300;
|
$ratio = rand() % 300;
|
||||||
if ($e1 / 5 > $energy1 && $damage1 > $damage2 && $gen1[$tp] >= $ratio) {
|
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;
|
$critical1 = 1;
|
||||||
$str = Util::choiceRandom($crticialSkillMap[$tnmt_type]);
|
$str = Util::choiceRandom($crticialSkillMap[$tnmt_type]);
|
||||||
$log[] = "<S>●</> <Y>{$gen1['name']}</>의 분노의 <M>{$str}</> 공격!";
|
$log[] = "<S>●</> <Y>{$gen1['name']}</>의 분노의 <M>{$str}</> 공격!";
|
||||||
}
|
}
|
||||||
$ratio = rand() % 300;
|
$ratio = rand() % 300;
|
||||||
if ($e2 / 5 > $energy2 && $damage2 > $damage1 && $gen2[$tp] >= $ratio) {
|
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;
|
$critical2 = 1;
|
||||||
|
|
||||||
$str = Util::choiceRandom($crticialSkillMap[$tnmt_type]);
|
$str = Util::choiceRandom($crticialSkillMap[$tnmt_type]);
|
||||||
$log[] = "<S>●</> <Y>{$gen2['name']}</>의 분노의 <M>{$str}</> 공격!";
|
$log[] = "<S>●</> <Y>{$gen2['name']}</>의 분노의 <M>{$str}</> 공격!";
|
||||||
}
|
}
|
||||||
|
$damage1 *= $factor1;
|
||||||
|
$damage2 *= $factor2;
|
||||||
|
|
||||||
//1합 승부
|
//1합 승부
|
||||||
if ($phase == 1) {
|
if ($phase == 1) {
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ function do도시임명(General $general, int $cityID, int $targetOfficerLevel):
|
|||||||
return '올바르지 않은 도시입니다';
|
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 '수뇌인 장수를 변경할 수 없는 상태입니다.';
|
return '수뇌인 장수를 변경할 수 없는 상태입니다.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -51,8 +51,9 @@ function MessageBox($str)
|
|||||||
|
|
||||||
function LogText($prefix, $variable)
|
function LogText($prefix, $variable)
|
||||||
{
|
{
|
||||||
$text = sprintf('%s : %s'."\r\n", $prefix, TVarDumper::dump($variable));
|
return;
|
||||||
file_put_contents(ROOT.'/d_log/'.UniqueConst::$serverName.'_dbg_logs.txt', $text, FILE_APPEND);
|
//$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{
|
function prepareDir(string $dirPath, bool $forceCreate=true):bool{
|
||||||
|
|||||||
+9
-1
@@ -8,4 +8,12 @@ $session = Session::getInstance()->setReadOnly();
|
|||||||
|
|
||||||
$db = DB::db();
|
$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
@@ -526,7 +526,7 @@ function DeleteConflict($nation)
|
|||||||
function getConquerNation($city): int
|
function getConquerNation($city): int
|
||||||
{
|
{
|
||||||
$conflict = Json::decode($city['conflict']);
|
$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)
|
function ConquerCity(array $admin, General $general, array $city, array $defenderCityGeneralList)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class Join extends \sammo\BaseAPI
|
|||||||
}
|
}
|
||||||
|
|
||||||
$gameID = UniqueConst::$serverID;
|
$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) {
|
if ($alreadyJoined) {
|
||||||
//이미 받았음
|
//이미 받았음
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class event_전투특기_공성 extends \sammo\BaseItem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function onCalcStat(General $general, string $statName, $value, $aux=null){
|
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;
|
$myArmType = 'dex'.GameUnitConst::T_SIEGE;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class event_전투특기_궁병 extends \sammo\BaseItem{
|
|||||||
if($statName === 'warAvoidRatio'){
|
if($statName === 'warAvoidRatio'){
|
||||||
return $value + 0.2;
|
return $value + 0.2;
|
||||||
}
|
}
|
||||||
if(\sammo\Util::starts_with($statName, 'dex')){
|
if(str_starts_with($statName, 'dex')){
|
||||||
$myArmType = 'dex'.GameUnitConst::T_ARCHER;
|
$myArmType = 'dex'.GameUnitConst::T_ARCHER;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class event_전투특기_귀병 extends \sammo\BaseItem{
|
|||||||
if($statName === 'warMagicSuccessProb'){
|
if($statName === 'warMagicSuccessProb'){
|
||||||
return $value + 0.2;
|
return $value + 0.2;
|
||||||
}
|
}
|
||||||
if(\sammo\Util::starts_with($statName, 'dex')){
|
if(str_starts_with($statName, 'dex')){
|
||||||
$myArmType = 'dex'.GameUnitConst::T_WIZARD;
|
$myArmType = 'dex'.GameUnitConst::T_WIZARD;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class event_전투특기_기병 extends \sammo\BaseItem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function onCalcStat(General $general, string $statName, $value, $aux=null){
|
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;
|
$myArmType = 'dex'.GameUnitConst::T_CAVALRY;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class event_전투특기_보병 extends \sammo\BaseItem{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function onCalcStat(General $general, string $statName, $value, $aux=null){
|
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;
|
$myArmType = 'dex'.GameUnitConst::T_FOOTMAN;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
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){
|
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;
|
$myArmType = 'dex'.GameUnitConst::T_SIEGE;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class che_event_궁병 extends \sammo\BaseSpecial{
|
|||||||
if($statName === 'warAvoidRatio'){
|
if($statName === 'warAvoidRatio'){
|
||||||
return $value + 0.2;
|
return $value + 0.2;
|
||||||
}
|
}
|
||||||
if(\sammo\Util::starts_with($statName, 'dex')){
|
if(str_starts_with($statName, 'dex')){
|
||||||
$myArmType = 'dex'.GameUnitConst::T_ARCHER;
|
$myArmType = 'dex'.GameUnitConst::T_ARCHER;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class che_event_귀병 extends \sammo\BaseSpecial{
|
|||||||
if($statName === 'warMagicSuccessProb'){
|
if($statName === 'warMagicSuccessProb'){
|
||||||
return $value + 0.2;
|
return $value + 0.2;
|
||||||
}
|
}
|
||||||
if(\sammo\Util::starts_with($statName, 'dex')){
|
if(str_starts_with($statName, 'dex')){
|
||||||
$myArmType = 'dex'.GameUnitConst::T_WIZARD;
|
$myArmType = 'dex'.GameUnitConst::T_WIZARD;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
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){
|
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;
|
$myArmType = 'dex'.GameUnitConst::T_CAVALRY;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
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){
|
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;
|
$myArmType = 'dex'.GameUnitConst::T_FOOTMAN;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class che_공성 extends \sammo\BaseSpecial{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function onCalcStat(General $general, string $statName, $value, $aux=null){
|
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;
|
$myArmType = 'dex'.GameUnitConst::T_SIEGE;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class che_궁병 extends \sammo\BaseSpecial{
|
|||||||
if($statName === 'warAvoidRatio'){
|
if($statName === 'warAvoidRatio'){
|
||||||
return $value + 0.2;
|
return $value + 0.2;
|
||||||
}
|
}
|
||||||
if(\sammo\Util::starts_with($statName, 'dex')){
|
if(str_starts_with($statName, 'dex')){
|
||||||
$myArmType = 'dex'.GameUnitConst::T_ARCHER;
|
$myArmType = 'dex'.GameUnitConst::T_ARCHER;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class che_귀병 extends \sammo\BaseSpecial{
|
|||||||
if($statName === 'warMagicSuccessProb'){
|
if($statName === 'warMagicSuccessProb'){
|
||||||
return $value + 0.2;
|
return $value + 0.2;
|
||||||
}
|
}
|
||||||
if(\sammo\Util::starts_with($statName, 'dex')){
|
if(str_starts_with($statName, 'dex')){
|
||||||
$myArmType = 'dex'.GameUnitConst::T_WIZARD;
|
$myArmType = 'dex'.GameUnitConst::T_WIZARD;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class che_기병 extends \sammo\BaseSpecial{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function onCalcStat(General $general, string $statName, $value, $aux=null){
|
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;
|
$myArmType = 'dex'.GameUnitConst::T_CAVALRY;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class che_보병 extends \sammo\BaseSpecial{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function onCalcStat(General $general, string $statName, $value, $aux=null){
|
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;
|
$myArmType = 'dex'.GameUnitConst::T_FOOTMAN;
|
||||||
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
$opposeArmType = 'dex'.$aux['opposeType']->armType;;
|
||||||
if($aux['isAttacker'] && $opposeArmType === $statName){
|
if($aux['isAttacker'] && $opposeArmType === $statName){
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ class che_랜덤임관 extends Command\GeneralCommand
|
|||||||
foreach ($rawGeneralsCnt as $nation) {
|
foreach ($rawGeneralsCnt as $nation) {
|
||||||
$calcCnt = $nation['warpower'] + $nation['develpower'];
|
$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;
|
$calcCnt *= 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class che_출병 extends Command\GeneralCommand
|
|||||||
|
|
||||||
$currDist = 999;
|
$currDist = 999;
|
||||||
|
|
||||||
$minDist = Util::array_first_key($distanceList);
|
$minDist = array_key_first($distanceList);
|
||||||
do {
|
do {
|
||||||
//1: 최단 거리 도시 중 공격 대상이 있는가 확인
|
//1: 최단 거리 도시 중 공격 대상이 있는가 확인
|
||||||
//2: 최단 거리 + 1 도시 중 공격 대상이 있는가 확인
|
//2: 최단 거리 + 1 도시 중 공격 대상이 있는가 확인
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class ChangeCity extends \sammo\Event\Action{
|
|||||||
return DB::db()->sqleval('ROUND(%b * %d, 0)', $keyMax, $value/100);
|
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보다만 높게.
|
//key가 아예 최대치에 대한 값이면 연산은 결과값이 0보다만 높게.
|
||||||
$op = $matches[1];
|
$op = $matches[1];
|
||||||
$value = $matches[2];
|
$value = $matches[2];
|
||||||
|
|||||||
@@ -2084,7 +2084,7 @@ class GeneralAI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$finalCityID = Util::array_first_key($cityScoreList);
|
$finalCityID = array_key_first($cityScoreList);
|
||||||
$dist = $distanceList[$capital][$finalCityID];
|
$dist = $distanceList[$capital][$finalCityID];
|
||||||
$targetCityID = $finalCityID;
|
$targetCityID = $finalCityID;
|
||||||
if ($dist > 1) {
|
if ($dist > 1) {
|
||||||
@@ -3129,8 +3129,14 @@ class GeneralAI
|
|||||||
$db = DB::db();
|
$db = DB::db();
|
||||||
|
|
||||||
$dupLord = $db->queryFirstField('SELECT COUNT(*) FROM general WHERE officer_level = 12 AND city = %i', $this->general->getCityID());
|
$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) {
|
||||||
return null;
|
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;
|
$general = $this->general;
|
||||||
|
|||||||
@@ -334,11 +334,19 @@ class InheritancePointManager
|
|||||||
$inheritStor->setValue($key, [$point, $aux]);
|
$inheritStor->setValue($key, [$point, $aux]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$oldInheritStor = KVStorage::getStorage(DB::db(), "inheritance_result");
|
$db = DB::db();
|
||||||
$serverID = UniqueConst::$serverID;
|
$serverID = UniqueConst::$serverID;
|
||||||
$year = $gameStor->year;
|
$year = $gameStor->year;
|
||||||
$month = $gameStor->month;
|
$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))
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ abstract class TriggerCaller{
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
$lastKey = Util::array_last_key($this->triggerListByPriority);
|
$lastKey = array_key_last($this->triggerListByPriority);
|
||||||
if($lastKey < $priority){
|
if($lastKey < $priority){
|
||||||
$this->triggerListByPriority[$priority] = [$uniqueID=>$trigger];
|
$this->triggerListByPriority[$priority] = [$uniqueID=>$trigger];
|
||||||
return $this;
|
return $this;
|
||||||
|
|||||||
@@ -422,7 +422,7 @@ class TurnExecutionHelper
|
|||||||
|
|
||||||
$date = TimeUtil::now(true);
|
$date = TimeUtil::now(true);
|
||||||
// 최종 처리 월턴의 다음 월턴시간 구함
|
// 최종 처리 월턴의 다음 월턴시간 구함
|
||||||
$lastExecuted = $gameStor->turntime;
|
//$lastExecuted = $gameStor->turntime;
|
||||||
$prevTurn = cutTurn($gameStor->turntime, $gameStor->turnterm);
|
$prevTurn = cutTurn($gameStor->turntime, $gameStor->turnterm);
|
||||||
$nextTurn = addTurn($prevTurn, $gameStor->turnterm);
|
$nextTurn = addTurn($prevTurn, $gameStor->turnterm);
|
||||||
|
|
||||||
@@ -455,9 +455,6 @@ class TurnExecutionHelper
|
|||||||
$gameStor->turntime = $currentTurn;
|
$gameStor->turntime = $currentTurn;
|
||||||
}
|
}
|
||||||
unlock();
|
unlock();
|
||||||
if($lastExecuted !== $currentTurn){
|
|
||||||
$executed = true;
|
|
||||||
}
|
|
||||||
return $gameStor->turntime;
|
return $gameStor->turntime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,10 +513,6 @@ class TurnExecutionHelper
|
|||||||
$gameStor->resetCache();
|
$gameStor->resetCache();
|
||||||
unlock();
|
unlock();
|
||||||
|
|
||||||
if($lastExecuted !== $currentTurn){
|
|
||||||
$executed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $turntime;
|
return $turntime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -583,6 +583,20 @@ CREATE TABLE IF NOT EXISTS `nation_env` (
|
|||||||
UNIQUE INDEX `key` (`namespace`, `key`),
|
UNIQUE INDEX `key` (`namespace`, `key`),
|
||||||
CONSTRAINT `json` CHECK (json_valid(`value`))
|
CONSTRAINT `json` CHECK (json_valid(`value`))
|
||||||
) COLLATE = 'utf8mb4_bin' ENGINE = Aria;
|
) 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
@@ -174,7 +174,7 @@ function tryNpmInstall()
|
|||||||
|
|
||||||
tryComposerInstall();
|
tryComposerInstall();
|
||||||
if (tryNpmInstall()) {
|
if (tryNpmInstall()) {
|
||||||
genJS(Util::array_last_key(ServConfig::getServerList()));
|
genJS(array_key_last(ServConfig::getServerList()));
|
||||||
}
|
}
|
||||||
$session = Session::requireLogin(null)->setReadOnly();
|
$session = Session::requireLogin(null)->setReadOnly();
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ if (!$allowFullUpdate || !$target) {
|
|||||||
$target = $request['target'];
|
$target = $request['target'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$baseServerName = Util::array_last_key(ServConfig::getServerList());
|
$baseServerName = array_key_last(ServConfig::getServerList());
|
||||||
|
|
||||||
if (!$target && $server != $baseServerName) {
|
if (!$target && $server != $baseServerName) {
|
||||||
Json::die([
|
Json::die([
|
||||||
|
|||||||
@@ -109,16 +109,16 @@ if($expires < $nowDate){
|
|||||||
$access_token = $result['access_token'];
|
$access_token = $result['access_token'];
|
||||||
$expires = TimeUtil::nowAddSeconds($result['expires_in']);
|
$expires = TimeUtil::nowAddSeconds($result['expires_in']);
|
||||||
if(isset($result['refresh_token'])){
|
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']);
|
$refresh_token_expires = TimeUtil::nowAddSeconds($result['refresh_token_expires_in']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$signupResult = $restAPI->signup();
|
$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([
|
Json::die([
|
||||||
'result'=>false,
|
'result'=>false,
|
||||||
'reason'=>'카카오 로그인 과정 중 앱 연결 절차를 실패했습니다'.Json::encode($signupResult)
|
'reason'=>'카카오 로그인 과정 중 앱 연결 절차를 실패했습니다'.Json::encode($signupResult)
|
||||||
@@ -126,7 +126,7 @@ if(!$kakaoID && Util::array_get($signupResult['msg'])!='already registered'){
|
|||||||
}
|
}
|
||||||
|
|
||||||
$me = $restAPI->meWithEmail();
|
$me = $restAPI->meWithEmail();
|
||||||
$me['code'] = Util::array_get($me['code'], 0);
|
$me['code'] = $me['code'] ?? 0;
|
||||||
$kakao_account = $me['kakao_account']??[];
|
$kakao_account = $me['kakao_account']??[];
|
||||||
if ($me['code']< 0) {
|
if ($me['code']< 0) {
|
||||||
$restAPI->unlink();
|
$restAPI->unlink();
|
||||||
|
|||||||
+375
@@ -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;
|
||||||
|
});
|
||||||
@@ -88,9 +88,9 @@ class Setting
|
|||||||
$tail = new FileTail($this->versionFile);
|
$tail = new FileTail($this->versionFile);
|
||||||
$version = 'noVersionJson';
|
$version = 'noVersionJson';
|
||||||
foreach ($tail->smart(5, 100, true) as $line) {
|
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 = Json::decode(substr($line, 2));
|
||||||
$version = Util::array_get($version['version'], 'noVersionValue');
|
$version = $version['version'] ?? 'noVersionValue';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,8 +106,8 @@ class Setting
|
|||||||
$templates = new \League\Plates\Engine(__DIR__.'/templates');
|
$templates = new \League\Plates\Engine(__DIR__.'/templates');
|
||||||
//TODO: .htaccess가 서버 오픈에도 사용될 수 있으니 별도의 방법이 필요함
|
//TODO: .htaccess가 서버 오픈에도 사용될 수 있으니 별도의 방법이 필요함
|
||||||
$allow_ip = Util::get_client_ip(false);
|
$allow_ip = Util::get_client_ip(false);
|
||||||
if (Util::starts_with($allow_ip, '192.168.') ||
|
if (str_starts_with($allow_ip, '192.168.') ||
|
||||||
Util::starts_with($allow_ip, '10.')) {
|
str_starts_with($allow_ip, '10.')) {
|
||||||
//172.16~172.31은 코딩하기 귀찮으니까 안할거다
|
//172.16~172.31은 코딩하기 귀찮으니까 안할거다
|
||||||
$allow_ip = Util::get_client_ip(true);
|
$allow_ip = Util::get_client_ip(true);
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-1
@@ -3,7 +3,7 @@ namespace sammo;
|
|||||||
|
|
||||||
use Iterator;
|
use Iterator;
|
||||||
|
|
||||||
class Util extends \utilphp\util
|
class Util
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -889,4 +889,110 @@ class Util extends \utilphp\util
|
|||||||
}
|
}
|
||||||
return join('|', $result);
|
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
@@ -81,7 +81,8 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"hwe/ts/**/*",
|
"hwe/ts/**/*",
|
||||||
"hwe/ts/**/*.vue"
|
"hwe/ts/**/*.vue",
|
||||||
|
"src/*.ts",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"**/*.test.ts"
|
"**/*.test.ts"
|
||||||
|
|||||||
Reference in New Issue
Block a user