fix(hiscores): stamp playtime only when a skill's level changes

Skill and overall rows rewrote playtime on every logout, even with no XP
gained. Playtime is the tiebreak (level desc, playtime asc), so maxed
players slid down the board for continuing to play. Now playtime records
when the current level was reached; XP-only changes still update value.
Matches the quest points board.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
max bittkerandClaude Opus 5.5 committed 2026-10-07 15:36:42 -04:00
1 parent ab59cc31bf
commit 415491a1cc
2 files changed
+16 -6

No files matched your search

+7
View File
@@ -104,6 +104,13 @@ survival) is described in the project memory; this file is the human-readable ch
outfit hiscore write; `/hiscores/outfit` shows a `/sprite/player` image for the top 10 and
`/sprite/item` icons (no in-browser viewer bundle). Sprites come from `web/sprites/`
(worker hosting webclient `src/viewer/ItemViewer.ts` `renderPlayerSprite`).
- [ ] **Hiscore playtime tiebreak** — `hiscore.playtime`/`hiscore_large.playtime` (migration
`20260208000000_hiscore_playtime_and_outfit`); boards rank level desc, playtime asc.
`LoginServer.ts` `updateHiscores()` stamps playtime only when a row's **level** changes
(XP-only changes keep it), so it means "playtime when this level was reached" — restamping
on every logout pushed maxed players down for continuing to play. Quest points (type 100,
`updateQuestHiscore()`) follows the same rule.
Verify: `grep -n "existing.level === totalLevel ? existing.playtime" src/server/login/LoginServer.ts`
- [ ] **Worker-thread crash hardening** — `src/server/InternalClient.ts` uses persistent
`.on('close'/'error')` handlers instead of `.once()` (a second ws error after a
successful open was an unhandled EventEmitter 'error' that killed the worker → every
@@ -115,15 +115,18 @@ async function updateHiscores(account: HiscoreAccount | undefined, player: Playe
totalLevel += player.baseLevels[i];
}
const existing = await db.selectFrom('hiscore_large').select('type').select('value').select('playtime').where('account_id', '=', account.id).where('type', '=', 0).where('profile', '=', profile).executeTakeFirst();
if (existing && (existing.value !== totalXp || existing.playtime !== player.playtime)) {
// rs-sdk: playtime is the tiebreak (level desc, playtime asc), so it's only stamped when the
// level changes - it records when the player reached that level. Restamping it on every
// logout pushed maxed players down the board for continuing to play.
const existing = await db.selectFrom('hiscore_large').select('type').select('level').select('value').select('playtime').where('account_id', '=', account.id).where('type', '=', 0).where('profile', '=', profile).executeTakeFirst();
if (existing && (existing.value !== totalXp || existing.level !== totalLevel)) {
await db
.updateTable('hiscore_large')
.set({
type: 0,
level: totalLevel,
value: totalXp,
playtime: player.playtime,
playtime: existing.level === totalLevel ? existing.playtime : player.playtime,
date: toDbDate(new Date())
})
.where('account_id', '=', account.id)
@@ -153,13 +156,13 @@ async function updateHiscores(account: HiscoreAccount | undefined, player: Playe
const hiscoreType = stat + 1;
// todo: can we upsert in kysely?
const existing = await db.selectFrom('hiscore').select('type').select('value').select('playtime').where('account_id', '=', account.id).where('type', '=', hiscoreType).where('profile', '=', profile).executeTakeFirst();
if (existing && (existing.value !== player.stats[stat] || existing.playtime !== player.playtime)) {
const existing = await db.selectFrom('hiscore').select('type').select('level').select('value').select('playtime').where('account_id', '=', account.id).where('type', '=', hiscoreType).where('profile', '=', profile).executeTakeFirst();
if (existing && (existing.value !== player.stats[stat] || existing.level !== player.baseLevels[stat])) {
update.push({
type: hiscoreType,
level: player.baseLevels[stat],
value: player.stats[stat],
playtime: player.playtime,
playtime: existing.level === player.baseLevels[stat] ? existing.playtime : player.playtime,
date: toDbDate(new Date())
});
} else if (!existing) {