# Aeshnidae.Expedition

**The Old Ring of Tenkarrdun**: the shared Aerlinthe island's quest. The design is the doc
"The Old Ring of Tenkarrdun" (Tom, 2026-09-27; https://claude.ai/code/artifact/6fe6c3a6-36ab-4781-97ee-89cc7ef09970):
one island for the whole server, played forward act by act, ending when Tenkarrdun erupts,
then back to ash for the next cycle. Built in the doc's order, each act live when it is done.

| Act | State |
| --- | --- |
| 1 - Ashfall | **built 2026-09-28** (this mod's first version) |
| 2 - Light the beacons | **built 2026-10-01** (Beacons.cs, Siege.cs; weenies ledger 2026-10-01_001) |
| 3 - The Old Ring | not started |
| 4 - Tenkarrdun wakes, the payout, the reset | not started |

The island is InstancesNoDat's shared region copy `aerlinthe`, copy 1 - the Aerlinthe Island
(Instance) portal (3000120). The PK Aerlinthe (copy 2) does not count. This mod never changes
InstancesNoDat or OverworldCopies; it binds to InstancesNoDat by reflection (Island.cs).

## Act 1 - Ashfall

- **Meters.** Every monster a player (or a player's pet) kills on the island copy adds its
  level x `MeterPointsPerLevel` (0.01: a level-100 monster is one point) to the meter of the
  zone it died in. The three coastal zones - West Shore, Raina's Harbor, East Shore - each need
  `MeterTarget` 1,500. Meters never go down. The lava floes and the north shore have no meter
  in Act 1 (later acts' ground).
- **The arrival line** (2026-09-28, `ArrivalLine`): arriving on the island by portal, recall or
  login, a player reads the act and each shore's progress and that every kill counts - there is no
  NPC to start the quest (Tom: not until the portal's final home is known). Once per arrival, not
  repeated within `ArrivalLineMinutes`; walking across the island's seams never triggers it.
- **Told.** 25/50/75% to everyone on the island; 100% to the whole server and Discord
  (`#invasions`; staging posts to `#content`).
- **Reclaimed.** At 100% a zone's monsters thin to `ThinTo` (a third): Thinning trims each
  monster generator profile's spawn queue in `GeneratorProfile.ProcessQueue` so it never
  holds more than its share, and removes the surplus standing in the zone's open landblocks at
  the moment of the reclaim (only monsters not in a fight).
- **Camps.** A reclaimed zone gets a settlers' camp: a Settler (3000122-3000124, with that
  zone's part of the story), retail's Campfire (4128) and a Portal to Holtburg (3000125) - at
  the spots where the island variant removed retail quest NPCs (the Ruined Watcher, Smith Ejan,
  the Devastated Watcher). Camps.cs keeps them by reconciling every 5 s: standing in each open
  landblock of the zone while it is reclaimed, gone otherwise - so the island unloading, closing
  and reopening needs nothing special. Camp objects carry PropertyInt 9046 and are never saved to
  the shard (a postfix on `IsDynamicThatShouldPersistToShard`; InstancesNoDat's Keep would
  otherwise save them in the shared copy).
- **Contribution.** Each kill credits its top damager level x `ContributionPerLevel` (0.1: an
  80 is 8). Kept per character per cycle for the payout at the end of Act 4.
- **The act ends** when all three shores are reclaimed; Act 2 opens.

## Act 2 - Light the beacons

The two Empyrean lighthouses at the harbour mouth (`Act2.Beacons`: the West Light, 84.4N 45.7E;
the East Light, 84.3N 47.2E) are relit with **Ember Pitch** (3000126). Act 2 ends when both burn.

- **Fuel.** Island fire elementals - Ember, Fire Wisp (level 80), Firestorm (100), Hellfire (115),
  `PitchDrops` by exact name - carry pitch in their corpses (1, 1, 1-2, 2-3), and each camp's
  settler spares `SettlerPitch` (10) to a character once every `SettlerHours` (20) when used.
  Only while Act 2 runs and a beacon is dark (`Beacons.OnTreasure`, `OnUse`).
- **The braziers.** Pitch is *given to the Lightkeeper* (Aldric Venn 3000128, West; Sefa Marrow
  3000129, East) - giving to an NPC is what the retail client does; the Beacon Brazier (3000127)
  beside each is the landmark. A prefix on `Player.GiveObjectToNPC` takes as much as the brazier
  still needs (`PitchPerBeacon` 200) and leaves the rest in the pack; 5 contribution each.
- **The lighting.** A full brazier's Lightkeeper, used, starts the siege (`Siege.Begin`): a
  `CountdownSeconds` (30) warning to the island, then a band every `BandEverySeconds` (20) for
  `SiegeMinutes` (8) - Spectral and Nightmare Wisps, Shadow Wraiths, sized by the players within
  `PlayerRadius` - then their leader (a Consumed Wraith, level 160) and escort, to be killed
  within `LeaderMinutes` (5). `StrengthPercent` raises every invader's six attributes and health
  (0 = retail; the island is retail strength, so tune it for level 275). One siege at a time.
- **Won:** the beacon burns - the server and #invasions hear it, the defenders' damage is
  contribution (1 per 100), and the beacon's WhenLit camp objects stand: the flame (retail
  Campfire 4128, in the brazier) and the **Portal to the Crater Rim** (3000130, landing
  0xB9F00006 [4.5 130.1 126.01], 89 m from the crater's centre - Act 4's road). Its light lifts
  health and stamina by `RegenPercent` (1%) every `RegenEverySeconds` (5) for players on
  reclaimed ground within `LightRadius` (250 m).
  **Lost** (the leader lives past the deadline, or slips away): `FailBurnShare` (half) of the
  pitch burns; the brazier can be filled again. Damage still counts.
- **The siege is Invasions' engine, copied** (cross-mod types are unsafe): bands, march, damage
  snapshots, effects. What differs: every invader goes into the island copy
  (`Island.EnterIntoCopy`), and every spot it is put on is found on a landblock open in the copy,
  tested with that copy as the ambient (`Siege.IslandSpot`). ACE's physics looks a landblock up
  through LandblockManager, so a ground test on the world thread with no copy in hand LOADS THE
  REAL LANDBLOCK - the first staging test brought the real B8E8, B8E9 and B7E8 up. (Invasions
  works on the real world, where that is what it wants.)
- **Objects** are `Settings.Camps` entries with `Beacon` (and `WhenLit` for the flame and the
  portal), kept by the camps' reconciler: Lightkeeper and brazier from Act 2 on.
- A restart or reload mid-siege ends it with nothing burned (`Siege.Shutdown`); the brazier stays full.

## Testing values (2026-10-01)

While the quest is tested, live and staging run a tenth of the design: shore `MeterTarget` 150 (1,500) and
`Act2.PitchPerBeacon` 20 (200), meters rescaled to keep their percent. The code's defaults are the design
values; the deployed Settings.json carries the testing ones (backup `.bak-2026-10-01-testing`). CLAUDE.md
has the restore steps.

## State

Shard tables, created on first load: `aeshnidae_expedition_cycle` (one row per cycle: act and
state JSON; the open cycle is the one with no `finished_at`) and
`aeshnidae_expedition_contribution` (cycle, character, contribution, kills). Written every
`FlushSeconds` off the world thread and on unload.

## Commands

| Command | Who | What |
| --- | --- | --- |
| `/expedition` | anyone, anywhere | the act, each zone's bar, your contribution, the leaders |
| `/expedition-admin` | Admin, console | status: binding, cycle, meters, counters, Discord |
| `/expedition-admin set <zone> <pct>` | Admin, console | test: a zone's meter, told and reclaimed as kills would |
| `/expedition-admin newcycle` | Admin, console | test: end the cycle, start the next at 0% |
| `/expedition-admin camps` | Admin, console | every camp object and whether it stands |
| `/expedition-admin act <n>` | Admin, console | test: jump to an act |
| `/expedition-admin pitch <beacon> <n>` | Admin, console | test: a brazier's pitch |
| `/expedition-admin light <beacon>` | Admin, console | test: start a siege, as the Lightkeeper would |
| `/expedition-admin unlight <beacon>` | Admin, console | test: put a beacon out (Act 2 back in play) |
| `/expedition-admin siege [stop\|skip\|smite [n]]` | Admin, console | the siege; stop (nothing burns), leader now, kill invaders on their own landblock (no credit) |
| `/expedition-admin ground <N> <E> [min max]` | Admin, console | open ground near map coordinates, for pinning spots (it loads the real landblocks it looks at) |
| `/expedition-admin reload` | Admin, console | re-read Settings.json |

## Tested (staging, 2026-09-28, console)

Milestones and the reclaim; the cull (1 monster in the one open landblock) and the queue trim
(5 spawns held back when B3EC reopened); the West Shore camp placed in the island copy and not
in the real B3EC (`inst-keep find`); closing the copy saved nothing (no 9046 rows); reopening
put the camp back within a check; a mod reload kept the reclaim and the camp; all three shores
ended Act 1; `newcycle` took the camps down. **Not yet**: a kill by a real player counting
(needs a client), camp placement looked at in the client.

## Act 2 tested (staging, 2026-10-01, console - nobody online, island landblocks opened with `inst-keep open`)

Lightkeepers and braziers stand in the island copy from Act 2 (`act 2`), flames and rim portals
only once lit. A siege: 30 s countdown, bands of 3 every 20 s placed in the copy ("added to copy 1
of B8E8/B8E9"), the leader and escort on `siege skip`. **Won** (`siege smite`, run on each
creature's own landblock): the beacon lit, its flame and portal stood, the invaders' corpses landed
in the copy, Discord posted; both lit ended Act 2 and opened Act 3 with the announcement. **Lost**
(leader window 30 s for the test): "Consumed Wraith was not killed in time", 200 pitch -> 100,
1 siege lost, Act 2 still in play. `unlight` and `stop` (nothing burned) work. Found and fixed on
the way: the ground test and the march each LOADED THE REAL landblock from the world thread
(both now run with the island copy as the ambient); a smite from the world thread put corpses in
the real landblock (now on the creature's own landblock). A real landblock still loaded once while
InstancesNoDat rescued player-less test copies from ACE's unload queue - a console-test artifact;
live shows no real-landblock loads during play. **Not yet**: anything in a client - giving pitch
to a Lightkeeper, a settler's handout, a pitch drop, the light's regeneration, the rim portal's
landing, how the brazier and flame look; a siege against real players and its strength.

## Deploy

Went live 2026-09-28 with a graceful restart; a new mod now goes live with RemoteConsole's
`mod-load` instead, and this one is already loaded, so later acts are hot reloads.
Staging: build from `~/ace/Mods/src-staging/Aeshnidae.Expedition` with
`-p:OutputPath=$HOME/ace-staging/Mods/Aeshnidae.Expedition/`. The weenies are ledger
2026-09-28_002 (Act 1) and 2026-10-01_001 (Act 2). A deployed Settings.json predating Act 2 gets its
beacon camp objects and the Act2 section merged in by `act2_merge.py` (kept in the session's
scratchpad; it preserves every existing value and leaves a .bak).
