<feed xmlns='http://www.w3.org/2005/Atom'>
<title>webao/webAO/client, branch master</title>
<subtitle>WebAO fork</subtitle>
<link rel='alternate' type='text/html' href='https://git.sof.beauty/webao/'/>
<entry>
<title>Replace cookies with localStorage</title>
<updated>2026-04-18T16:52:23+00:00</updated>
<author>
<name>Osmium Sorcerer</name>
<email>os@sof.beauty</email>
</author>
<published>2026-04-07T02:55:26+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sof.beauty/webao/commit/?id=4bd750ca1f3e446f68e0f88fabf0682fd4d61848'/>
<id>4bd750ca1f3e446f68e0f88fabf0682fd4d61848</id>
<content type='text'>
Cookies's use case is to store persistent data and send it to the server
in subsequent requests, such as to remember logged-in sessions. WebAO is
using them to store site settings like ad-hoc hash tables that require
parsing and serialization.

As a nasty side-effect of how cookies work, clients send all their
settings every time they connect to the server. Server has absolutely no
use for them, but each client sends them anyway, which is an
uncalled-for privacy leak.

Remove this mechanism entirely, switch to localStorage which serves
exactly the purpose of per-origin store with data that never leaves the
browser.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Cookies's use case is to store persistent data and send it to the server
in subsequent requests, such as to remember logged-in sessions. WebAO is
using them to store site settings like ad-hoc hash tables that require
parsing and serialization.

As a nasty side-effect of how cookies work, clients send all their
settings every time they connect to the server. Server has absolutely no
use for them, but each client sends them anyway, which is an
uncalled-for privacy leak.

Remove this mechanism entirely, switch to localStorage which serves
exactly the purpose of per-origin store with data that never leaves the
browser.
</pre>
</div>
</content>
</entry>
<entry>
<title>Temporarily default to blips value "m"</title>
<updated>2026-04-18T16:52:22+00:00</updated>
<author>
<name>Osmium Sorcerer</name>
<email>os@sof.beauty</email>
</author>
<published>2026-04-06T22:05:03+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sof.beauty/webao/commit/?id=1c458e5841ae30bed8b6f3107d3f65353d9b731c'/>
<id>1c458e5841ae30bed8b6f3107d3f65353d9b731c</id>
<content type='text'>
Blips aren't handled correctly every time, resulting in a lot of 404
URLs and invalid blips.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Blips aren't handled correctly every time, resulting in a lot of 404
URLs and invalid blips.
</pre>
</div>
</content>
</entry>
<entry>
<title>Separate the MC packet into music and area change</title>
<updated>2026-04-18T16:52:22+00:00</updated>
<author>
<name>Osmium Sorcerer</name>
<email>os@sof.beauty</email>
</author>
<published>2026-03-16T16:33:54+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sof.beauty/webao/commit/?id=3f1140da7779f568137d62b3f35392edc9e02e1e'/>
<id>3f1140da7779f568137d62b3f35392edc9e02e1e</id>
<content type='text'>
Historically, MC packet ended up in a ridiculous spot. It had this
single structure:

MC#something#cid#%

It used to change music track to `something`, and the character ID `cid`
was used in clientside muting (blindly trusted, by the way). Then,
this packet was expanded to mean area change as well, so the same
generic structure carried two completely different meanings.

How does one differentiate the two? Whether the client tried to move to
an area `something`, or played a music track called `something`?

The solution was to assume that having ".extension" at the end magically
implied that it was a name of a music file, check the string `something`
within the MC packet, and pray that you guessed correctly. So,
understanding the protocol message required penetrating into one of its
data fields and ambiguously inferring what the whole message even meant.

Modern AO gives us a more logical solution. Not as good as having two
separate packets for two unrelated actions, but we can at least discern
the area and music change directly from the framing.

Area change uses the same two-field structure: MC#area#cid#%

Music change, however, has acquired two additional fields:
MC#music#cid#showname#flags#%

We consider four-field MC to be music change, and two-field MC to be
area change, resolving the ambiguity and eliminating odd constraints
on area and music names.

WebAO still uses the old logic and sends two-field MC packets for both
cases. CSDWASASH server, as a result, thinks that web users try to
change areas when they play music. This commit fixes this behavior and
adds special sendAreaChange instead of using sendMusicChange for both.
The flags are hardcoded to 0 because WebAO can't set fade-in, fade-out,
or position sync, and it ignores the server flags.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Historically, MC packet ended up in a ridiculous spot. It had this
single structure:

MC#something#cid#%

It used to change music track to `something`, and the character ID `cid`
was used in clientside muting (blindly trusted, by the way). Then,
this packet was expanded to mean area change as well, so the same
generic structure carried two completely different meanings.

How does one differentiate the two? Whether the client tried to move to
an area `something`, or played a music track called `something`?

