⚓ Seaworth Games

lib/entities · lib/modding · lib/IHandlerBase.h

One Handler, Every Entity

Creatures, spells, artifacts, factions, heroes, even the seven basic resources — six different config-loading systems that turn out to be one template, instantiated six times. This is that template, and the mod loader that feeds it strings it has to resolve to numbers it doesn't have yet.

One template, six entities

lib/IHandlerBase.h — not inside lib/entities/ at all, but the file everything here is built on — declares one template that every entity handler in the game instantiates.

CHandlerBase<ObjectID, ObjectBase, Object, ServiceBase> one template — storage, lookup, iteration, all generic Creature CCreatureHandler lib/ root Spell CSpellHandler lib/spells Artifact CArtHandler entities/artifact Faction CTownHandler entities/faction Hero CHeroHandler entities/hero Resource ResourceTypeHandler entities/ root every concrete handler's load path is the same three calls loadObject() loadFromJson() registerObject() loadFromJson() is pure virtual — the one method each concrete handler writes itself → IdentifierStorage, below
CHandlerBase<ObjectID, ObjectBase, Object, ServiceBase> supplies everything generic — the objects vector, getById()/getByIndex()/getByName(), forEach(), both operator[] overloads. Six real instantiations confirmed directly from source: CCreatureHandler : CHandlerBase<CreatureID, Creature, CCreature, CreatureService>, CSpellHandler : CHandlerBase<SpellID, spells::Spell, CSpell, spells::Service>, and four more with the identical shape for artifacts, factions, heroes, and resources. A concrete handler's entire job is one override, loadFromJson() — turn one JSON object into one shared_ptr<Object> — plus whatever domain-specific extras it wants on top.

Where the handlers actually live

The Systems Map's own entities/ row underclaims this directory. It's not a flat pile of 36 files — it's four subdirectories, and it doesn't hold every handler.

entities/artifact/ is the largest and busiest (16 files) because artifacts have real runtime state beyond config — CArtifactInstance, CArtifactSet, CArtifactFittingSet for combination artifacts, alongside CArtHandler itself. entities/building/ is nearly empty (3 files, mostly data, not code) because a building's behavior lives in generic bonus-granting config, not a bespoke class per building. entities/faction/ and entities/hero/ each pair their handler with the object classes it produces (CFaction+CTownHandler; CHero/CHeroClass+CHeroHandler/CHeroClassHandler).

What's not here: CCreatureHandler lives at the top of lib/, and CSpellHandler lives in lib/spells/ — this atlas's Spell System doc already named it without saying what it was built on. Placement here is historical, not architectural — the pattern is what's consistent, not the folder.

One correction to this atlas's own Tooling Belt doc: it called ScriptHandler “a config-loading handler exactly like every other entity handler this atlas has met.” Checked directly — ScriptHandler final : public IHandlerBase, public ScriptService. It shares the mod-facing half of the pattern (loadObject(), registerObject(), the whole point of this section) but not CHandlerBase itself; it manages Lua script registration through its own storage, not the generic objects vector. Close, not identical — worth the correction now that the actual template is on the table.

Resolving a name later

A creature's JSON can reference a spell that hasn't loaded yet. CIdentifierStorage exists because “load everything in the right order” isn't a promise this system makes.

Every string identifier follows one format, per the class's own header comment: <type>.<name>, camelCase, e.g. creature.grandElf, optionally prefixed with a mod scope (core:creature.grandElf). Instead of resolving a reference the instant it's read, requestIdentifier(scope, type, name, callback) stores a std::function<void(si32)> and moves on — the JSON parser never blocks waiting for something that might not exist yet. Resolution happens later, during a dedicated FINALIZING phase (ELoadingState tracks LOADING → FINALIZING → FINISHED) once every mod has finished registering its own objects.

finalize() is where every scheduled callback actually runs. Anything that still can't resolve is collected into failedRequests — unless it was marked optional — and getModsWithFailedRequests() hands back exactly which mods have dangling references, by name, for the error a modder actually sees. The forward-reference problem doesn't get solved by ordering; it gets solved by deferring the question until every answer that's going to exist, exists.

One content type, many mods

Between a mod's mod.json saying it adds creatures and CCreatureHandler::loadObject() actually running sits one more layer — and it's also where one mod gets to edit another's data.

ContentTypeHandler

one data type, one handler, every mod's contribution

Wraps a single IHandlerBase * for one content type ("creature", "spell", …). Its modData map holds each mod's own new objects, and separately, ModInfo::patches — map<object name, vector<JsonNode>> — every other mod's edits to this mod's objects, keyed by which object they touch.

CContentHandler

the map that ties a type name to its handler

map<std::string, ContentTypeHandler>, one entry per content type, indexed by the exact string a mod's JSON uses. preloadData() then load() walk every mod in load order; a patch is just another JSON blob merged in before loadFromJson() ever sees the final object.

A save remembers its mods

This atlas's Bytes and Types doc found one compatibility check — a bounded serialization-version window. This is a second, independent one, layered on top.

ActiveModsInSaveList::serialize() branches on h.saving like any other serializable class this atlas has met, but what it does on each side is asymmetric. Saving: collect every gameplay-affecting active mod and write each one's ModVerificationInfo alongside it. Loading: read that same list back, then call verifyActiveMods() — which, per its own declaration, “throws on failure.” A save file doesn't just remember what version of VCMI wrote it; it remembers exactly which mods were active and checks the running game against that list before anything else happens. Two separate compatibility gates, one for the wire format, one for the content that format describes — neither substitutes for the other.