Warring NationsWarring Nations MC 1.21.1.NEOFORGE.JAVA 21
Wiki/Developer/BlueMap Rendering Internals

BlueMap Rendering Internals

How WN-BlueMap builds, colors, caches, and paces the four marker layers it pushes to BlueMap: nation territory, active wars, diplomacy lines, and villages. This is the deep internals reference for extending or debugging that rendering pipeline, not a player-facing guide.

ADDON DEVS

Overview

All four layers are pushed to BlueMap by MarkerService, which owns one MarkerSet per layer per map.

Layer
Marker set id
Config toggle
Label config key
Nations
wnbluemap.territory
[features] claims
[labels] claims (default "Nations")
Active Wars
wnbluemap.war
[features] warOverlay
[labels] war (default "Active Wars")
Diplomacy
wnbluemap.diplomacy
[features] diplomacyLines
[labels] diplomacy (default "Diplomacy")
Villages
wnbluemap.villages
[features] villages
[labels] villages (default "Villages")

The marker set id is part of BlueMap's own web API (bookmarkable/embeddable via its ?markers= URL query parameter), so it stays stable even when [labels] renames a layer's display name. wnbluemap.territory is the historical id and is deliberately not renamed to wnbluemap.claims to match the territory to claims config-key rename, so an existing saved link or embed never breaks.

i
Markers are global. Every viewer of the web map sees the same markers, whether logged in or not. This is a shared political map, not a personal per-player minimap; nothing here is hidden from any particular viewer, including a nation's own enemies.

Nations Geometry and Rendering

Each nation's claimed chunks, merged into clean polygons and colored with the nation's own color.

Claimed chunks are merged with RegionGrouper (4-connected BFS into disjoint connected regions) and then traced with ChunkPolygonBuilder (boundary edge tracing into outer rings plus holes, even-odd nested) - see Geometry Algorithms below. A nation whose land is split into several disjoint chunks of territory (scattered claims, land lost to war) gets one polygon set per region, not one polygon spanning gaps; an unclaimed pocket, or an enemy enclave fully surrounded by one nation's claims, becomes a real hole in that polygon rather than being silently painted over.

Border walls

A chain of short vertical wall panels along the polygon's edge. Every borderSampleSpacing blocks, the wall resamples real ground height (first opaque block, or a fluid surface counted as ground so coastal borders trace the water line rather than the seabed; the median across nearby samples so one stray lamppost base can't drag a whole panel's height) and steps to match it, so the border reads correctly from BlueMap's angled 3D view. Consecutive same-height samples merge into one longer panel instead of paying for a marker per sample, so long flat runs cost nothing extra.

Ground tint

A flat, depth-tested fill just above sampled ground height across the whole claimed area, not just the border. From directly overhead it reads as solid filled territory; viewed at an angle, real terrain occludes it the way the ground would occlude a decal painted onto it - which is exactly why the border walls exist as a separate layer, since they stick up and stay visible from an angle on purpose.

tintHeightOffset needs real clearance above ground (0.1 blocks measured invisible from directly overhead in testing, not merely flickering) to reliably win BlueMap's depth test from a top-down camera.

i
Flat-only maps. If a BlueMap map's own config has both enable-perspective-view and enable-free-flight-view disabled (it can only ever render the flat top-down view), ground-height sampling is skipped entirely for that dimension and one flat combined shape is drawn per region instead, styled by [style] fillOpacity/lineOpacity/lineWidth/shapeMinY/shapeMaxY. Rebuild cost stops scaling with claim size almost entirely on such a map. This is detected by BlueMapConnector/BlueMapMapConfig reading BlueMap's own maps/<mapId>.conf file directly, since the BlueMap API itself exposes no such flag; a dimension is only treated as flat-only if every BlueMap map that renders it has both settings disabled.

Nations Coloring

Color comes from FTB Teams' TeamProperties.COLOR, the value /nation color sets.