The solution was to assume that having ".extension" at the end magically
implied that it was a name of a music file, check the string `something`
within the MC packet, and pray that you guessed correctly. So,
understanding the protocol message required penetrating into one of its
data fields and ambiguously inferring what the whole message even meant.

Modern AO gives us a more logical solution. Not as good as having two
separate packets for two unrelated actions, but we can at least discern
the area and music change directly from the framing.

Area change uses the same two-field structure: MC#area#cid#%

Music change, however, has acquired two additional fields:
MC#music#cid#showname#flags#%

We consider four-field MC to be music change, and two-field MC to be
area change, resolving the ambiguity and eliminating odd constraints
on area and music names.

WebAO still uses the old logic and sends two-field MC packets for both
cases. CSDWASASH server, as a result, thinks that web users try to
change areas when they play music. This commit fixes this behavior and
adds special sendAreaChange instead of using sendMusicChange for both.
The flags are hardcoded to 0 because WebAO can't set fade-in, fade-out,
or position sync, and it ignores the server flags.
</pre>
</div>
</content>
</entry>
<entry>
<title>Change image extension priority</title>
<updated>2026-04-18T16:52:22+00:00</updated>
<author>
<name>Osmium Sorcerer</name>
<email>os@sof.beauty</email>
</author>
<published>2026-03-16T16:19:15+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sof.beauty/webao/commit/?id=29571c0da3b3a588b57125e5dc56eaa78639c1b7'/>
<id>29571c0da3b3a588b57125e5dc56eaa78639c1b7</id>
<content type='text'>
Sometimes, WebP icons won't load despite extensions.json clearly
defining it as the only extension used for all image data.

I suspect there's a race condition between fetching extensions.json,
parsing it into client, and checking what extension we should use to get
character icons during loading. Sometimes it correctly loads images,
sometimes it falls back and starts requesting PNG instead.

I couldn't precisely identify where it happens and what's the root
cause. As a workaround, this commit instead makes WebP the
first-priority extension and a fallback.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Sometimes, WebP icons won't load despite extensions.json clearly
defining it as the only extension used for all image data.

I suspect there's a race condition between fetching extensions.json,
parsing it into client, and checking what extension we should use to get
character icons during loading. Sometimes it correctly loads images,
sometimes it falls back and starts requesting PNG instead.

I couldn't precisely identify where it happens and what's the root
cause. As a workaround, this commit instead makes WebP the
first-priority extension and a fallback.
</pre>
</div>
</content>
</entry>
<entry>
<title>Remove hardcoded (a) and (b) emote subdirectories</title>
<updated>2026-04-18T16:52:22+00:00</updated>
<author>
<name>Osmium Sorcerer</name>
<email>os@sof.beauty</email>
</author>
<published>2026-03-16T15:59:07+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sof.beauty/webao/commit/?id=4dc891716ba6fb33dc35e9908283c1b6b9755efd'/>
<id>4dc891716ba6fb33dc35e9908283c1b6b9755efd</id>
<content type='text'>
Note: this is an old commit that was relevant prior to 10b413c
("Add asset preloading system for IC message rendering")

WebAO assumes every character has an idle and talking animations which
reside in fixed "(a)" and "(b)" subdirectories. This assumption, of
course, breaks many valid characters that don't use these magical
directory names, as it prepends them to every emote URL.

This commit removes this fossil. Animation, or any character structure
at all, shouldn't depend on magical subdirectory names, and instead
defined explicitly.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Note: this is an old commit that was relevant prior to 10b413c
("Add asset preloading system for IC message rendering")

WebAO assumes every character has an idle and talking animations which
reside in fixed "(a)" and "(b)" subdirectories. This assumption, of
course, breaks many valid characters that don't use these magical
directory names, as it prepends them to every emote URL.

This commit removes this fossil. Animation, or any character structure
at all, shouldn't depend on magical subdirectory names, and instead
defined explicitly.
</pre>
</div>
</content>
</entry>
<entry>
<title>Remove toLowerCase mangling</title>
<updated>2026-04-18T16:52:22+00:00</updated>
<author>
<name>Osmium Sorcerer</name>
<email>os@sof.beauty</email>
</author>
<published>2026-03-16T14:35:42+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sof.beauty/webao/commit/?id=8538104fd5573ba5eeade30ee4a20893224960f9'/>
<id>8538104fd5573ba5eeade30ee4a20893224960f9</id>
<content type='text'>
For whatever reason, WebAO decides to normalize almost every string
component in URLs, packets, and INI files to lower case.

First, the glaring issue. In the URLs, this handling of paths is utterly
broken and corrupts data. By mangling characters, you change the
resource identity and break valid URLs. According to section 6.2.2.1 of
RFC 3986 (Case Normalization):

