⚓ Seaworth Games

lib/campaign

What Crosses the Border

A campaign isn't its own map format. It's a container holding several ordinary .h3m files — the exact read-only format Map Formats already covered — plus the rules for what a hero is allowed to carry from the end of one into the start of the next.

A tree of ordinary maps

CampaignScenario::mapName is declared, in the source's own comment, as //*.h3m. Each scenario is a real map file, not a special campaign-only structure.

Campaign : public CampaignHeader holds std::map<CampaignScenarioID, CampaignScenario> scenarios, and each CampaignScenario carries its own preconditionRegions — the set of other scenarios that must be conquered first. That's a real unlock graph, not a numbered sequence; nothing stops two scenarios from sharing a precondition or unlocking in parallel. CampaignState : public Campaign is the save-in-progress subclass, and its mapPieces field is where the container idea becomes literal: std::map<CampaignScenarioID, std::vector<uint8_t>> — the raw, still-binary .h3m bytes for every scenario, embedded whole inside the one loaded .h3c. CampaignHandler.cpp confirms it directly: mapPieces[scenarioID] = files[fileIndex], copied straight out of the archive, no reformatting.

What crosses

Every edge in that unlock graph carries its own CampaignTravel — the answer to “what does a hero keep, and what does the player get to choose,” scenario by scenario.

CampaignTravel — one per scenario edge WhatHeroKeeps experience, primarySkills, secondarySkills, spells, artifacts — five plain bools Explicit keep-lists monstersKeptByHero artifactsKeptByHero named, not categorical bonusesToChoose vector<CampaignBonus> one of ten variants a real player choice startOptions: NONE / START_BONUS / HERO_CROSSOVER / HERO_OPTIONS
Ten CampaignBonus variants exist — Spell, Creatures, Building, Artifact, SpellScroll, PrimarySkill, SecondarySkill, StartingResources, HeroesFromScenario, StartingHero — and bonusesToChoose is a list of them for one scenario, picked at the campaign map screen before that scenario's own game session even starts. This is a different moment than What's On the Map's in-game reward Querys — there's no running CGameState yet to block a query against, just a menu screen deciding what the next one starts with.

A third serialization path

Every other pack and every savegame this atlas has traced goes through the one binary framework Bytes and Types covered. A hero crossing from one scenario into the next doesn't.

CampaignState::crossoverSerialize(CGHeroInstance * hero) returns a JsonNode, not bytes through BinarySerializer; crossoverDeserialize() reads one back. That's deliberate, not an inconsistency: each scenario is its own complete game session with its own freshly-constructed CGameState, and Bytes and Types's polymorphic-pointer machinery and vectorized ids only mean anything inside one running session's object graph — there's nothing on the other side of a scenario boundary for a raw pointer or a type id to resolve against. JSON is the format that survives a hero being fully torn down and rebuilt as a different object in a different map's CGameState a scenario later.

Two more carry-overs, easy to conflate but distinct: scenarioHeroPool/globalHeroPool hold heroes reserved for campaign placeholder slots — a different pool from The New Turn's HeroPoolProcessor, which only ever manages an ordinary tavern. And persistentScriptVariables, saved and reseeded via savePersistentVariables()/seedPersistentVariables(), is where a Lua/ERM script marked to persist survives the same boundary — the scripting side of Tooling Belt's registry, carried by name rather than by any of the mechanisms above.