How a name becomes a Resource object: which pool is asked, which sources it walks in what order, the
two version rules that decide whether a copy is taken, what holds a loaded one and for how long, and the
Loading protocol a read goes through while none of that has happened yet. What a .res carries once
it is loaded is resources; the Java one can carry is published code.
| Pool | Built by | Sources, in the order Pool.handle walks them |
|---|---|---|
Resource.local() |
Resource.local (lazily, once) |
JarSource("res") — the res/ tree of builtin-res.jar; then a FileSource(resdir) when HAFEN_RESDIR / haven.resdir names a directory (a development aid — an error opening it is swallowed) |
Resource.remote() |
Resource.remote (lazily, once), with local() as its parent |
JarSource("res-preload") — the res-preload/ tree of hafen-res.jar; then JarSource("brodgar-res") — the brodgar-res/ tree of brodgar-res.jar, the brodgar.io resource pack (every resource at the version the cache held when it was packed; hafen.jar’s Class-Path names the jar, the launcher puts it beside hafen.jar, and without it the source answers nothing); then CacheSource(prscache) — the on-disk store Client.setupres passed to Resource.setcache: the HashDirCache under Config.localdir()/data, or savedata/rescache.sqlite with -Dhaven.store=sqlite (rescache.md); then whatever Resource.addurl appended, an HttpSource for the server’s resource URL wrapped in a TeeSource that writes each fetched stream into that same store as res/<name> |
Pool.load(name, ver, prio) asks the parent first: a Queued in the child waits on the parent’s
(Queued.awaiting, rdep), and only when the parent has answered with an error does the child enqueue
itself and walk its own sources (Queued.prior). So the order a name is looked for is builtin jar →
resdir → preload jar → brodgar.io pack → disk cache → server, and the first source to produce a stream that parses is the
one whose bytes are kept. Resource.source names it; every failure before it is chained on the
LoadException (error.prev, addSuppressed).
Everything the game names goes through remote(). Session.pool() is Resource.remote(); the
local() pool is asked directly only by the client’s own start-up art (Resource.loadrimg,
loadtex, …). A pool has its own Loader threads (nloaders = 2, daemon HackThreads in
Resource.loadergroup, each exiting after ten idle seconds and started again by ckld), and a layer’s
constructor runs on one of them — UI.scale inside Image, ImageIO.read, Font.createFont.
The server refers to resources by a per-session integer. Session.rescache maps it to a CachedRes;
the name and version arrive in their own message (RMessage.RMSG_RESID → CachedRes.set, which also
starts the fetch at a low priority), and CachedRes.Ref.get() — the Indir<Resource> every OCache
delta and uimsg carries — throws Session.LoadingIndir until the name is known and then
Resource.remote().load(resnm, resver, prio).get(). One id is one Indir while anything holds it:
CachedRes keeps its Ref by WeakReference, getres(id) hands that same Ref back, and set
resets it in place, so a message re-sending an id can be diffed by identity. Session implements
Resource.Resolver, the interface (getres(id)) a widget or a message decoder asks;
Resolver.ResourceMap re-bases one over a message’s own id table.
Asking (Pool.load). With ver == -1 any version satisfies: a cached object is answered at once
(cur.indir()), a queued fetch is joined. With a version:
| The cache holds | Result |
|---|---|
| that version | the cached object |
| a newer one | BadVersionException (“Obsolete version … requested”), thrown synchronously into the caller |
| an older one | a new Queued for the newer version; on success it replaces the cache entry under that name |
A Queued that has already failed is retried only by an ask for a newer version than it failed
under, or by an unversioned ask when it failed under a version; an unversioned ask that failed answers
the same failed Queued for the pool’s lifetime (Pool.load’s XXX branch). A name the server has no
resource for therefore costs one round of every source, once.
⚠️ A queued ask that a newer version supersedes. With the name queued and not yet cached, an ask for
a newer version than the queued one makes a new Queued and takes the old one off queued. Upstream takes
it off the queue as well and never completes it, so whatever holds it waits for good — Resource.Saved
keeps the versioned ask it made (wver) and asks that same one on every get. The fork (// addon:) has
the superseded ask wait on the new one and be answered with what it loads, the way a child pool’s ask
waits on its parent’s (Queued.awaiting, rdep, prior): no ask is left hanging, and no version nothing
asks for any more is fetched — the disk cache holds one copy per name (res/<name>), so a fetched old
version would push out the current one. Only when the new ask fails does the old one go back to loading
its own version. Queued.done caches its object only when nothing newer is cached under the name, and
takes out only its own queued entry.
A saved reference (Resource.Saved). A Saved names a resource at the version it was saved with:
MapFile records every grid’s tilesets and overlays so (MapFile.Grid.from, the version the session had
loaded), a zoom grid takes the highest version of its four children, and markers and GobIcon settings
keep theirs. The server serves only its latest, so a record keeps versions that no longer exist anywhere.
Upstream’s get asks the saved version first and the latest only when that fails, and for a version older
than the latest that first ask can only fail — after every source has been asked, the network included:
the brodgar.io cache answers 404 for <name>.res.v<old>, and the proxy behind it the latest, which the
parse refuses. The fork’s get (// addon:) takes the copy at hand first — Pool.load(name, -1): this
pool’s, a jar’s, the disk cache’s, or else the network’s latest — when its version is at least the saved
one, and asks the saved version only when that copy is older than it. It ends with the object upstream’s
order ends with, in every case, without the round trips that could only fail. ⚠️ Upstream’s order costs
those round trips for every reader of a record asking before a current copy is loaded: a handful for the
map window, which shows what the character has walked, while the live terrain loads the current tilesets
around them; dozens at login for the remembered ground, which draws the record out to the view distance
(mapfile.md) — ten seconds of the resource loaders’ time, measured, with every tile the far
ground and the map window wait for queued behind them.
Parsing (Resource.load(Message)). The stream opens with "Haven Resource 1" and a uint16
version. When the object was created with ver == -1 that number becomes Resource.ver; otherwise any
mismatch — higher or lower — is LoadException("Wrong res version") and Pool.handle asks the next
source. That is how a served version wins over the jar’s: the jar’s copy parses and is refused, the cache
or the server produces the one the session named. It is also why a copy dropped into HAFEN_RESDIR is
taken only when its version equals what the server asks for.
The wanted version travels with the ask. Pool.handle calls ResSource.get(name, ver), a default
that falls back to get(name) for every source but HttpSource, which sends it as the
X-Brodgar-io-Res-Version request header when ver >= 0 (TeeSource passes it through to the source
it wraps). A Brodgar resource proxy holding exactly that version answers from its cache without asking
the official server; any other server ignores the header, and the parse rule above still judges whatever
comes back.
The brodgar.io resource cache (-U https://res.brodgar.io/). Resource.addurl recognises that
host (Resource.isbrodgarcache) and appends two network sources instead of one: a
BrodgarCacheSource, which asks the cache for <name>.res.v<ver> when the ask names a version and for
<name>.res (the latest the cache knows) when it does not, and behind it a plain HttpSource at
Resource.BRODGAR_CACHE_FALLBACK (http://brodgar.io/res/), the proxy that fetches from the official
server and fills the cache. The cache has no logic: a 404 is “not yet”, RetryingInputStream does not
retry a FileNotFoundException, and Pool.handle moves on to the proxy. Both sources are wrapped in the
same caching tee. The local cache keeps the proxy’s identity for a cache -U (HashDirCache.create,
BaseFileCache.create), so moving a client from the proxy to the cache re-downloads nothing.
An empty stream is FileNotFoundException("empty file") on purpose: custom clients have been seen to leave
zero-length files in the disk cache under a resource’s name, and the tee is what wrote them.
Pool.cache is a CacheMap at its default RefType.SOFT: a loaded Resource stays only while
something holds it or the heap is not under pressure, and a collected one is fetched again on the next
ask — from the disk cache, not the server, when it came from there. Pool.cached() copies the live
entries of this pool and its parent into a fresh set; Pool.used() is the subset something has read a
layer of (Resource.used, set by every layer/layers call and cleared at the end of load).
Resource.indir() is the object’s own Indir, minted once; Pool.load hands it back for a cache hit, and a Queued otherwise. Resource.equals is name and version.
Loading and failureQueued.get() throws Resource.Loading (a haven.Loading) until done; Loading.waitfor blocks on
the Waitable.Queue for code that may block (Pool.loadwait, the client’s start-up), and everything
under the frame loop catches it and retries next tick (boot and the frame loop).
Once done, get() is the object or one of two exceptions, kept on the Queued for every later ask:
| Exception | When |
|---|---|
Resource.NoSuchResourceException |
every source threw FileNotFoundException (Queued.found false) |
Resource.LoadFailedException |
at least one source produced a stream and it failed to parse |
Both are BadResourceExceptions carrying the name and version; getCause() is the last source’s
LoadException, whose prev chain walks back through the earlier ones.
load and init: the order layers are built inResource.load(Message) reads records of string type, int32 len, bytes; a type with no
LayerFactory in ltypes is skipped by length, so an unknown layer never fails a resource. Each
known one is constructed through LayerConstructor.cons(res, buf) — Layer is a non-static inner
class, so a layer exists only bound to its resource (Layer.getres() is Resource.this) — and the
whole list is then assigned to Resource.layers by reference, replacing the old list rather than
mutating it. Only after every layer exists does Layer.init() run over the list, which is what lets one
layer find another: Anim.init binds each frame to the Images sharing its id, FastMesh.MeshRes.init
resolves its vertex buffer and material, CodeEntry.init indexes the resource’s Code layers.
ltypes is filled at class-init from every class annotated @Resource.LayerName (jglob’s Discoverable)
across the whole tree — TexR.Encoded, FastMesh.MeshRes, Tileset, … — so the known types are the
loaded classes, not a list anywhere.
Gotchas. Resource.layers is read unsynchronised on the render thread, which is why it is swapped
and never mutated. init() is not idempotent on every type (MeshRes.init consumes its temporary
index buffer), so a second init() over a live list is a corrupted mesh, not a refresh. Resource.used
is set by any layer() read, including one made only to inspect.
The record’s bytes after string type, int32 len, for the layers carrying pictures, shapes and key/value blocks:
| Layer | Layout |
|---|---|
Resource.Image |
uint8 ver; ver >= 128 is the keyed form: int16 id, then string key, tto value pairs to an empty key (z, subz, nooff as an int, off and tsz as Coord, scale as a float, anything else lands in info), then the PNG. ver < 128 is the old fixed header. tsz left out becomes sz.add(o), so a parsed layer cannot tell a served tsz from the default |
TexR.Encoded |
int16 id, uint16 off.x, off.y, uint16 sz.x, sz.y, then parts: uint8 t whose top two bits are the framing (0 inline, 1 an uint8-length sub-message, 2 an uint8 flags byte and an int32 length) and whose low six are the part (0 the PNG as int32 len, bytes, 1 mipmapper, 2/3 filters, 4 the alpha mask). img and mask are private |
Resource.Neg |
cc, ac, bc as three int16 pairs, four unread bytes, uint8 en, then en rings of uint8 epid, uint16 n, n × int16 pair |
Resource.Obstacle |
uint8 ver (1 or 2), string id when ver == 2, uint8 rings, one uint8 count per ring, then every point as float16 x, float16 y in tile units — the constructor multiplies by MCache.tilesz |
Resource.Props |
uint8 1, then a Message.list: tto values alternating key and value, to T_END or the end |
Message.addtto picks the narrowest integer tag for a whole number, T_FLOAT64 for a Double, T_COORD
for a Coord, T_TTOL for an Object[], and has no Boolean case — nooff travels as an int.
Pool.reload(res) walks the pool’s sources again for a resource the cache already holds and ends in
load(msg, keep): the same parse on the same object, so Resource.indir() and every Indir the client
holds stay valid and only layers is swapped. Two traps it steps around:
Code/CodeEntry are carried over, never re-parsed. keep is the object’s current code layers;
the stream’s own code/codeentry records are skipped by length. A second parse would construct a
second CodeEntry, whose lazily built ResClassLoader would defineClass every served class again
under a new loader — two Class objects with one name, and getcode handing out the other one.Pool.handle versus reload on the version rule. handle creates the object with the version
the ask named (or -1, which takes the stream’s); reload keeps Resource.ver, so load refuses any
source whose uint16 differs. A disk-cache file replaced by a newer version since the object loaded
fails every source "Wrong res version": reload throws the last LoadException, old layers standing.Pool.peek(name) reads this cache and its parents’ without enqueuing; Resource.newLayer(res, type,
buf) is the ltypes factory as a static, so a caller outside haven builds a layer bound to res
(Layer is a non-static inner class) through the same constructor a served one takes. Resource.Virtual
(public constructor over a pool, name and version; add(Layer) appends) is a resource never loaded, the
one DynresWindow composes on — and a scratch on which foreign layer bytes can be constructed and
init()ed against each other while nothing else holds them.
.res carries: reading a layer by class, predicate or idcode layer, @FromResource adoption, and the ABI served code links againstLoading protocol under the frame loop