&gt; When a URI uses components of the generic syntax, the component syntax
&gt; equivalence rules always apply; namely, that the scheme and host are
&gt; case-insensitive and therefore should be normalized to lowercase. For
&gt; example, the URI &lt;HTTP://www.EXAMPLE.com/&gt; is equivalent to
&gt; &lt;http://www.example.com/&gt;. The other generic syntax components are
&gt; assumed to be case-sensitive unless specifically defined otherwise by
&gt; the scheme (see Section 6.2.3)

Scheme and host _are_ case-insensitive. Path is _not_, so isn't
everything else. Section 6.2.3 doesn't define any normalization for the
path component in HTTP schemes. Thus, example.com/item and
example.com/Item are two different resources.

I can only think of idiotic conventions of a particular poorly designed
file system when it comes to this absurdity. There's no reason to drag
them around in our developments. For these systems, case doesn't matter
anyway, normalization is their job, not server hosts' who end up having
to either rewrite every URL request for every asset, or mangle their
asset directory and then rewrite almost every INI config (and spam
"showname=Name" everywhere because now your character directory has to
be "name").

So, instead of using absurd ad-hoc solutions to a broken implementation
such as forcing everything to lower case on the server side, this commit
attempts to fix the root issue and make URL handling conformant to
relevant standards.

Similar situation with strings within packets, although not as severe
in practice. Case must be preserved, otherwise it's corrupting data for
no reason. If a normalization is needed, it should be done at the call
site of whatever requires it (like a filtering function), not by the
parser.

As for the INI, it's opinionated. While the values absolutely must not
be normalized, a case can be made for keys and section names: why not
allow "Options", "options", or even "oPtiOnS"? It's more convenient, and
corresponds to the platform quirk of Windows (which Qt unfortunately
inherits in AO2 Client). I don't think there's a good reason to allow
such leniency in parsing, and removing superfluous normalization is a
better move: less data transformations, less ambiguity, more strictness.
In practice, INIs tend to be well-formed, and it's good discipline to
write them this way.

In several places, the case-folding does make sense: callwords,
OOC commands, CSS class names for areas, and character list filters.
These will behave weirdly and inconveniently without it. In most places,
however, it only causes unnecessary breakage.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
For whatever reason, WebAO decides to normalize almost every string
component in URLs, packets, and INI files to lower case.

First, the glaring issue. In the URLs, this handling of paths is utterly
broken and corrupts data. By mangling characters, you change the
resource identity and break valid URLs. According to section 6.2.2.1 of
RFC 3986 (Case Normalization):

&gt; When a URI uses components of the generic syntax, the component syntax
&gt; equivalence rules always apply; namely, that the scheme and host are
&gt; case-insensitive and therefore should be normalized to lowercase. For
&gt; example, the URI &lt;HTTP://www.EXAMPLE.com/&gt; is equivalent to
&gt; &lt;http://www.example.com/&gt;. The other generic syntax components are
&gt; assumed to be case-sensitive unless specifically defined otherwise by
&gt; the scheme (see Section 6.2.3)

Scheme and host _are_ case-insensitive. Path is _not_, so isn't
everything else. Section 6.2.3 doesn't define any normalization for the
path component in HTTP schemes. Thus, example.com/item and
example.com/Item are two different resources.

I can only think of idiotic conventions of a particular poorly designed
file system when it comes to this absurdity. There's no reason to drag
them around in our developments. For these systems, case doesn't matter
anyway, normalization is their job, not server hosts' who end up having
to either rewrite every URL request for every asset, or mangle their
asset directory and then rewrite almost every INI config (and spam
"showname=Name" everywhere because now your character directory has to
be "name").

So, instead of using absurd ad-hoc solutions to a broken implementation
such as forcing everything to lower case on the server side, this commit
attempts to fix the root issue and make URL handling conformant to
relevant standards.

Similar situation with strings within packets, although not as severe
in practice. Case must be preserved, otherwise it's corrupting data for
no reason. If a normalization is needed, it should be done at the call
site of whatever requires it (like a filtering function), not by the
parser.

As for the INI, it's opinionated. While the values absolutely must not
be normalized, a case can be made for keys and section names: why not
allow "Options", "options", or even "oPtiOnS"? It's more convenient, and
corresponds to the platform quirk of Windows (which Qt unfortunately
inherits in AO2 Client). I don't think there's a good reason to allow
such leniency in parsing, and removing superfluous normalization is a
better move: less data transformations, less ambiguity, more strictness.
In practice, INIs tend to be well-formed, and it's good discipline to
write them this way.

