The Autoload Model
Underroot runs almost all of its game state through Godot autoload singletons. Below: every autoload in load order with a one-line role, and the reset() contract that returns the shared singletons to a clean state for a new run.
What the autoloads are
Autoloads are registered in the [autoload] block of project.godot. Each entry is a script attached as a globally accessible singleton node; the * prefix means the singleton is a node added to the scene tree root. Because they are nodes at the tree root, they persist across scene changes (IntroStory to TitleScreen to Main) and hold the run's authoritative state while scenes come and go.
Load order matters: an autoload listed later can safely reference one listed earlier during _ready(), but not the reverse. For example EventBus loads before every system that emits on it, and DataRegistry loads before the managers that read material and recipe data at startup.
All autoloads (load order)
The 36 autoloads, in the exact order they appear in project.godot:
| # | Name | Script | Role |
|---|---|---|---|
| 1 | FontSetup |
scripts/core/FontSetup.gd |
Attaches embedded fallback fonts (emoji subset, etc.) to the default font — the Web export has no OS fonts. |
| 2 | PerfSetup |
scripts/core/PerfSetup.gd |
Applies Low Performance Mode at startup, before any renderer reads PerformanceMode.enabled. |
| 3 | TimeManager |
scripts/core/TimeManager.gd |
Day cycle (day_time 0–1, 360 s/day) and unix time. |
| 4 | AudioManager |
scripts/core/AudioManager.gd |
Music play/stop/volume; songbook and surge-track playback. Also owns what a Black Hollow expedition restores on exit — the town rotation in-run, the title or death theme from the title screen. |
| 5 | ToolState |
scripts/core/ToolState.gd |
Equipped tools, durability, owned tools, processing machines. |
| 6 | EventBus |
scripts/core/EventBus.gd |
All cross-system signals. Nothing else lives here. |
| 7 | DataRegistry |
scripts/resources/DataRegistry.gd |
Loads every data/*.json at startup; material/tool/recipe/machine lookups. |
| 8 | SaveManager |
scripts/core/SaveManager.gd |
12 save slots, config.json, save versioning and migration. |
| 9 | Inventory |
scripts/resources/Inventory.gd |
resources dict; add / remove / get_amount / can_craft. |
| 10 | TaskQueue |
scripts/idle/TaskQueue.gd |
Offline/idle task queue. |
| 11 | WorldManager |
scripts/world/WorldManager.gd |
Tile grid, walls, rooted walls, gravestone, surface connectivity. |
| 12 | BuildManager |
scripts/building/BuildManager.gd |
Wall placement and stacking logic (1 raw unit per layer). |
| 13 | MawAdaptation |
scripts/maw/MawAdaptation.gd |
Per-material familiarity multipliers, credited per unit eaten. |
| 14 | MawController |
scripts/maw/MawController.gd |
Maw fronts, chew rate, pressure escalation, breach detection. |
| 15 | ForestManager |
scripts/forest/ForestManager.gd |
Tree/bush/pond/boulder state. |
| 16 | SurvivalManager |
scripts/village/SurvivalManager.gd |
food/water/stored/well drain logic and idle multiplier. |
| 17 | VillageManager |
scripts/village/VillageManager.gd |
Population, support metrics, buildings, daily tick, villager visits. |
| 18 | ProjectManager |
scripts/village/ProjectManager.gd |
Villager task spawn/fulfil/expiry (data/projects.json). |
| 19 | OfflineSimulator |
scripts/idle/OfflineSimulator.gd |
Catch-up simulation on resume (tasks, survival, fuel, Maw, machines). |
| 20 | MachineManager |
scripts/machines/MachineManager.gd |
Active machines dict + spatial footprint index. |
| 21 | GameManager |
scripts/core/GameManager.gd |
Run state (current_run), pause, revive/lineage. |
| 22 | SkillManager |
scripts/core/SkillManager.gd |
Per-run skill XP + chosen class; credits use-based XP off action signals, applies back-loaded % bonuses (via SkillMath), halves every skill on lineage death. State rides current_run (skills/class). |
| 23 | SkillNotify |
scripts/core/SkillNotify.gd |
Turns EventBus.skill_leveled into a player toast (keeps SkillManager UI-free). |
| 24 | CosmeticManager |
scripts/core/CosmeticManager.gd |
Account-wide cosmetic ownership + equipped loadout; all unlocks route through unlock(). Owns the dedicated class cosmetic slot (auto-seeded per calling; node-7 prestige grant). |
| 25 | RockManager |
scripts/world/RockManager.gd |
Surface rock depletion state. |
| 26 | DiscoveryManager |
scripts/world/DiscoveryManager.gd |
Underground discovery pockets. |
| 27 | CraftingUnlockManager |
scripts/core/CraftingUnlockManager.gd |
Unlocks recipes once all input materials have been seen; also gates class-landmark recipes on champion class + skill node, and the Hollow Crucible on recovered schematics (unlock_requires_schematics). |
| 28 | SpecialistManager |
scripts/village/SpecialistManager.gd |
Four Astrolabe specialists; Artificer gated by village population. |
| 29 | AstrolabeManager |
scripts/core/AstrolabeManager.gd |
Ritual activation, permanent Maw escalation, exotic seeding. |
| 30 | WeatherManager |
scripts/core/WeatherManager.gd |
Rain/storm state machine (storms kill villagers and destroy stock); suspends during an in-run Black Hollow expedition. |
| 31 | FireworksLayer |
scenes/ui/FireworksLayer.gd |
Milestone fireworks overlay (layer 20). |
| 32 | SessionTelemetry |
scripts/core/SessionTelemetry.gd |
Local-only playtest telemetry — JSONL per session in user://telemetry/; nothing transmitted. |
| 33 | CodeManager |
scripts/core/CodeManager.gd |
Code redemption; codes.json keyed by SHA-256 of the uppercase code; one-time tracking in config.json. |
| 34 | ChallengeManager |
scripts/core/ChallengeManager.gd |
Opt-in run modifiers (Two Fronts, Black Rot, …); active set in current_run.challenges; effect multipliers. |
| 35 | HarrowManager |
scripts/core/HarrowManager.gd |
The Artificer's Harrow — custom-challenge designer (7th Challenge card); catalog, unlock gate, config hygiene. |
| 36 | BlackHollowManager |
scripts/core/BlackHollowManager.gd |
Wren's Expeditions: Black Hollow — expedition availability/cooldown, depth-unlock, entry-fee, depth-band/multiplier helpers, the standalone story mode's route chain (is_story_unlocked / story_advance_depth / advance_story_chain), and resolve_expedition(haul) (credits rewards + advances village time). The mini-game overlays talk only to this + DataRegistry. |
HarrowManageris a real autoload (registered inproject.godot) but is absent from the architecture guide's autoload table. Effect math for a Harrow config lives inChallengeManager;HarrowManagerowns the dial catalog and encode/decode.
The skill/class system spans two of these autoloads plus one non-autoload helper: SkillManager is the numeric spine (XP crediting, get_mult, handicaps, inheritance), SkillNotify is its UI adapter, and SkillMath (below) is the pure math. See Skill Tree and Classes.
The singleton pattern
Every autoload script extends Node. State lives in plain member variables (current_run, resources, walls, fronts, …), and behaviour is exposed as public methods. Callers reference the singleton by its registered name from anywhere:
Inventory.get_amount("iron") # → int
DataRegistry.get_material(mat_id) # → Dictionary ({} on miss)
MawController.refresh_threat_state()
Two conventions keep this decoupled:
- Cross-system events go through
EventBus. Managers emit signals rather than calling each other's methods directly. See EventBus and the Signal Convention. - Data lookups go through
DataRegistryand return{}on a miss, so callers always use.get("key", default).
Note that some frequently referenced globals are not autoloads. GameConstants (scripts/core/GameConstants.gd), SkillMath (scripts/core/SkillMath.gd — pure static leveling math for the skill system; takes its config explicitly so tools/smoke_test.gd can exercise it deterministically), CommunityStats (scripts/core/CommunityStats.gd), UITheme (scripts/ui/UITheme.gd), HudShell (scripts/ui/HudShell.gd — HUD-skin state for the two-skin HUD), and LayerNav (scripts/ui/LayerNav.gd) are class_name types used statically — they are not in the [autoload] block and hold no per-run state.
The reset() contract
A fresh run must return the shared singletons to a clean baseline without reloading the whole game. GameManager.reset_for_new_run() is the single orchestration point: it rebuilds current_run and then calls each subsystem's reset entry point in dependency order.
The architecture guide highlights three singletons that expose an explicit reset() and should be reset through it rather than by poking _-prefixed fields:
| Singleton | Reset entry point |
|---|---|
SurvivalManager |
reset(p_food, p_water, p_stored_food = 0.0, p_stored_water = 0.0) |
MawController |
reset() |
ToolState |
reset() |
SurvivalManager.reset() takes the starter stores explicitly; reset_for_new_run() passes the canonical GameConstants.STARTER_* values:
SurvivalManager.reset(GameConstants.STARTER_FOOD, GameConstants.STARTER_WATER,
GameConstants.STARTER_STORED_FOOD, GameConstants.STARTER_STORED_WATER)
MawController.reset()
MawAdaptation.familiarity.clear()
Inventory.resources.clear()
ToolState.reset()
The same reset_for_new_run() continues through the rest of the world: MachineManager.reset(), WorldManager.reset_world(randi()), RockManager.generate(randi()), ForestManager.reset(), VillageManager.reset_village(), ProjectManager.reset(), TaskQueue.clear(), DiscoveryManager.reset(), CraftingUnlockManager.reset(), AstrolabeManager.reset(), and finally ChallengeManager.apply_run_start() (applied last so challenge starting conditions win over the normal resets). Several other autoloads expose their own reset() for this purpose, including WeatherManager, MachineManager, ForestManager, ProjectManager, AstrolabeManager, CraftingUnlockManager, and DiscoveryManager. SkillManager.reset() is a no-op — the run's skills/class are rebuilt with current_run by GameManager, and the chosen class is applied from SkillManager.pending_class. BlackHollowManager needs no reset(): its only per-run state is current_run.black_hollow_cooldown, rebuilt with current_run; everything else it owns — the chronicle, schematic count, best depths, and the story mode's chain and best scores — is account-level in config.json and deliberately survives a new run.
A lineage continuation (carry-on after a death) is not a new run: it keeps the same world and never calls
reset_for_new_run(). See GameManager and the Run Lifecycle for the revive path.
Key files
project.godot— the[autoload]block; authoritative load order.scripts/core/GameManager.gd— ownsreset_for_new_run()and the run lifecycle.scripts/core/EventBus.gd— the signal hub every autoload emits through.scripts/resources/DataRegistry.gd— startup data load consumed by the manager autoloads.scripts/core/GameConstants.gd— non-autoload constants (STARTER_*,TILE_SIZE) used during reset.