fix: block S100 inheritance stat resets
This commit is contained in:
@@ -4,6 +4,7 @@ namespace sammo\API\InheritAction;
|
|||||||
|
|
||||||
use sammo\Session;
|
use sammo\Session;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
|
use sammo\CentennialAllStarGrowthService;
|
||||||
use sammo\DB;
|
use sammo\DB;
|
||||||
use sammo\Enums\APIRecoveryType;
|
use sammo\Enums\APIRecoveryType;
|
||||||
use sammo\Enums\RankColumn;
|
use sammo\Enums\RankColumn;
|
||||||
@@ -98,6 +99,10 @@ class ResetStat extends \sammo\BaseAPI
|
|||||||
return 'NPC는 능력치 초기화를 할 수 없습니다.';
|
return 'NPC는 능력치 초기화를 할 수 없습니다.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!CentennialAllStarGrowthService::isStatResetAllowed()) {
|
||||||
|
return '100기 올스타 장수는 능력치 초기화를 사용할 수 없습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$db = DB::db();
|
$db = DB::db();
|
||||||
$gameStor = KVStorage::getStorage($db, 'game_env');
|
$gameStor = KVStorage::getStorage($db, 'game_env');
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ final class CentennialAllStarGrowthService
|
|||||||
return GameConst::$targetGeneralPool === self::POOL_CLASS;
|
return GameConst::$targetGeneralPool === self::POOL_CLASS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function isStatResetAllowed(): bool
|
||||||
|
{
|
||||||
|
return !self::isActive();
|
||||||
|
}
|
||||||
|
|
||||||
public static function initialAux(array $targetInfo, ?array $userInitialStats = null): array
|
public static function initialAux(array $targetInfo, ?array $userInitialStats = null): array
|
||||||
{
|
{
|
||||||
$granted = array_fill_keys(array_merge(self::STAT_KEYS, self::DEX_KEYS), 0);
|
$granted = array_fill_keys(array_merge(self::STAT_KEYS, self::DEX_KEYS), 0);
|
||||||
|
|||||||
@@ -172,7 +172,7 @@
|
|||||||
<BButton class="col-6 offset-6" variant="primary" @click="checkOwner"> 소유자 찾기 </BButton>
|
<BButton class="col-6 offset-6" variant="primary" @click="checkOwner"> 소유자 찾기 </BButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-lg-4 col-sm-6 col-12 py-2">
|
<div v-if="canResetStat" class="col col-lg-4 col-sm-6 col-12 py-2">
|
||||||
<div class="row px-4">
|
<div class="row px-4">
|
||||||
<div class="a-right col-6 align-self-center">능력치 초기화</div>
|
<div class="a-right col-6 align-self-center">능력치 초기화</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
@@ -222,6 +222,17 @@
|
|||||||
<BButton class="col-6 offset-6" variant="primary" @click="resetStat"> 능력치 초기화</BButton>
|
<BButton class="col-6 offset-6" variant="primary" @click="resetStat"> 능력치 초기화</BButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="col col-lg-4 col-sm-6 col-12 py-2">
|
||||||
|
<div class="row px-4">
|
||||||
|
<div class="a-right col-6 align-self-center">능력치 초기화</div>
|
||||||
|
<div class="col-6 align-self-center">사용 불가</div>
|
||||||
|
</div>
|
||||||
|
<div class="a-right">
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
100기 올스타 장수는 장수 전환 시 능력치 성장 기록을 보존하기 위해 능력치 초기화를 사용할 수 없습니다.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -297,6 +308,7 @@ declare const staticValues: {
|
|||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
availableTargetGeneral: Record<number, string>;
|
availableTargetGeneral: Record<number, string>;
|
||||||
|
canResetStat: boolean;
|
||||||
currentStat: {
|
currentStat: {
|
||||||
leadership: number;
|
leadership: number;
|
||||||
strength: number;
|
strength: number;
|
||||||
@@ -447,6 +459,7 @@ const {
|
|||||||
availableSpecialWar,
|
availableSpecialWar,
|
||||||
availableUnique,
|
availableUnique,
|
||||||
availableTargetGeneral,
|
availableTargetGeneral,
|
||||||
|
canResetStat,
|
||||||
currentStat
|
currentStat
|
||||||
} = staticValues;
|
} = staticValues;
|
||||||
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ $lastInheritPointLogs = $db->query('SELECT id, server_id, year, month, date, tex
|
|||||||
'availableUnique' => $availableUnique,
|
'availableUnique' => $availableUnique,
|
||||||
'lastInheritPointLogs' => $lastInheritPointLogs,
|
'lastInheritPointLogs' => $lastInheritPointLogs,
|
||||||
'availableTargetGeneral' => $availableTargetGeneral,
|
'availableTargetGeneral' => $availableTargetGeneral,
|
||||||
|
'canResetStat' => CentennialAllStarGrowthService::isStatResetAllowed(),
|
||||||
'currentStat' => [
|
'currentStat' => [
|
||||||
'leadership' => Util::clamp($me->getVar('leadership'), GameConst::$defaultStatMin, GameConst::$defaultStatMax),
|
'leadership' => Util::clamp($me->getVar('leadership'), GameConst::$defaultStatMin, GameConst::$defaultStatMax),
|
||||||
'strength' => Util::clamp($me->getVar('strength'), GameConst::$defaultStatMin, GameConst::$defaultStatMax),
|
'strength' => Util::clamp($me->getVar('strength'), GameConst::$defaultStatMin, GameConst::$defaultStatMax),
|
||||||
|
|||||||
@@ -25,6 +25,20 @@ final class CentennialAllStarGrowthTest extends TestCase
|
|||||||
self::assertSame(1.0, CentennialAllStarGrowth::progress(180, 210, 1));
|
self::assertSame(1.0, CentennialAllStarGrowth::progress(180, 210, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testStatResetIsDisabledOnlyForCentennialAllStarPool(): void
|
||||||
|
{
|
||||||
|
$previousPool = GameConst::$targetGeneralPool;
|
||||||
|
try {
|
||||||
|
GameConst::$targetGeneralPool = CentennialAllStarGrowthService::POOL_CLASS;
|
||||||
|
self::assertFalse(CentennialAllStarGrowthService::isStatResetAllowed());
|
||||||
|
|
||||||
|
GameConst::$targetGeneralPool = 'RandomNameGeneral';
|
||||||
|
self::assertTrue(CentennialAllStarGrowthService::isStatResetAllowed());
|
||||||
|
} finally {
|
||||||
|
GameConst::$targetGeneralPool = $previousPool;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function testMAndGGeneralsAdvanceAtNinetyPercentProgress(): void
|
public function testMAndGGeneralsAdvanceAtNinetyPercentProgress(): void
|
||||||
{
|
{
|
||||||
self::assertSame(
|
self::assertSame(
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import {createHash} from 'node:crypto';
|
||||||
|
import {chromium} from 'playwright';
|
||||||
|
|
||||||
|
const baseURL = process.env.REF_BROWSER_URL;
|
||||||
|
const screenshotPath = process.env.REF_SCREENSHOT_PATH;
|
||||||
|
const resultPath = process.env.REF_RESULT_PATH;
|
||||||
|
const username = process.env.REF_USER_ID ?? 's100user01';
|
||||||
|
const password = fs.readFileSync('/run/secrets/test_user_password', 'utf8').trim();
|
||||||
|
const expectedReason = '100기 올스타 장수는 능력치 초기화를 사용할 수 없습니다.';
|
||||||
|
const expectedWarning = '100기 올스타 장수는 장수 전환 시 능력치 성장 기록을 보존하기 위해 능력치 초기화를 사용할 수 없습니다.';
|
||||||
|
|
||||||
|
if (!baseURL || !screenshotPath || !resultPath) {
|
||||||
|
throw new Error('REF_BROWSER_URL, REF_SCREENSHOT_PATH, and REF_RESULT_PATH are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await chromium.launch({headless: true});
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport: {width: 1280, height: 960},
|
||||||
|
deviceScaleFactor: 1,
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
const browserMessages = [];
|
||||||
|
page.on('console', message => {
|
||||||
|
browserMessages.push({type: message.type(), text: message.text()});
|
||||||
|
});
|
||||||
|
page.on('pageerror', error => {
|
||||||
|
browserMessages.push({type: 'pageerror', text: error.message});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(baseURL, {waitUntil: 'domcontentloaded', timeout: 60_000});
|
||||||
|
const salt = await page.locator('#global_salt').inputValue();
|
||||||
|
const passwordHash = createHash('sha512')
|
||||||
|
.update(salt + password + salt)
|
||||||
|
.digest('hex');
|
||||||
|
const loginResponse = await page.request.post(
|
||||||
|
new URL('api.php?path=Login/LoginByID', baseURL).href,
|
||||||
|
{data: {username, password: passwordHash}},
|
||||||
|
);
|
||||||
|
const loginResult = await loginResponse.json();
|
||||||
|
if (!loginResponse.ok() || loginResult.result !== true) {
|
||||||
|
throw new Error(`login failed: ${String(loginResult.reason ?? loginResponse.status())}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameLoginResponse = await page.request.get(
|
||||||
|
new URL('hwe/api.php?path=Global/GetConst', baseURL).href,
|
||||||
|
);
|
||||||
|
const gameLoginResult = await gameLoginResponse.json();
|
||||||
|
if (!gameLoginResponse.ok() || gameLoginResult.result !== true) {
|
||||||
|
throw new Error(`game login failed: ${JSON.stringify(gameLoginResult)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.goto(
|
||||||
|
new URL('hwe/v_inheritPoint.php', baseURL).href,
|
||||||
|
{waitUntil: 'networkidle', timeout: 60_000},
|
||||||
|
);
|
||||||
|
const bodyText = await page.locator('body').innerText();
|
||||||
|
if (bodyText.includes('#0 /var/www/html/')) {
|
||||||
|
throw new Error('PHP stack trace was rendered in the inheritance page');
|
||||||
|
}
|
||||||
|
if (!bodyText.includes(expectedWarning)) {
|
||||||
|
await page.screenshot({path: screenshotPath, fullPage: true});
|
||||||
|
fs.writeFileSync(
|
||||||
|
resultPath,
|
||||||
|
`${JSON.stringify({
|
||||||
|
username,
|
||||||
|
url: page.url(),
|
||||||
|
bodyText: bodyText.slice(0, 4000),
|
||||||
|
browserMessages,
|
||||||
|
}, null, 2)}\n`,
|
||||||
|
{mode: 0o600},
|
||||||
|
);
|
||||||
|
throw new Error('S100 stat-reset restriction was not rendered');
|
||||||
|
}
|
||||||
|
if (await page.getByRole('button', {name: '능력치 초기화'}).count() !== 0) {
|
||||||
|
throw new Error('S100 stat-reset button must not be rendered');
|
||||||
|
}
|
||||||
|
|
||||||
|
const warning = page.getByText(expectedWarning, {exact: true});
|
||||||
|
await warning.waitFor({state: 'visible', timeout: 60_000});
|
||||||
|
const warningGeometry = await warning.evaluate(element => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
return {
|
||||||
|
rect: {
|
||||||
|
x: rect.x,
|
||||||
|
y: rect.y,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
color: style.color,
|
||||||
|
fontFamily: style.fontFamily,
|
||||||
|
fontSize: style.fontSize,
|
||||||
|
lineHeight: style.lineHeight,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const beforeState = await page.evaluate(() => ({
|
||||||
|
currentStat: staticValues.currentStat,
|
||||||
|
previousPoint: staticValues.items.previous,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const resetResponse = await page.request.put(
|
||||||
|
new URL('hwe/api.php?path=InheritAction/ResetStat', baseURL).href,
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
leadership: 55,
|
||||||
|
strength: 55,
|
||||||
|
intel: 55,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const resetResult = await resetResponse.json();
|
||||||
|
if (!resetResponse.ok()
|
||||||
|
|| resetResult.result !== false
|
||||||
|
|| resetResult.reason !== expectedReason
|
||||||
|
) {
|
||||||
|
throw new Error(`unexpected ResetStat response: ${JSON.stringify(resetResult)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.reload({waitUntil: 'networkidle', timeout: 60_000});
|
||||||
|
const afterState = await page.evaluate(() => ({
|
||||||
|
currentStat: staticValues.currentStat,
|
||||||
|
previousPoint: staticValues.items.previous,
|
||||||
|
}));
|
||||||
|
if (JSON.stringify(afterState) !== JSON.stringify(beforeState)) {
|
||||||
|
throw new Error(`ResetStat rejection changed state: ${JSON.stringify({beforeState, afterState})}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.screenshot({path: screenshotPath, fullPage: true});
|
||||||
|
fs.writeFileSync(
|
||||||
|
resultPath,
|
||||||
|
`${JSON.stringify({
|
||||||
|
username,
|
||||||
|
url: page.url(),
|
||||||
|
viewport: {width: 1280, height: 960, deviceScaleFactor: 1},
|
||||||
|
warningGeometry,
|
||||||
|
resetResult,
|
||||||
|
beforeState,
|
||||||
|
afterState,
|
||||||
|
browserMessages,
|
||||||
|
}, null, 2)}\n`,
|
||||||
|
{mode: 0o600},
|
||||||
|
);
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log(`S100 inheritance stat-reset guard verified: ${screenshotPath}`);
|
||||||
Reference in New Issue
Block a user