In several places, the case-folding does make sense: callwords,
OOC commands, CSS class names for areas, and character list filters.
These will behave weirdly and inconveniently without it. In most places,
however, it only causes unnecessary breakage.
</pre>
</div>
</content>
</entry>
<entry>
<title>Remove CH-sending timer</title>
<updated>2026-04-18T16:52:22+00:00</updated>
<author>
<name>Osmium Sorcerer</name>
<email>os@sof.beauty</email>
</author>
<published>2026-03-16T14:12:22+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sof.beauty/webao/commit/?id=2ef41402209b82279656ae4b1affe6484be1ed77'/>
<id>2ef41402209b82279656ae4b1affe6484be1ed77</id>
<content type='text'>
CH is an application-level keepalive packet that clients periodically
send for two reasons:

1. It tells the server they're still connected, preventing timeouts.

2. By measuring latency between sending CH and receiving CHECK, a client
   can display ping.

Keepalive is redundant because WebSocket can handle that via PING frames on a
transport layer. WebAO also completely ignores CHECK and sends CH every
five seconds, which is superfluous (AO2 Client sends it once every 45
seconds, in comparison).

Sending CH via `setInterval` was also problematic: browsers seem to
throttle it when the tab becomes inactive, preventing periodic pings and
leading to the server disconnecting inactive browser clients.
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
CH is an application-level keepalive packet that clients periodically
send for two reasons:

1. It tells the server they're still connected, preventing timeouts.

2. By measuring latency between sending CH and receiving CHECK, a client
   can display ping.

Keepalive is redundant because WebSocket can handle that via PING frames on a
transport layer. WebAO also completely ignores CHECK and sends CH every
five seconds, which is superfluous (AO2 Client sends it once every 45
seconds, in comparison).

Sending CH via `setInterval` was also problematic: browsers seem to
throttle it when the tab becomes inactive, preventing periodic pings and
leading to the server disconnecting inactive browser clients.
</pre>
</div>
</content>
</entry>
<entry>
<title>hide confusing text for banned players</title>
<updated>2026-04-18T11:55:56+00:00</updated>
<author>
<name>stonedDiscord</name>
<email>Tukz@gmx.de</email>
</author>
<published>2026-04-18T11:55:56+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sof.beauty/webao/commit/?id=a7e664d5527dac59d722cdb48bfd8e3edf86645f'/>
<id>a7e664d5527dac59d722cdb48bfd8e3edf86645f</id>
<content type='text'>
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
</pre>
</div>
</content>
</entry>
<entry>
<title>Merge pull request #301 from AttorneyOnline/rendering-fix</title>
<updated>2026-04-06T18:38:41+00:00</updated>
<author>
<name>stonedDiscord</name>
<email>Tukz@gmx.de</email>
</author>
<published>2026-04-06T18:38:41+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sof.beauty/webao/commit/?id=4be4f4665fe03a0267ac88c36f0e3b73d8fc2d48'/>
<id>4be4f4665fe03a0267ac88c36f0e3b73d8fc2d48</id>
<content type='text'>
Fix IC rendering race conditions with asset preloading</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Fix IC rendering race conditions with asset preloading</pre>
</div>
</content>
</entry>
<entry>
<title>Add asset preloading system for IC message rendering</title>
<updated>2026-04-01T11:59:13+00:00</updated>
<author>
<name>David Skoland</name>
<email>davidskoland@gmail.com</email>
</author>
<published>2026-04-01T11:59:13+00:00</published>
<link rel='alternate' type='text/html' href='https://git.sof.beauty/webao/commit/?id=10b413c0f0a31bc9476eed86812b6bb90f82caed'/>
<id>10b413c0f0a31bc9476eed86812b6bb90f82caed</id>
<content type='text'>
Fix rendering race conditions where character sprites, pre-animations,
and paired character assets were displayed before being downloaded.
All assets referenced in an MS packet are now resolved and preloaded
into the browser cache before the animation timeline starts.

- Add unified assetCache module with session-wide promise caching
- Add preloadMessageAssets orchestrator for parallel asset resolution
- Cache fileExists HEAD requests so missing files aren't re-probed
- Preload all SFX (emote, shout, realization, stab) alongside sprites
- Use synchronous setEmoteFromUrl at all render transition points
- Graceful fallback to legacy setEmote if preloading times out

Co-Authored-By: Claude Opus 4.6 (1M context) &lt;noreply@anthropic.com&gt;
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Fix rendering race conditions where character sprites, pre-animations,
and paired character assets were displayed before being downloaded.
All assets referenced in an MS packet are now resolved and preloaded
into the browser cache before the animation timeline starts.

- Add unified assetCache module with session-wide promise caching
- Add preloadMessageAssets orchestrator for parallel asset resolution
- Cache fileExists HEAD requests so missing files aren't re-probed
- Preload all SFX (emote, shout, realization, stab) alongside sprites
- Use synchronous setEmoteFromUrl at all render transition points
- Graceful fallback to legacy setEmote if preloading times out

Co-Authored-By: Claude Opus 4.6 (1M context) &lt;noreply@anthropic.com&gt;
</pre>
</div>
</content>
</entry>
</feed>