If unset (FTB's default is pure white) or pure black - either one indistinguishable or useless on a colored map - a stable, vivid color is instead derived deterministically from the nation's UUID: hue = (uuid.hashCode() & 0x7FFFFFFF) % 360, at fixed saturation 0.65 / brightness 0.95. This means the same nation always renders the same color across restarts even without ever setting one explicitly, and every nation stays visually distinguishable from its neighbors. An at-war nation's territory color is blended toward the war color (see Active Wars below) so conflict is visible even with the war layer's own markers turned off.

Nations Popup

One popup marker per region, not per wall panel or tint shape, and not one per nation either - a nation with several disjoint regions gets a full card on each one, so a reader lands on complete information at every piece of land a nation holds.

Field
Notes
Name + accent color
The nation's own resolved color.
Tier
Named (e.g. "Kingdom"), with its war-power multiplier. Falls back to "Tier " + level only for the placeholder case (a claimed FTB team WN doesn't recognize as a nation at all).
Region size
Chunk count and area in blocks squared (formatted K/M for large claims).
Region centre
Block X/Z of this region's centroid.
Capital
A pill with the nation's designated capital name, if any - never its coordinates (see below).
Alliance bloc
A pill naming the multi-nation bloc this nation belongs to, if any.
Leader + members
Leader plus up to rosterDisplayLimit members, each with a small player-head avatar (via minotar.net), collapsing into "+N more".
Allies / enemies
Up to relationDisplayLimit each, collapsing into "+N more" - the standing diplomatic relations this nation holds, not the same as an active war.
War banner
A red "This nation is at war" line, shown only while atWar is true.

Only WN nations draw - ordinary FTB personal/party claims (any FTB team that isn't a recognized WN nation) are filtered out entirely before any geometry work happens.

!
Capitals are never located. A capital's chunk coordinates (like /nation home's) are withheld from the public WarringNationsAPI/NationView surface on purpose, the same light opsec /nation info already applies against other nations in-game. Since BlueMap markers are public to every viewer including enemies, this addon never reads or renders a capital's (or a home's) actual position - only its name ever crosses onto the map. A dedicated capital pin marker used to exist, back before Warring Nations had an in-game capital concept for it to reflect; it was removed rather than resurrected once capitals landed, specifically to keep this rule intact.

Active Wars Frontline

Not a whole territory repaint: only a nation's own claimed chunks that are adjacent to a chunk owned by a nation it's actively fighting.

Computed as a simple neighbor scan (frontlineCells) over the combined single-owner chunk map built for the whole rebuild pass. This deliberately narrow band keeps the layer readable as "where the fighting actually is" rather than repainting an entire territory red the moment any war starts anywhere on its border.

War state is read from WarringNationsAPI#activeWars(), not NationView#atWar() or the older areAtWar predicate - Warring Nations runs two parallel war implementations (the legacy model and file-defined composable war presets), and those two older accessors only ever saw the first one. A composable war fought entirely through the newer system used to render no frontline and no war-tint at all; activeWars() is the one accessor that sees both, so every war shows up regardless of which system declared it.

Territory color bleedAn at-war nation's Nations-layer territory color (walls + tint) is blended toward the war color by [war] claimsTintMix (default 0.55, roughly half-and-half), so conflict is visible on the Nations layer even with the Active Wars layer switched off entirely in the viewer's own layer toggles.

Active Wars Popup

A live scoreboard, not just "at war": one row per war this nation is currently fighting on this frontline.

Field
Detail
Opponent
Named, or "an enemy nation" if the lookup ever fails.
Score
Written from the viewed nation's own point of view (its score first), with a leading/level/behind verdict.
Mobilizing state
A war still in its post-declaration countdown (DECLARED, fighting not yet started) reads as "Mobilizing" rather than a meaningless 0-0.
War preset
The composable war-preset id (e.g. border_skirmish) or legacy WarModel enum constant, both title-cased the same way.
Elapsed time
Coarse buckets ("just started", "12m in", "3h in", "2 days in") - deliberately not minute-precise, since the underlying marker is only as fresh as the last rebuild.
Siege status line
For a siege-scored war only: "N of M rings fallen", or once every ring is down, "Rings breached - holding the keep <duration>". Deliberately never a raw breach-progress percentage - SiegeRingView#breachProgress() is a bare counter racing a per-tick-reevaluated formula threshold, not a stable percentage.

Siege Rings, Keep, and Objectives

Real geometry, not just text, for wars using the composable scoring engines.

For a war using the composable siege-ring-breach scoring engine, WarringNationsAPI#siegeStatusOf exposes the engine's own per-chunk geometry (SiegeRingView#chunks, built from ComposableSiegeEngine's Chebyshev-distance ring grouping, plus SiegeStatusView#keep). Each ring and the keep draw as their own real, independently colored polygon over the defender's claimed chunks.

Standing ring

Uses [siege] ringColorRgb.

Fallen ring

Stays drawn, not hidden - just visually demoted to ringFallenColorRgb, so a viewer can see overall siege progress at a glance, not merely what's left.

The keep

Ring 0 gets its own distinct keepColorRgb, since it's the attacker's actual win condition once every wall ring has fallen.

Each ring / the keep also carries its own small popup (fallen/standing, chunk count; the keep's popup additionally shows keep-hold duration once reached).

For a war using the composable objectives-capture scoring engine, WarringNationsAPI#objectiveProgressOf exposes each objective's live-bound site chunk (ObjectiveProgressView#siteChunk, from ComposableObjectivesEngine's per-objective claim binding). Each objective draws as a small square marker (objectiveMarkerRadiusBlocks half-width, kept well inside one 16-block chunk so it reads as a point, not a chunk fill) with its own popup showing fields relevant to its type - tier, and one of HP remaining (destructible), payload waypoint progress, or current holder (contested).

Both siege and objective geometry are built only from the defender's side of each war (building it again under the attacker's id would draw the identical shapes twice), and are strictly additive alongside the frontline overlay and the scoreboard's existing text summary - never a replacement for either.

i
Staleness note. Ring/objective geometry rides the same frontline cache as everything else in this layer, not a dedicated invalidation trigger of its own - it refreshes whenever that nation's frontline would rebuild anyway (a claim/color change, or markWarDirty on a score change), so it can lag a live breach-progress tick by up to one rebuild cycle. This is the same staleness characteristic the text-only scoreboard already had before real geometry was added.
i
Live per-chunk capture progress is deliberately not rendered, even though WN tracks it internally. Marker rebuilds are debounced and paced across ticks by design; a progress value that finishes inside a minute would be stale on the map more often than it was accurate, and WN already surfaces that finer granularity in-game where it belongs (boss bars, capture-rate readouts, waypoints). Whole-war score is the right granularity for a shared web map, and that's what the popup shows.

Diplomacy

Lines between two nations that both hold land in the same dimension and share a standing diplomatic relation.

Drawn for ALLY or ENEMY relations, from WarringNationsAPI#relationsOf, anchored at each nation's own diplomacy anchor point (see below). Hidden by default ([features] diplomacyLinesDefaultHidden = true) - a viewer opts in from the web app's own layer toggles.

Alliance lines

Use [war] allianceColorRgb (default green) and allianceLineWidth.

Rivalry lines

The ENEMY half of standing diplomacy (a declared enemy, not necessarily an active war) uses rivalryColorRgb (default amber) and the thinner rivalryLineWidth by default, reflecting that a rivalry is a lower-certainty posture than an alliance. Deliberately not the war/frontline color - a rivalry line and an active-war frontline must never read as the same thing from a distance. Gated by its own toggle, [features] rivalryLines, on top of diplomacyLines.

A cross-dimension pair is never drawn: both nations must hold land in the same dimension for a line to appear there.

COST MODEL

Unlike every other layer, diplomacy lines are deliberately left uncached, and recomputed fresh on every single rebuild pass. Its cost is an O(nations squared) pairwise relation check per dimension, not O(claims) - it tracks how many nations hold land in a dimension, not how large any one claim burst is. Each nation's own relationsOf read is memoized for the pass, so a nation's relations are read once regardless of how many candidate pairs it appears in.

The anchor point. Each line's endpoint is the centroid of that nation's largest claimed region - computed as part of the Nations layer's own region-building pass and cached alongside it (anchorPosCache), not a separate lookup. This point is invisible on the map itself; nothing is drawn there, it only decides where a diplomacy line lands.

This anchor is deliberately not the player-set /nation home position. Warring Nations tracks a real home (/nation home set|go|clear) but hides other nations' home coordinates from /nation info as opsec, and the public API surface doesn't expose it at all. Since BlueMap markers are public to every viewer, broadcasting the true home would undo that opsec intent entirely, so this addon never reads or renders it. Using the largest-region centroid instead reveals nothing the Nations layer's own territory shading doesn't already show.

Villages

The territorial tier below a nation: either affiliated with one nation, or fully standalone.

A village's territory lives entirely in Warring Nations' own village claim store, not in FTB Chunks - WarringNationsAPI#claimsOfVillage is the only way to learn a village's shape, unlike nation territory there is no FTB-side claim data to fall back to - so it's read and rendered by an entirely separate code path (buildVillageTerritory) from nation territory.

Rendering. Always a flat, depth-tested decal - never the terrain-hugging border walls nation territory gets. This is a deliberate visual choice: a village never reading as a scaled-down nation at a glance is worth more than terrain-accurate walls here, and it comes with a real performance upside - village rendering does no per-block ground-height scanning at all.

Coloring. An affiliated village inherits its owning nation's color, pulled a third of the way toward white (ColorUtil.mix(nationColor, 0xFFFFFF, 1/3)) - close enough to visually associate the two, but always paler than the nation's own territory next to it. A standalone village gets its own hue, deterministically derived from its own UUID the same way a nation's fallback color is, but at lower saturation and higher brightness (0.40/0.90 vs. a nation's 0.65/0.95) - a muted pastel family a nation's own vivid fallback hue never lands in.

