aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Skoland <davidskoland@gmail.com>2026-02-07 13:04:31 +0100
committerDavid Skoland <davidskoland@gmail.com>2026-02-07 13:04:31 +0100
commit4ab187b991ec40993c4b030e1612d9bb41f18924 (patch)
treed715237d1f6a1652e9f157ec2b78b72da309f2e0
parent7391a61b881af5a515abfbf5905f65e749814fa4 (diff)
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 <noreply@anthropic.com>
-rw-r--r--webAO/client/handleCharacterInfo.ts174
-rw-r--r--webAO/packets/handlers/handleMS.ts5
-rw-r--r--webAO/packets/handlers/handlePU.ts6
-rw-r--r--webAO/packets/handlers/handlePV.ts3
-rw-r--r--webAO/packets/handlers/handleSC.ts7
-rw-r--r--webAO/packets/handlers/handleSI.ts1
6 files changed, 116 insertions, 80 deletions
diff --git a/webAO/client/handleCharacterInfo.ts b/webAO/client/handleCharacterInfo.ts
index 3764c84..0235f3b 100644
--- a/webAO/client/handleCharacterInfo.ts
+++ b/webAO/client/handleCharacterInfo.ts
@@ -2,50 +2,21 @@ import { client } from "../client";
import { safeTags } from "../encoding";
import iniParse from "../iniParse";
import request from "../services/request";
-import fileExists from "../utils/fileExists";
import { AO_HOST } from "./aoHost";
-export const getCharIcon = async (img: HTMLImageElement, charname: string) => {
- img.alt = charname;
- const charIconBaseUrl = `${AO_HOST}characters/${encodeURI(
- charname.toLowerCase(),
- )}/char_icon`;
- for (let i = 0; i < client.charicon_extensions.length; i++) {
- const fileUrl = charIconBaseUrl + client.charicon_extensions[i];
- const exists = await fileExists(fileUrl);
- if (exists) {
- img.alt = charname;
- img.title = charname;
- img.src = fileUrl;
- return;
- }
- }
-};
-
/**
- * Handles the incoming character information, and downloads the sprite + ini for it
- * @param {Array} chargs packet arguments
- * @param {Number} charid character ID
+ * Lightweight character setup that runs on join. Sets the icon src directly
+ * (letting the browser handle loading) and stores default character data.
+ * Does NOT fetch char.ini — that is deferred until needed via ensureCharIni.
*/
-export const handleCharacterInfo = async (chargs: string[], charid: number) => {
+export const setupCharacterBasic = (chargs: string[], charid: number) => {
const img = <HTMLImageElement>document.getElementById(`demo_${charid}`);
if (chargs[0]) {
- let cini: any = {};
-
- getCharIcon(img, chargs[0]);
-
- // If the ini doesn't exist on the server this will throw an error
- try {
- const cinidata = await request(
- `${AO_HOST}characters/${encodeURI(chargs[0].toLowerCase())}/char.ini`,
- );
- cini = iniParse(cinidata);
- } catch (err) {
- cini = {};
- img.classList.add("noini");
- console.warn(`character ${chargs[0]} is missing from webAO`);
- // If it does, give the user a visual indication that the character is unusable
- }
+ img.alt = chargs[0];
+ img.title = chargs[0];
+ img.src = `${AO_HOST}characters/${encodeURI(
+ chargs[0].toLowerCase(),
+ )}/char_icon.png`;
const mute_select = <HTMLSelectElement>(
document.getElementById("mute_select")
@@ -56,47 +27,108 @@ export const handleCharacterInfo = async (chargs: string[], charid: number) => {
);
pair_select.add(new Option(safeTags(chargs[0]), String(charid)));
- // sometimes ini files lack important settings
- const default_options = {
- name: chargs[0],
- showname: chargs[0],
- side: "def",
- blips: "male",
- chat: "",
- category: "",
- };
- cini.options = Object.assign(default_options, cini.options);
-
- // sometimes ini files lack important settings
- const default_emotions = {
- number: 0,
- };
- cini.emotions = Object.assign(default_emotions, cini.emotions);
-
+ // Store defaults — these get replaced with actual ini values by ensureCharIni
client.chars[charid] = {
name: safeTags(chargs[0]),
- showname: safeTags(cini.options.showname),
+ showname: safeTags(chargs[0]),
desc: safeTags(chargs[1]),
- blips: safeTags(cini.options.blips).toLowerCase(),
- gender: safeTags(cini.options.gender).toLowerCase(),
- side: safeTags(cini.options.side).toLowerCase(),
- chat:
- cini.options.chat === ""
- ? safeTags(cini.options.category).toLowerCase()
- : safeTags(cini.options.chat).toLowerCase(),
+ blips: "male",
+ gender: "",
+ side: "def",
+ chat: "",
evidence: chargs[3],
- icon: img.src,
- inifile: cini,
+ icon: "",
muted: false,
};
+ } else {
+ console.warn(`missing charid ${charid}`);
+ img.style.display = "none";
+ }
+};
- if (
- client.chars[charid].blips === "male" &&
- client.chars[charid].gender !== "male" &&
- client.chars[charid].gender !== ""
- ) {
- client.chars[charid].blips = client.chars[charid].gender;
+/**
+ * Fetches and parses char.ini for a character if not already loaded.
+ * Replaces default values in client.chars[charid] with actual ini values.
+ */
+export const ensureCharIni = async (charid: number): Promise<any> => {
+ const char = client.chars[charid];
+ if (!char) return {};
+ if (char.inifile) return char.inifile;
+
+ const img = <HTMLImageElement>document.getElementById(`demo_${charid}`);
+ let cini: any = {};
+
+ try {
+ const cinidata = await request(
+ `${AO_HOST}characters/${encodeURI(char.name.toLowerCase())}/char.ini`,
+ );
+ cini = iniParse(cinidata);
+ } catch (err) {
+ cini = {};
+ if (img) img.classList.add("noini");
+ console.warn(`character ${char.name} is missing from webAO`);
+ }
+
+ const default_options = {
+ name: char.name,
+ showname: char.name,
+ side: "def",
+ blips: "male",
+ chat: "",
+ category: "",
+ };
+ cini.options = Object.assign(default_options, cini.options);
+
+ const default_emotions = {
+ number: 0,
+ };
+ cini.emotions = Object.assign(default_emotions, cini.emotions);
+
+ // Replace defaults with actual ini values
+ char.showname = safeTags(cini.options.showname);
+ char.blips = safeTags(cini.options.blips).toLowerCase();
+ char.gender = safeTags(cini.options.gender).toLowerCase();
+ char.side = safeTags(cini.options.side).toLowerCase();
+ char.chat =
+ cini.options.chat === ""
+ ? safeTags(cini.options.category).toLowerCase()
+ : safeTags(cini.options.chat).toLowerCase();
+ char.icon = img ? img.src : "";
+ char.inifile = cini;
+
+ if (
+ char.blips === "male" &&
+ char.gender !== "male" &&
+ char.gender !== ""
+ ) {
+ char.blips = char.gender;
+ }
+
+ return cini;
+};
+
+/**
+ * Full character info load (used by iniEdit and handleMS ini-edit path).
+ * Fetches icon + ini for a single character, replacing any existing data.
+ */
+export const handleCharacterInfo = async (chargs: string[], charid: number) => {
+ const img = <HTMLImageElement>document.getElementById(`demo_${charid}`);
+ if (chargs[0]) {
+ img.alt = chargs[0];
+ img.title = chargs[0];
+ img.src = `${AO_HOST}characters/${encodeURI(
+ chargs[0].toLowerCase(),
+ )}/char_icon.png`;
+
+ // Reset inifile so ensureCharIni will re-fetch
+ if (client.chars[charid]) {
+ client.chars[charid].name = safeTags(chargs[0]);
+ client.chars[charid].inifile = null;
+ } else {
+ setupCharacterBasic(chargs, charid);
}
+
+ await ensureCharIni(charid);
} else {
console.warn(`missing charid ${charid}`);
img.style.display = "none";
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 = <HTMLImageElement>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 = <HTMLElement>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})`;