# Dynamic flag atlas (runtime TEXTURE_2D_ARRAY)

Replaces the build-time `flag-atlas.png` with a runtime
`TEXTURE_2D_ARRAY`
populated on demand from each player's server-resolved flag URL. Layers
are
deduped by URL (every "Mercia" bot shares one slot), so the per-game
working
set is bounded by unique flags, not player count.

## Why

The store will eventually ship hundreds of custom flags fetched from the
CDN,
which can't be baked into a static atlas. Moving to a runtime array also
lets
the catalog grow without bloating the client bundle.

## Side effect (bonus)

Human players' country flags (`country:US`, etc.) now display next to
their
names in-game. The old atlas only contained nation names, so non-nation
flags
were silently dropped.

## Notes

- Cell size is fixed at 128×85; loaded images are aspect-fit and
centered.
- Layer cap is 512 (clamped to `MAX_ARRAY_TEXTURE_LAYERS`). Past the
cap,
  further flag requests render no icon.
- Mipmaps are regenerated after each layer upload.
- Recommend store pipeline caps custom flag uploads at SVG or PNG ≤
256×170,
  ≤ 50 KB (decode-time RAM and bandwidth, not VRAM).


## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
Evan
2026-05-22 13:19:22 +01:00
committed by GitHub
parent fe6581e3fe
commit 19beab9a70
14 changed files with 443 additions and 889 deletions
+7 -6
View File
@@ -1,6 +1,6 @@
import { Colord } from "colord";
import { base64url } from "jose";
import { extractFlagName } from "../core/AssetUrls";
import { assetUrl } from "../core/AssetUrls";
import { decodePatternData } from "../core/PatternDecoder";
import { PlayerType } from "../core/game/Game";
import { GameView } from "../core/game/GameView";
@@ -134,10 +134,11 @@ export class WebGLFrameBuilder {
this.writePaletteEntry(smallID, p.territoryColor(), p.borderColor());
let flag = p.cosmetics.flag;
if (flag) {
flag = extractFlagName(flag);
}
// p.cosmetics.flag has already been server-resolved to either a full URL
// or a relative asset path (e.g. "/flags/US.svg" or a CDN URL for a
// custom flag). assetUrl() passes URLs through and rewrites paths.
const flagRef = p.cosmetics.flag;
const flagUrl = flagRef ? assetUrl(flagRef) : undefined;
const pattern = p.cosmetics.pattern;
if (pattern && pattern.patternData) {
@@ -160,7 +161,7 @@ export class WebGLFrameBuilder {
newPlayers.push({
...p.static,
flag: flag,
flag: flagUrl,
color: p.territoryColor().toHex(),
});
}