Field
Notes
Name
Prefixed with a village icon.
Affiliation
"Standalone village", or "Village of <nation>" for an affiliated one.
Region size
Chunk count and area, same formatting as the Nations popup.
Region centre
Block X/Z.
Leader
With avatar.
Member count
Count only, never a roster - VillageView only ever exposes a member count, not names, unlike NationView.
Under-siege banner
Shown only while under village-conquest siege (a separate contest from a war's siege-ring breach), same red-banner convention a nation at war gets. Repaints promptly since WnEvents.VillageSiegeStarted/VillageSiegeLifted mark the village dirty directly.

No war banner: villages have no war state of their own (only a nation can be a war belligerent), so that field is simply absent rather than always-false.

Pacing

The expensive part of a rebuild - reading real world blocks for border walls/tint, plus the polygon-tracing that feeds on it - is spread across server ticks at five independently-paced queues, rather than done in one synchronous burst, because each scales with a different thing.

Queue
Config key
Default
Scales with
Nation setup
nationsPerTick
3
Nothing costly by itself - one BFS per dirty nation, only enumerates work.
Border wall building
wallRegionsPerTick
8
A region's perimeter. Exists separately from nation-level pacing because a nation split into many disjoint regions used to have every region built synchronously in one call just because they shared a nation id.
Tint sampling
tintCellsPerTick
150
Claimed area directly - one ground-height sample per claimed chunk.
Tint grouping
tintGroupsPerTick
200
Merged same-height plateau count - measured at over 100ms synchronously for one 42,000-chunk nation on merely 10%-varied terrain before this queue existed.
Village territory
villagesPerTick
10
Village count - no ground scanning to pace, just boundary-tracing per village.

War frontline, diplomacy lines, and the actual push to BlueMap's maps stay a single unpaced step (finalizePass) that runs once every queue above has fully drained for the pass: pure in-memory arithmetic and cache lookups, never the per-block world reads or per-group polygon-tracing that made everything else worth pacing.

Caching and Rebuild Triggers

Every rebuild is triggered by an actual Warring Nations event (WnEvents), never a polling timer.

Cache
Keyed by
Invalidated by
Territory (walls / flat-only shape)
dimId|nationId
That nation's own claims or color changing.
Tint
dimId|nationId
Same condition as territory, tracked separately since it's paced by its own two queues.
Diplomacy anchor point
dimId|nationId
Rides the same condition as territory (derived from the same claim geometry).
War frontline
dimId|nationId
This nation's own claims/color, or any currently-at-war enemy's claims/color, plus a war-score/siege-event dirty flag.
Village territory
dimId|villageId
That village's own claims, name, or nation affiliation.
Diplomacy lines
(not cached)
Recomputed every pass - see the Diplomacy section's cost-model note above.
i
A war-score change (WnEvents.WarScoreChanged) and siege/objective progress deliberately invalidate only the frontline cache, never the territory cache - not one chunk moves and no color changes when a score ticks up, so routing it through the territory-dirty path would have turned every capture point awarded into a full ground-height border rescan of both belligerents, for a change that alters nothing but popup text.
Event
What it invalidates
ClaimChanged (incl. war captures)
The new owner's territory; if the owner is null (unclaim) or the previous owner is unknown from the event alone, every nation is marked dirty rather than risk a shrunk nation's stale, oversized territory staying cached forever.
NationCreated / NationDisbanded / NationRenamed
That nation's territory (renaming changes the marker label text).
MemberJoined / MemberLeft
Only the cached leader/member name list, not territory geometry.
RelationChanged
Both nations' territory popups (the popup lists allies/enemies by name), not the war overlay.
WarStarted / WarEnded
Both belligerents' territory (war-tint color) and the Discord bridge, if bound.
WarScoreChanged
Frontline only (markWarDirty), never territory. High-frequency during active captures; the debounce window coalesces a capture burst into one rebuild.
SiegeRingFallen / SiegeRingRelieved / SiegeKeepHoldPhaseEntered
Frontline only, plus a Discord notification if bound. Discrete transitions, not per-tick, so no burst concern.
PeaceProposed / PeaceDeclined / PeaceAccepted
Frontline only, for both current belligerents (resolved from the live activeWars() snapshot at event time; a harmless no-op if the war already ended).
VillageFounded / VillageDisbanded / VillageRenamed / VillageClaimChanged / VillageAffiliationChanged
That village's own territory (affiliation change also affects its inherited color).
VillageSiegeStarted / VillageSiegeLifted
That village's under-siege popup banner, plus a Discord notification if bound.
PresetApplied
The affected nation's territory (a preset can change color/tier in one shot), plus a Discord notification if bound.
BlueMap enabling/reloading
Every marker set on every map (BlueMap resets its own web markers on every restart/reload).
Server start
Everything, once.

Rebuilds are debounced (debounceMillis, default 1500ms of quiet) so a burst of rapid changes collapses into a single redraw instead of one per event, and incremental: only the nations/villages actually marked dirty recompute, everything else is reused verbatim from cache. A periodic full, uncached recompute (fullRebuildEvery, default every 500 passes) exists purely as insurance against an unknown cache-invalidation gap, not as the routine mechanism for catching anything - color changes in particular are now detected directly every pass at negligible cost (an O(1) per-nation comparison against the last-known color), rather than relying on this periodic sweep the way an earlier version did.

i
Why WN events instead of FTB Chunks events? WnEvents.ClaimChanged fires for exactly the claims this addon renders (nation claims, war captures included) and needs no Architectury event dependency. On FTB Chunks for Minecraft 1.21.1, the underlying claim event is the Architectury ClaimedChunkEvent.AFTER_CLAIM/AFTER_UNCLAIM (not the newer FTBChunksEvent.ChunkChange.Post); this addon deliberately avoids coupling to that layer at all.

Geometry Algorithms

Both are pure JDK (no Minecraft or BlueMap types), unit-tested independently of a running server (ChunkPolygonBuilderTest, RegionGrouperTest).

RegionGrouper

4-connected BFS over a set of claimed chunk cells into disjoint connected components ("regions"). Also has a same-key variant used to group tint samples into same-height plateaus.

ChunkPolygonBuilder

Boundary edge tracing. Every claimed cell's side whose neighbor isn't claimed is a directed boundary edge (emitted counter-clockwise so the claimed interior is always on the edge's left). Directed edges chain corner-to-corner into closed loops, taking the sharpest left turn at any "pinch" corner where two diagonally-touching regions meet (provably keeps the two loops separate and non-self-intersecting). Collinear vertices collapse into single edges.

Rings are then nested by even-odd containment (robust and winding-independent): a ring inside an even number of others is an outer ring, inside an odd number it's a hole, and each hole attaches to the smallest outer ring that contains it - correctly handling both a donut hole (an unclaimed pocket, or an enemy enclave, fully inside one nation's claim) and an island-in-lake (which becomes its own outer polygon nested inside the hole).

Example use case: a claim change becomes a rendered border

WN-BlueMap has no addon-facing extension API of its own to hook a custom marker or popup field into; the useful thing to understand instead is the real pipeline a claim change already runs through, end to end, from a nation's own claim event to pixels on the map.

Example use case

A player runs /nation claim on a chunk bordering their existing territory. WN Core fires WnEvents.ClaimChanged. WN-BlueMap marks that nation's dimId|nationId territory and tint cache entries dirty (see Caching and Rebuild Triggers above), then waits out the debounceMillis quiet window (default 1500ms) in case more claims land in the same burst. Once the pass runs, RegionGrouper re-groups that nation's claimed chunks with 4-connected BFS into disjoint regions (see Geometry Algorithms), and ChunkPolygonBuilder traces the region containing the new chunk into outer rings and holes. The border-wall queue (wallRegionsPerTick, default 8) then samples real ground height every borderSampleSpacing blocks along that new edge and builds the wall panels, while the tint queues (tintCellsPerTick/tintGroupsPerTick) separately extend the ground-tint fill over the newly claimed chunk. Both are pushed to BlueMap by MarkerService as part of the same wnbluemap.territory marker set once their queues drain for the pass, and every other viewer of the web map sees the new border on their next marker refresh, with no page reload needed.