Extension Points
A practical, use-it-today reference for WN Core's four addon-facing registries: what each method signature actually looks like, when to reach for it, and a concrete example. For the high-level picture of how these fit into WN Core's architecture, see Architecture first.
ClaimEligibilityRegistry: pluggable claim vetoes
WN Core's only built-in claim-eligibility rule is same-nation contiguity, opt-in via requireContiguousClaims. ClaimEligibilityRegistry is the seam for a second rule (or a dozen) without forking WnCommands.java: a tier-gated expansion cap, an alliance-aware border rule, a distance-from-capital limit, a region allowlist or denylist.
api/claim/
A policy is a ClaimEligibilityPolicy: a FunctionalInterface taking a plain-data ClaimEligibilityContext (nation id, tier weight, target chunk, current claim count, first-claim-in-dimension flag, and a map of adjacent-chunk-owner-nation-id to relation) and returning Optional<String>: empty means allow, present means deny with the string shown to the player as the reason. Register one at addon init:
ClaimEligibilityRegistry.register(ctx -> {
// below tier weight 4, deny expansion into a chunk bordering
// a non-ALLY nation; allow it at tier 4+, or if the border is an ALLY
...
return Optional.of("Your nation is not yet tier 4 enough to expand into contested territory");
});
WnCommands#claimChunk runs ClaimEligibilityRegistry.checkAll(...) after WN's own built-in checks, short-circuiting on the first denial. Multiple registered policies compose: any one denial wins.
A tier-gated territorial-expansion rule: "until tier 3, a nation may not claim or take territory belonging to another nation unless the nations share a land border." WN's single contiguity check can't express a tier-gated, relationship-aware rule like this; a registered policy can.
WnCustomData: per-owner metadata
WN Core's domain objects (Nation, ComposableWarState, claim records) only carry the fields WN itself needs, with no capability slot for extra state and no generic persistence hook. WnCustomData gives an addon a place to attach its own data to a nation, war, or claim that automatically persists and automatically cleans itself up, without a separate save file or world-load wiring.
api/data/, exposed via WarringNationsAPI#customData()
get/set/remove/namespaces are keyed by a CustomDataOwner (a plain {type: NATION|WAR|CLAIM, id: String} record, no dependency on WN's internal id types) and an addon-chosen namespace string, so two unrelated addons never collide on the same owner. Values are opaque CompoundTags that WN never reads or interprets.
WnApi.get().customData().set(
CustomDataOwner.nation(id),
"myaddon.progression",
tag);
Callable from anywhere: a command handler, a tick loop, or an effect handler (see below). Storage is backed by CustomDataStoreImpl and persisted through WN's own existing WnSavedDataIO/WnPersistable machinery, exactly like every other WN store.
A per-nation tier-progression checklist (settlements, production, logistics, institutions, military counts), a per-war War Support value, a per-claim "is core city" tag, or a civil war's declaration-time support snapshot: none of these are WN concepts, but every ruleset needs some owner-attached state that isn't one.
EffectHandlerRegistry: open war-script effects
WN's composable war engine (see War Presets) already lets a war-preset file define its own tick/hook scripts. What was closed was the vocabulary those scripts could call: EffectName, a fixed 8-value enum (add_score, transfer_claim, annex_claims, broadcast_message, end_war, set_variable, grant_treasury, take_treasury) dispatched through a hardcoded switch. EffectHandlerRegistry lets an addon add a genuinely new effect, not just a new scoring formula built from the 8 built-ins.
war/composable/
An EffectHandler is a FunctionalInterface taking the existing EffectContext plus resolved args, the identical calling convention a built-in effect uses. Register one by wire name at addon init:
EffectHandlerRegistry.register("set_war_support", (ctx, args) -> {
...
});
From then on, any war-preset file (built-in or a config-dir override) can call set_war_support(nation, amount) from a tick/hook script exactly like a built-in effect. EffectContext.execute is changed by exactly one thing to make this work: when EffectName.fromWireName misses, it now consults EffectHandlerRegistry.get(name) before throwing, instead of throwing immediately. The 8 built-ins, their arity checks, and their switch are untouched; a registered wire name colliding with a built-in name is inert, since built-ins are always checked first, so it fails safe rather than silently overriding.
Two accessors on EffectContext exist specifically for handler convenience: currentWarId() (the war's UUID, useful for keying WnCustomData) and resolveNationRef(Object) (a public wrapper around the same "attacker"/"defender"/third-party-name resolution every built-in effect already uses internally).
A War Support stat driven by an open-ended list of triggering events, or a strategic-target-loss penalty: a war-preset's own script logic decides when to apply them, calling out to an effect WN core has no built-in name for. Combined with WnCustomData for persistence (keyed by currentWarId()), this is a complete, addon-owned effect with no core change.
SubordinationRegistry: hierarchical nation relationships
Every part of claim protection and war participation assumed a nation is a sovereign top-level actor: "at war" was a flat direct-pair check between two nations, with no notion that one nation might answer to another politically, or that a parent's war (or a subordinate's) might legitimately expose the other party's territory. nation.hierarchy.SubordinationRegistry is the seam for real overlord/vassal/colony-style political hierarchy where the subordinate remains a fully independent Nation - its own members, claims, tier, diplomacy, wars - but the relationship itself has consequences.
nation.hierarchy/, plus claim.WarExposureResolver for the claim-protection integration
SubordinationRegistry is a persisted store mapping a subordinate NationId to at most one direct parent, plus a free-form addon-defined kind string ("vassal", "colony", ... not a closed enum, exactly like EffectHandlerRegistry's wire-name vocabulary). establish/release are pure mutation plus a DomainEvent list, matching Nation's own method convention; establish rejects self-parenting and cycles, and supersedes any prior relationship (firing Released then Established). Read-side accessors (parentOf, directSubordinatesOf, allSubordinatesOf, ancestorsOf, isSubordinateOf) give both the immediate and transitive views. A disbanding nation's own relationship is auto-released, and its direct subordinates are cascaded to independence rather than re-parented, since a disbanded nation cannot approve or control anything on their behalf.
Register a relationship at any point after both nations exist:
WnApi.get().establishSubordination(vassalId, empireId, "vassal");
Relationship behavior is a second, independently-registered piece: SubordinationPolicyRegistry (the same "static holder, empty by default" shape as ClaimEligibilityRegistry), keyed by kind and registered once at addon init:
SubordinationPolicyRegistry.register("vassal", myPolicy);
A SubordinationPolicy exposes three independently-defaultable hooks per kind: requiresParentApproval(ctx, actionId) (WN fires SubordinationEvents.ApprovalRequired for a call site that checks this and cares; WN itself has no generic approval queue), parentWarExposesSubordinateTerritory(ctx), and subordinateWarExposesParentTerritory(ctx) (the rarer reverse case, default false). A kind with nothing registered - including one whose defining addon got uninstalled - behaves as fully independent rather than erroring, the same tolerant degradation EffectHandlerRegistry already establishes for a missing wire name.
The actual claim-protection integration, claim.WarExposureResolver.areAtWar, tries the direct-pair check first (zero overhead when hierarchy isn't wired), then walks the hierarchy in both directions: an upward walk from a nation through its own ancestor chain (an ancestor's war reaching down), and a downward walk through a nation's descendant subtree (a descendant's war reaching up). Both walks stop propagating the moment one link in the chain doesn't opt in, and both sides of a pair are checked so areAtWar(a, b) always equals areAtWar(b, a).
A Volkrune-style empire ruleset: an addon calls establishSubordination when a smaller nation accepts vassalage, and registers a "vassal" policy where the empire's wars expose the vassal's territory but not the reverse, and any vassal war declaration first requires the empire's approval. A rebellion inside the vassal (WN's existing RebellionService mechanism) needs no special-casing at all: the splinter nation simply has no relationship recorded, so it's just another third party as far as WarExposureResolver is concerned.
