From 4ab187b991ec40993c4b030e1612d9bb41f18924 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 13:04:31 +0100 Subject: Defer char.ini loading and use direct img src for char icons Instead of eagerly fetching char_icon (with HEAD requests per extension) and char.ini for every character on join, set img.src directly to char_icon.png and defer char.ini loading until actually needed (character selection via handlePV, or first IC message via handleMS). This eliminates thousands of HTTP requests on join for large character lists. Co-Authored-By: Claude Opus 4.6 --- webAO/packets/handlers/handleMS.ts | 5 ++++- webAO/packets/handlers/handlePU.ts | 6 ++++-- webAO/packets/handlers/handlePV.ts | 3 ++- webAO/packets/handlers/handleSC.ts | 7 ++----- webAO/packets/handlers/handleSI.ts | 1 + 5 files changed, 13 insertions(+), 9 deletions(-) (limited to 'webAO/packets/handlers') diff --git a/webAO/packets/handlers/handleMS.ts b/webAO/packets/handlers/handleMS.ts index 2622fe6..9b46bc0 100644 --- a/webAO/packets/handlers/handleMS.ts +++ b/webAO/packets/handlers/handleMS.ts @@ -1,7 +1,7 @@ /* eslint indent: ["error", 2, { "SwitchCase": 1 }] */ import { client, extrafeatures, UPDATE_INTERVAL } from "../../client"; -import { handleCharacterInfo } from "../../client/handleCharacterInfo"; +import { handleCharacterInfo, ensureCharIni } from "../../client/handleCharacterInfo"; import { resetICParams } from "../../client/resetICParams"; import { prepChat, safeTags } from "../../encoding"; import { handle_ic_speaking } from "../../viewport/utils/handleICSpeaking"; @@ -27,6 +27,9 @@ export const handleMS = (args: string[]) => { ); const chargs = (`${char_name}&` + "iniediter").split("&"); handleCharacterInfo(chargs, char_id); + } else if (!client.chars[char_id].inifile) { + // Lazily load char.ini in background so future messages have proper data + ensureCharIni(char_id); } } diff --git a/webAO/packets/handlers/handlePU.ts b/webAO/packets/handlers/handlePU.ts index 508bb51..18e508e 100644 --- a/webAO/packets/handlers/handlePU.ts +++ b/webAO/packets/handlers/handlePU.ts @@ -1,6 +1,6 @@ import { client } from "../../client"; -import { getCharIcon } from "../../client/handleCharacterInfo"; import { updatePlayerAreas } from "../../dom/updatePlayerAreas"; +import { AO_HOST } from "../../client/aoHost"; /** * Handles a playerlist update @@ -19,7 +19,9 @@ export const handlePU = (args: string[]) => { break; case 1: const playerImg = playerRow.childNodes[0].firstChild; - getCharIcon(playerImg, data); + playerImg.alt = data; + playerImg.title = data; + playerImg.src = `${AO_HOST}characters/${encodeURI(data.toLowerCase())}/char_icon.png`; const charName = playerRow.childNodes[1]; charName.innerText = `[${args[1]}] ${data}`; break; diff --git a/webAO/packets/handlers/handlePV.ts b/webAO/packets/handlers/handlePV.ts index 4ac747f..149d90e 100644 --- a/webAO/packets/handlers/handlePV.ts +++ b/webAO/packets/handlers/handlePV.ts @@ -3,6 +3,7 @@ import fileExists from "../../utils/fileExists"; import { updateActionCommands } from "../../dom/updateActionCommands"; import { pickEmotion } from "../../dom/pickEmotion"; import { AO_HOST } from "../../client/aoHost"; +import { ensureCharIni } from "../../client/handleCharacterInfo"; function addEmoteButton(i: number, imgurl: string, desc: string) { const emotesList = document.getElementById("client_emo"); @@ -34,7 +35,7 @@ export const handlePV = async (args: string[]) => { const emotesList = document.getElementById("client_emo"); emotesList.style.display = ""; emotesList.innerHTML = ""; // Clear emote box - const ini = me.inifile; + const ini = await ensureCharIni(client.charID); me.side = ini.options.side; updateActionCommands(me.side); if (ini.emotions.number === 0) { diff --git a/webAO/packets/handlers/handleSC.ts b/webAO/packets/handlers/handleSC.ts index 271a164..f7b789f 100644 --- a/webAO/packets/handlers/handleSC.ts +++ b/webAO/packets/handlers/handleSC.ts @@ -1,7 +1,7 @@ import queryParser from "../../utils/queryParser"; import { client } from "../../client"; -import { handleCharacterInfo } from "../../client/handleCharacterInfo"; +import { setupCharacterBasic } from "../../client/handleCharacterInfo"; const { mode } = queryParser(); /** @@ -17,13 +17,10 @@ export const handleSC = async (args: string[]) => { document.getElementById("client_charselect")!.style.display = "block"; } - document.getElementById("client_loadingtext")!.innerHTML = - "Loading Characters"; for (let i = 1; i < args.length; i++) { const chargs = args[i].split("&"); const charid = i - 1; - - setTimeout(() => handleCharacterInfo(chargs, charid), charid * 6); + setupCharacterBasic(chargs, charid); } // We're done with the characters, request the music client.sender.sendServer("RM#%"); diff --git a/webAO/packets/handlers/handleSI.ts b/webAO/packets/handlers/handleSI.ts index f20f4b2..eac84e0 100644 --- a/webAO/packets/handlers/handleSI.ts +++ b/webAO/packets/handlers/handleSI.ts @@ -20,6 +20,7 @@ export const handleSI = (args: string[]) => { const demothing = document.createElement("img"); demothing.className = "demothing"; + demothing.loading = "lazy"; demothing.id = `demo_${i}`; const demoonclick = document.createAttribute("onclick"); demoonclick.value = `pickChar(${i})`; -- cgit From 0c76b200cc68c59772df930acd34a58bd6272c7f Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 13:10:27 +0100 Subject: Catch rejected play() promises from browser autoplay policy Browsers reject .play() with a DOMException when the user hasn't interacted with the document yet. Add .catch(() => {}) to all 9 play() call sites to suppress the uncaught promise rejection. Co-Authored-By: Claude Opus 4.6 --- webAO/packets/handlers/handleMC.ts | 2 +- webAO/packets/handlers/handleRMC.ts | 2 +- webAO/packets/handlers/handleZZ.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'webAO/packets/handlers') diff --git a/webAO/packets/handlers/handleMC.ts b/webAO/packets/handlers/handleMC.ts index 8a1c0a2..44237ea 100644 --- a/webAO/packets/handlers/handleMC.ts +++ b/webAO/packets/handlers/handleMC.ts @@ -24,7 +24,7 @@ export const handleMC = (args: string[]) => { music.src = `${AO_HOST}sounds/music/${encodeURI(track.toLowerCase())}`; } music.loop = looping; - music.play(); + music.play().catch(() => {}); try { musicname = client.chars[charID].name; diff --git a/webAO/packets/handlers/handleRMC.ts b/webAO/packets/handlers/handleRMC.ts index 7758844..0cc6aa7 100644 --- a/webAO/packets/handlers/handleRMC.ts +++ b/webAO/packets/handlers/handleRMC.ts @@ -17,7 +17,7 @@ export const handleRMC = (args: string[]) => { music.currentTime += parseFloat( music.totime + (new Date().getTime() / 1000 - music.offset), ).toFixed(3); - music.play(); + music.play().catch(() => {}); }, false, ); diff --git a/webAO/packets/handlers/handleZZ.ts b/webAO/packets/handlers/handleZZ.ts index 31ceef5..0496d42 100644 --- a/webAO/packets/handlers/handleZZ.ts +++ b/webAO/packets/handlers/handleZZ.ts @@ -18,6 +18,6 @@ export const handleZZ = (args: string[]) => { const oldvolume = client.viewport.getSfxAudio().volume; client.viewport.getSfxAudio().volume = 1; client.viewport.getSfxAudio().src = `${AO_HOST}sounds/general/sfx-gallery.opus`; - client.viewport.getSfxAudio().play(); + client.viewport.getSfxAudio().play().catch(() => {}); client.viewport.getSfxAudio().volume = oldvolume; }; -- cgit From 5eeb7ac9d90137c3b5ce9578c47bcc2ccff21c7e Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 21:10:00 +0100 Subject: Use charicon_extensions from extensions.json for char icon URLs Instead of hardcoding .png, read the preferred extension from client.charicon_extensions[0] (populated via extensions.json), falling back to .png if unavailable. Co-Authored-By: Claude Opus 4.6 --- webAO/packets/handlers/handlePU.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'webAO/packets/handlers') diff --git a/webAO/packets/handlers/handlePU.ts b/webAO/packets/handlers/handlePU.ts index 18e508e..0f51029 100644 --- a/webAO/packets/handlers/handlePU.ts +++ b/webAO/packets/handlers/handlePU.ts @@ -21,7 +21,8 @@ export const handlePU = (args: string[]) => { const playerImg = playerRow.childNodes[0].firstChild; playerImg.alt = data; playerImg.title = data; - playerImg.src = `${AO_HOST}characters/${encodeURI(data.toLowerCase())}/char_icon.png`; + const iconExt = client.charicon_extensions[0] || ".png"; + playerImg.src = `${AO_HOST}characters/${encodeURI(data.toLowerCase())}/char_icon${iconExt}`; const charName = playerRow.childNodes[1]; charName.innerText = `[${args[1]}] ${data}`; break; -- cgit From 95e4124b0c02b4e5be383c41ed4241566f40e6e6 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 21:53:59 +0100 Subject: Prefetch char.ini for characters present in area via playerlist Store player data (charId, area) in an in-memory Map on the client, updated by PR/PU packet handlers. Use this to eagerly load char.ini when a player's character appears in our area or when switching areas, eliminating the lazy-load delay on first IC message. Co-Authored-By: Claude Opus 4.6 --- webAO/packets/handlers/handlePR.ts | 9 +++++++-- webAO/packets/handlers/handlePU.ts | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) (limited to 'webAO/packets/handlers') diff --git a/webAO/packets/handlers/handlePR.ts b/webAO/packets/handlers/handlePR.ts index aeb3969..e39103d 100644 --- a/webAO/packets/handlers/handlePR.ts +++ b/webAO/packets/handlers/handlePR.ts @@ -53,6 +53,11 @@ function removePlayer(playerID: number) { */ export const handlePR = (args: string[]) => { const playerID = Number(args[1]); - if (Number(args[2]) === 0) addPlayer(playerID); - else if (Number(args[2]) === 1) removePlayer(playerID); + if (Number(args[2]) === 0) { + addPlayer(playerID); + client.players.set(playerID, { charId: -1, area: 0 }); + } else if (Number(args[2]) === 1) { + removePlayer(playerID); + client.players.delete(playerID); + } }; diff --git a/webAO/packets/handlers/handlePU.ts b/webAO/packets/handlers/handlePU.ts index 0f51029..d8d9b44 100644 --- a/webAO/packets/handlers/handlePU.ts +++ b/webAO/packets/handlers/handlePU.ts @@ -1,6 +1,7 @@ import { client } from "../../client"; import { updatePlayerAreas } from "../../dom/updatePlayerAreas"; import { AO_HOST } from "../../client/aoHost"; +import { ensureCharIni } from "../../client/handleCharacterInfo"; /** * Handles a playerlist update @@ -25,6 +26,18 @@ export const handlePU = (args: string[]) => { playerImg.src = `${AO_HOST}characters/${encodeURI(data.toLowerCase())}/char_icon${iconExt}`; const charName = playerRow.childNodes[1]; charName.innerText = `[${args[1]}] ${data}`; + const charId = client.chars.findIndex( + (c: any) => c && c.name.toLowerCase() === data.toLowerCase() + ); + if (charId >= 0) { + const player = client.players.get(Number(args[1])); + if (player) { + player.charId = charId; + if (player.area === client.area) { + ensureCharIni(charId); + } + } + } break; case 2: const showName = playerRow.childNodes[2]; @@ -33,6 +46,13 @@ export const handlePU = (args: string[]) => { case 3: playerRow.className = `area${data}`; updatePlayerAreas(client.area); + const puPlayer = client.players.get(Number(args[1])); + if (puPlayer) { + puPlayer.area = Number(data); + if (puPlayer.area === client.area && puPlayer.charId >= 0) { + ensureCharIni(puPlayer.charId); + } + } default: break; } -- cgit From f26d35429e66ddcdd02c10f57b315b0f02b4add2 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 22:21:42 +0100 Subject: Fix playerlist icons using wrong asset URL PR/PU packets arrive before the ASS packet, so playerlist icon srcs were set with the default AO_HOST. Now handleASS re-applies the correct asset URL to existing playerlist images after AO_HOST updates. Co-Authored-By: Claude Opus 4.6 --- webAO/packets/handlers/handleASS.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'webAO/packets/handlers') diff --git a/webAO/packets/handlers/handleASS.ts b/webAO/packets/handlers/handleASS.ts index a46d68e..1ecfd04 100644 --- a/webAO/packets/handlers/handleASS.ts +++ b/webAO/packets/handlers/handleASS.ts @@ -1,4 +1,5 @@ -import { setAOhost } from "../../client/aoHost"; +import { setAOhost, AO_HOST } from "../../client/aoHost"; +import { client } from "../../client"; /** * new asset url!! @@ -6,4 +7,20 @@ import { setAOhost } from "../../client/aoHost"; */ export const handleASS = (args: string[]) => { if (args[1] !== "None") setAOhost(args[1]); + + // Re-apply playerlist icon srcs that were set before AO_HOST was known + const iconExt = client.charicon_extensions[0] || ".png"; + for (const [playerID, player] of client.players) { + if (player.charId >= 0) { + const char = client.chars[player.charId]; + if (char) { + const img = document.querySelector( + `#client_playerlist_entry${playerID} img` + ); + if (img) { + img.src = `${AO_HOST}characters/${encodeURI(char.name.toLowerCase())}/char_icon${iconExt}`; + } + } + } + } }; -- cgit From 9c68a1afcf178a86063f094b96471fa73531bd9a Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 7 Feb 2026 23:01:25 +0100 Subject: Use setAOhost return value instead of stale AO_HOST import setAOhost now returns the current AO_HOST so handleASS can use the freshly set value rather than the import captured before the update. Co-Authored-By: Claude Opus 4.6 --- webAO/packets/handlers/handleASS.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'webAO/packets/handlers') diff --git a/webAO/packets/handlers/handleASS.ts b/webAO/packets/handlers/handleASS.ts index 1ecfd04..092e4f9 100644 --- a/webAO/packets/handlers/handleASS.ts +++ b/webAO/packets/handlers/handleASS.ts @@ -1,4 +1,4 @@ -import { setAOhost, AO_HOST } from "../../client/aoHost"; +import { setAOhost } from "../../client/aoHost"; import { client } from "../../client"; /** @@ -6,7 +6,7 @@ import { client } from "../../client"; * @param {Array} args packet arguments */ export const handleASS = (args: string[]) => { - if (args[1] !== "None") setAOhost(args[1]); + const host = args[1] !== "None" ? setAOhost(args[1]) : args[1]; // Re-apply playerlist icon srcs that were set before AO_HOST was known const iconExt = client.charicon_extensions[0] || ".png"; @@ -18,7 +18,7 @@ export const handleASS = (args: string[]) => { `#client_playerlist_entry${playerID} img` ); if (img) { - img.src = `${AO_HOST}characters/${encodeURI(char.name.toLowerCase())}/char_icon${iconExt}`; + img.src = `${host}characters/${encodeURI(char.name.toLowerCase())}/char_icon${iconExt}`; } } } -- cgit From 020dfcda00ca06b9a06e7076eaf8a0164ae1327e Mon Sep 17 00:00:00 2001 From: David Skoland Date: Tue, 10 Feb 2026 23:38:17 +0100 Subject: Refactor playerlist to state-driven rendering with renderPlayerList handlePR and handlePU now only update client.playerlist state, and renderPlayerList handles all DOM rendering from that state. Co-Authored-By: Claude Opus 4.6 --- webAO/packets/handlers/handleASS.ts | 21 ++------------ webAO/packets/handlers/handlePR.ts | 55 +++---------------------------------- webAO/packets/handlers/handlePU.ts | 47 ++++++++++--------------------- 3 files changed, 22 insertions(+), 101 deletions(-) (limited to 'webAO/packets/handlers') diff --git a/webAO/packets/handlers/handleASS.ts b/webAO/packets/handlers/handleASS.ts index 092e4f9..c50443a 100644 --- a/webAO/packets/handlers/handleASS.ts +++ b/webAO/packets/handlers/handleASS.ts @@ -1,26 +1,11 @@ import { setAOhost } from "../../client/aoHost"; -import { client } from "../../client"; +import { renderPlayerList } from "../../dom/renderPlayerList"; /** * new asset url!! * @param {Array} args packet arguments */ export const handleASS = (args: string[]) => { - const host = args[1] !== "None" ? setAOhost(args[1]) : args[1]; - - // Re-apply playerlist icon srcs that were set before AO_HOST was known - const iconExt = client.charicon_extensions[0] || ".png"; - for (const [playerID, player] of client.players) { - if (player.charId >= 0) { - const char = client.chars[player.charId]; - if (char) { - const img = document.querySelector( - `#client_playerlist_entry${playerID} img` - ); - if (img) { - img.src = `${host}characters/${encodeURI(char.name.toLowerCase())}/char_icon${iconExt}`; - } - } - } - } + if (args[1] !== "None") setAOhost(args[1]); + renderPlayerList(); }; diff --git a/webAO/packets/handlers/handlePR.ts b/webAO/packets/handlers/handlePR.ts index e39103d..f81d8dc 100644 --- a/webAO/packets/handlers/handlePR.ts +++ b/webAO/packets/handlers/handlePR.ts @@ -1,51 +1,5 @@ import { client } from "../../client"; -import { kickPlayer, banPlayer } from "../../dom/banPlayer"; - -function addPlayer(playerID: number) { - const list = document.getElementById("client_playerlist"); - const playerRow = list.insertRow(); - playerRow.id = `client_playerlist_entry${playerID}`; - playerRow.className = `area0`; - - const imgCell = playerRow.insertCell(0); - imgCell.style.width = "64px"; - const img = document.createElement("img"); - imgCell.appendChild(img); - - const name = document.createTextNode("No Data"); - - const charNameCell = playerRow.insertCell(1); - charNameCell.appendChild(name); - const showNameCell = playerRow.insertCell(2); - showNameCell.appendChild(name); - const oocNameCell = playerRow.insertCell(3); - oocNameCell.appendChild(name); - - const kickCell = playerRow.insertCell(4); - kickCell.style.width = "64px"; - const kick = document.createElement("button"); - kick.innerText = "Kick"; - kick.onclick = () => { - window.kickPlayer(playerID); - }; - kickCell.appendChild(kick); - - const banCell = playerRow.insertCell(5); - banCell.style.width = "64px"; - const ban = document.createElement("button"); - ban.innerText = "Ban"; - ban.onclick = () => { - window.banPlayer(playerID); - }; - banCell.appendChild(ban); -} - -function removePlayer(playerID: number) { - const playerRow = ( - document.getElementById(`client_playerlist_entry${playerID}`) - ); - playerRow.remove(); -} +import { renderPlayerList } from "../../dom/renderPlayerList"; /** * Handles a player joining or leaving @@ -54,10 +8,9 @@ function removePlayer(playerID: number) { export const handlePR = (args: string[]) => { const playerID = Number(args[1]); if (Number(args[2]) === 0) { - addPlayer(playerID); - client.players.set(playerID, { charId: -1, area: 0 }); + client.playerlist.set(playerID, { charId: -1, charName: "", showName: "", name: "", area: 0 }); } else if (Number(args[2]) === 1) { - removePlayer(playerID); - client.players.delete(playerID); + client.playerlist.delete(playerID); } + renderPlayerList(); }; diff --git a/webAO/packets/handlers/handlePU.ts b/webAO/packets/handlers/handlePU.ts index d8d9b44..3b70ad3 100644 --- a/webAO/packets/handlers/handlePU.ts +++ b/webAO/packets/handlers/handlePU.ts @@ -1,59 +1,42 @@ import { client } from "../../client"; -import { updatePlayerAreas } from "../../dom/updatePlayerAreas"; -import { AO_HOST } from "../../client/aoHost"; import { ensureCharIni } from "../../client/handleCharacterInfo"; +import { renderPlayerList } from "../../dom/renderPlayerList"; /** * Handles a playerlist update * @param {Array} args packet arguments */ export const handlePU = (args: string[]) => { - const playerRow = ( - document.getElementById(`client_playerlist_entry${Number(args[1])}`) - ); + const playerID = Number(args[1]); + const player = client.playerlist.get(playerID); + if (!player) return; + const type = Number(args[2]); const data = args[3]; + switch (type) { case 0: - const oocName = playerRow.childNodes[3]; - oocName.innerText = data; + player.name = data; break; case 1: - const playerImg = playerRow.childNodes[0].firstChild; - playerImg.alt = data; - playerImg.title = data; - const iconExt = client.charicon_extensions[0] || ".png"; - playerImg.src = `${AO_HOST}characters/${encodeURI(data.toLowerCase())}/char_icon${iconExt}`; - const charName = playerRow.childNodes[1]; - charName.innerText = `[${args[1]}] ${data}`; + player.charName = data; const charId = client.chars.findIndex( (c: any) => c && c.name.toLowerCase() === data.toLowerCase() ); if (charId >= 0) { - const player = client.players.get(Number(args[1])); - if (player) { - player.charId = charId; - if (player.area === client.area) { - ensureCharIni(charId); - } - } + player.charId = charId; + ensureCharIni(charId); } break; case 2: - const showName = playerRow.childNodes[2]; - showName.innerText = data; + player.showName = data; break; case 3: - playerRow.className = `area${data}`; - updatePlayerAreas(client.area); - const puPlayer = client.players.get(Number(args[1])); - if (puPlayer) { - puPlayer.area = Number(data); - if (puPlayer.area === client.area && puPlayer.charId >= 0) { - ensureCharIni(puPlayer.charId); - } - } + player.area = Number(data); + break; default: break; } + + renderPlayerList(); }; -- cgit