diff --git a/server/PATCHES.md b/server/PATCHES.md index 4234f3177..297ea65f5 100644 --- a/server/PATCHES.md +++ b/server/PATCHES.md @@ -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 diff --git a/server/engine/src/server/login/LoginServer.ts b/server/engine/src/server/login/LoginServer.ts index 039e33316..25b4084ac 100644 --- a/server/engine/src/server/login/LoginServer.ts +++ b/server/engine/src/server/login/LoginServer.ts @@ -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) {