Spec Handlers
How a generated spec becomes the SpecHandler the loop drives, via CombatSystemBuilder and the inventory registry
The loop drives a SpecHandler trait object and knows nothing else. The thing on the other side of that trait, for every spec, is one type: CombatHandler. This page is about how a declarative spec definition becomes a CombatHandler the loop can run. The path runs from generated code, through the builder, into the registry the bootstrap looks up.
One handler, every spec
SpecHandler is the contract between the clock and the combat logic. Its methods mirror the event variants: on_player_ready returns the next SpecAction, on_cast_complete runs the cast pipeline, and on_aura_tick, on_aura_expire, on_auto_attack, on_cooldown_ready handle the rest. Around those are lifecycle hooks like on_sim_start and reset, the flush_scheduled drain the loop calls after every dispatch, and the total_damage read-out the loop needs after a run.
There is exactly one production implementation, and it bundles a CombatState (the static spell/aura definitions plus mutable non-buffer state), a DenseBuffer (the runtime state the rotation reads), a RotationEngine (the compiled rotation), and its own SimRng:
Its trait methods map cleanly onto combat functions. on_player_ready syncs resources and target health, evaluates the rotation, and GCD-gates the result. on_cast_complete builds a CombatCtx and calls process_cast. flush_scheduled drains the handler's pending events into the loop's queue.
So "a spec" is not a type. It is a configuration of spells, auras, resources, and a rotation, fed into the one CombatHandler.
From manifest to handler
That configuration is authored as a TOML manifest and turned into Rust at build time by the code generator (covered under Codegen). The generated code for each spec is three things wired together: a build_combat_system function, a new_handler factory, and a define_spec_descriptor! registration.
build_combat_system is a long fluent chain on CombatSystemBuilder. It declares the resource, the secondary resource if any, every aura and spell and auto-attack, threads in the ResolvedGameData snapshot, sets fight duration and enemy count, registers equipped items, and finally calls build(rotation_json). The builder methods take closures so each spell and aura can be configured inline, spell(name, id, |s| ...) and aura(name, id, |a| ...), and the generated builder functions inside those closures read values straight out of ResolvedGameData wherever the manifest left a field unspecified. The manifest is overrides; the game data is the default.
build is where compilation happens. It compiles the rotation JSON into a RotationEngine (JIT or interpreter, per the rotation compiler), assembles the Vec<SpellData>, populates the initial dense buffer, and returns the four pieces a handler needs:
The generated new_handler then wraps that built system into a CombatHandler boxed as a Box<dyn SpecHandler>, by default through default_new_handler, or through a spec-specific custom handler when the manifest declares one.
The inventory registry
The bootstrap does not import any spec by name. It looks one up by id at runtime, and the lookup works because every generated spec registers itself at link time through the inventory crate. The define_spec_descriptor! macro expands to an inventory::submit! of a SpecDescriptor, carrying the spec id, class id, display name, talent list, hero-talent trees, and the handler_factory function pointer:
At bootstrap, find_descriptor(spec_id) iterates the inventory and returns the matching descriptor, or a SpecConstruction error if none is registered. Then bootstrap calls that descriptor's handler_factory with a HandlerParams carrying the resolved game data, rotation JSON, stats, talents, gear, buffs, and race, and gets back the boxed handler. That is the join: the orchestration layer finds a descriptor it never linked against by name, and the descriptor knows how to build its own handler.
One subtlety the registry depends on: inventory registrations can be dead-code-eliminated if nothing references the generated module. The generated specs/mod.rs defends against this with a force_link_generated_specs function that touches each build_combat_system pointer through black_box, so the linker keeps the submit! calls. Without it, a release build could silently drop specs from the registry.
The introspection trick
There is a chicken-and-egg problem in this pipeline worth calling out, because it shapes how ResolvedGameData is built. To resolve a spec's game data, the bootstrap needs to know which spell and aura ids the spec uses. But the only thing that knows those ids is the handler, which cannot be built without the resolved game data.
The resolver breaks the cycle by building a throwaway handler against empty default game data, solely to call introspect() and read back the spell, aura, and auto-attack ids the spec declares. Those ids drive the real resolution pass, and the real handler is built afterward against the populated data. The SpecHandler trait carries introspect precisely so this discovery step exists. The accessors on ResolvedGameData are written to tolerate the empty case. They return defaults when the data map is empty (the introspection path) but None when the map is populated yet missing a spell, which the generated code turns into a hard MissingSpellData error. The full mechanics are under Data Resolution.
With the handler built, registered, and driven by the loop, the only thing left is reading the result back out. The metrics page covers how a run's events become the telemetry the rest of the platform consumes.
Nächste Schritte
