The Package API is the part of the Module a Package may name. A Package compiles into the Module
wasm and can reach any crate:: path the compiler resolves; this document says which of those paths
core will keep working, and cargo test -p lyracore-module --test package_api_lint reports paths outside that contract.
This is a contract, not a tutorial. Read packages/README.md for how a Package is installed and
the Reference Packages for working examples.
May be relied on. Everything listed below keeps working across core changes at this version. A hook event keeps its name and its payload fields. A listed operation keeps its name, its parameter order and its Refusal behaviour. A table accessor keeps its name.
May move with notice. The surface grows by addition: a new hook event, a new payload field, a new
operation. A change that breaks a Package bumps this version and is called out in CHANGELOG.md. An
_Avoid_ rename or a module split may move a path, and the lint below is what makes that visible at
build time rather than at run time.
Never promised. Every crate:: root not listed here. Row layouts of tables a Package does not
own. The order core operations run in, beyond the ordering stated below. Anything reached through
ctx.db that is not a listed table accessor: it compiles, and core may change it in any release.
The Package API is a compatibility contract, not a sandbox. Compiled Package code runs inside the
Module with full access to every table, exactly as lyracore packages add's Trust Review states. The
lint tells a Package author when core moved under them; it stops nothing at run time.
crate::actor::EffectKind defines the known effect codes shared by the Module, Gateway and
Importer. Convert a row's kind byte with EffectKind::try_from; handle unknown authored codes
explicitly. Existing Actor byte constants keep their values. Version 4 retains the five roots
and Actor operations from version 3.
The sanctioned extension points. Each is a text-scanned marker module/build.rs turns into a
registry entry; the macro documentation in module/src/lib.rs is authoritative for the exact shape,
which must be written literally.
| marker | registers |
|---|---|
crate::game_hook!(EVENT, fn NAME(ctx, payload) { .. }) |
a notify handler for one hook event |
crate::game_tick_pass!(fn NAME(ctx) { .. }) |
a periodic pass, run at the end of every tick_creatures tick (0.5s), after every core pass |
crate::game_client_command!(PARSE, APPLY, REPLY) |
the single Package parser, admitted apply operation and reply command name for authenticated addon commands |
crate::character_owned!(delete | restamp | transfer | not_transported, ..) |
a Package table's character-keyed sweeps and its cross-shard transport arm |
crate::encounter_package!(BINDING, fn NAME(ctx, instance_id, signal) { .. }) |
encounter authority for one Encounter Binding |
crate::game_package_characters!(fn NAME(ctx) { .. }) |
the guids of the Characters the Package controls on this Shard, read by Package Teardown |
A Package table that is keyed by character_guid needs a delete marker and a transport arm. Without
them a despawned character leaves rows behind, and a character that crosses a shard loses them.
Handlers are notify-only: they observe the payload and may act through the operations below. There is
no veto and no fold. Payload fields are documented at the struct in module/src/hooks.rs, which is
authoritative; this list is the set of event names and their payload types.
| event | payload |
|---|---|
on_damage_taken |
crate::hooks::DamageTakenPayload |
on_death_prevented |
crate::hooks::DeathPreventedPayload |
on_creature_spawn |
crate::hooks::CreatureSpawnPayload |
on_levelup |
crate::hooks::LevelupPayload |
on_group_invite |
crate::hooks::GroupInvitePayload |
on_death |
crate::hooks::DeathPayload |
on_kill |
crate::hooks::KillPayload |
on_aggro |
crate::hooks::AggroPayload |
on_cast_resolved |
crate::hooks::CastResolvedPayload |
on_cast_finished |
crate::hooks::CastFinishedPayload |
on_loot |
crate::hooks::LootPayload |
on_quest_accept |
crate::hooks::QuestAcceptPayload |
on_quest_turnin |
crate::hooks::QuestTurninPayload |
on_login |
crate::hooks::LoginPayload |
on_character_relocated |
crate::hooks::CharacterRelocatedPayload |
on_logout |
crate::hooks::LogoutPayload |
on_gossip_select |
crate::hooks::GossipSelectPayload |
on_creature_death |
crate::hooks::CreatureDeathPayload |
on_hp_threshold |
crate::hooks::HpThresholdPayload |
on_go_used |
crate::hooks::GoUsedPayload |
actor::set_sessionless_action_consent(ctx, character_guid, allowed) records a core-owned consent
projection during bot setup or controller selection. Call it for repeated selection too: every call
removes that Character's unclaimed Group Intents. Do not call it on decision ticks. Absent consent
preserves Legacy behavior. Core Gates check consent when accepting an invite or claiming an Intent.
The Gateway obtains acknowledged admission from the owning World Shard before it accepts at Realm-core. Admission committed before a concurrent controller change can finish afterwards; this ordering does not promise an atomic operation across Shards.
package::create_character(ctx, package_name, name, race, class) -> Result<u64, String>
creates a Character with no Session and returns its guid. It applies the Refusals a client-created
Character meets: NAME_IN_USE for a taken name, INVALID_RACE_CLASS for a pair the imported
CharBaseInfo does not list, and the guid range Refusal. These Refusals write nothing.
The Character goes on the first Account that package_name owns with room for another Character.
When every owned Account is full, Core creates a new Account without credentials, so no login can
reach it. game_package_account records package_name as its owner. That record is a Core table,
so it stays when the Package is disabled. Pass the Package's own name.
The Character starts at level 1 at its race and class start position, with zero gender and appearance bytes. The Package places it, levels it and builds its live entity.
teardown_package(package_name) is an Operator reducer. Run it on every Shard of the Realm before a
Package leaves the build, then run it once more on each Shard. The second pass catches a Character
that crossed into a Shard that was already torn down. lyracore packages disable runs both passes.
In one transaction, teardown does these steps:
- It makes each Character of the Package a Dormant Character. The Character goes offline and loses its live entity, its Sessionless Action Consent and its pending Transfer and Group Intents. Its Account and Character rows stay.
- It empties every table the Package declares.
- It deletes the Package's Package Config rows.
- It stops every hook, tick pass, encounter handler and client command the Package registered.
The Characters of a Package are the Characters on Accounts it owns on this Shard, plus the guids its
game_package_characters! read returns. Register the read when Core cannot find every Character
through ownership. Two cases need it: a Character that crossed from another Shard, and a Character
on an Account made before Package-owned Accounts existed. A Character with a World Session is never
made Dormant.
Teardown refuses while one of these Characters has a Transfer escrow row or a claimed Transfer Intent on the Shard, because the escrow names the Package's tables. A Refusal writes nothing. Retry when the crossing settles.
A Package stays stopped until the first tick of a build that does not compile it. Enabling it again then starts it fresh: empty tables, default Package Config, and no Characters from before. Its old Characters stay Dormant.
crate::package::encounter holds the encounter state machine and the choreography verbs a Package drives it
with: get_encounter_state, set_encounter_state, get_encounter_data, set_encounter_data,
watch_hp_threshold, reset_hp_fired, open_door, spawn_wave, equip_swap, move_to_point,
encounter_reset, encounter_reset_full, and the four ENCOUNTER_* state constants. Core ships no
encounter content; the kernel exists for Packages.
move_to_point sends the creature on one leg from the point the client draws it at. The creature's
stored position follows the leg and reaches the destination when the leg lands, so a Package that
needs the arrival schedules it from the leg, as a relay arrival does, and does not read the
destination from the row at once.
crate::actor holds explicit-guid operations with the Gates of the core operation each names.
Existing verbs keep fn verb(ctx, actor_guid, ..) -> Result<(), String>. The table in
module/src/actor.rs lists their contracts.
The same root exposes the typed client-command parser and admitted apply values, the target Receipt
capacity that bounds retained per-issuer command fences, and the exact companion_target_facts
read. Core authenticates the issuer and the Gateway certifies Realm-core party authority before a
Package receives an admitted command.
actor::cast_readiness(ctx, actor_guid, spell_id, target_guid) applies the same read-only spellbook,
supported-lifecycle, range, line-of-sight, and ordinary cast Gates used at cast start. A Package can
turn an OutOfRange or NoLineOfSight Refusal into a movement prerequisite without spending power,
starting a cooldown, or creating a cast.
Provisioning uses six Actor operations. select_profile_talent performs a bounded preferred-tree
read through the owning talent admission calculation. reconcile_profile_spell,
reconcile_profile_skill, and learn_profile_talent return Result<bool, ActionRefusal>; true
means they changed normal Character state and false means the selected profile entry was already
satisfied.
reconcile_profile_item returns the granted count and caps its target at 200.
equip_profile_upgrade returns whether it equipped the carried item and preserves equal or stronger
gear. These verbs do not authorize an Actor. A Package must first pass the same controller and
Sessionless Action Gate as any other gameplay request. Their grants have no money cost because the
selected profile is the source of the entitlement.
actor::reconcile_starter_role_spell_levels(ctx) repairs source-derived training levels only on
complete old curated header shapes. Imported or Operator-tuned rows remain authoritative.
actor::request_cast(ctx, actor_guid, spell_id, target_guid) returns
Result<actor::CastStart, actor::CastRefusal>. Started carries a Cast Handle. Waiting carries
an existing cast's original identity, spell, target, and current due time, even when the new request
names a different spell or target. It never restarts that cast. Resolved means effects dispatched
synchronously, which does not imply a projectile hit. Channeled spells return UnsupportedChannel
until bot requests can retain their lifecycle. Client casts keep their existing channel behavior.
actor::cast_at is the compatibility adapter that discards this distinction. Character Actor
requests require the spell in game_player_spell. Creature and triggered spell-engine entries keep
their existing spellbook exemption.
A scheduled cast ends through on_cast_finished. Its payload carries the caster, target, scheduled
identity, and actor::CastFinish. Packages must match the scheduled identity before updating retained
work. The hook runs after the pending row is removed. An instant cast returns its result directly and
does not fire this hook. on_cast_resolved keeps its existing effect-dispatch contract.
actor::pending_cast reads the current Cast Handle by caster. Refusal kinds are supplied by the
owning Gate. Other preserves a Gate's message when no current caller needs a separate policy.
UnlearnedSpell and NoLineOfSight identify the spellbook and targeted visibility Gates.
actor::cancel_cast_attempt(ctx, caster_guid, scheduled_id) cancels only that scheduled cast.
actor::expire_cast_attempt(ctx, caster_guid, scheduled_id, deadline_micros) also requires the
caller's action deadline to have passed. They return whether they removed the cast and report
Cancelled or Expired through on_cast_finished. A stale identity cannot cancel a replacement.
crate::actor also exposes Character and live-entity reads, nearby entities, partition checks,
quest eligibility, loot entitlement, spell readiness and cast observations. The named re-exports
in module/src/actor.rs are the supported operations. Package authorization, Character setup and
encounter operations are named in module/src/package.rs.
actor::party_facts(ctx, character_guid) reads the Character's local durable party mirror. It names
the leader and every member, with nullable live position, health, and death facts. An absent local
Unit may instead carry a Realm-core-certified map, instance, and locator revision. Pending Transfer,
unknown location, and departed-member fences expose no partition. Enemy facts cover hostile
creatures with current party melee, cast, threat, or control evidence. Hostile Characters are
excluded because PvP party assistance is outside this contract. An absent membership returns
Ok(None).
A Realm-owned Roster Revision orders the complete member list, leader, loot rules, Group kind, and
every Raid Slot. World Shards retain its disband state, so delayed Gateway fanout cannot remove a
newer member, restore older party rules, or recreate a disbanded party.
A membership whose Group row is missing returns MissingGroup. Party facts cover every
Raid member, whatever the Subgroup. FightLimit reports more than 40 members, which only a damaged
mirror holds. It also reports 24 incoming melee or threat rows for one member, one pending cast for
one member, or 24 aggregate enemy GUIDs. For each retained enemy, the read permits 80 threat
sources, 64 control auras, and three effects on a pending spell.
Either failure returns PartyFactsUnavailable, so a Package can hold party control instead of
acting from an arbitrary prefix.
An Operator-tunable value, seeded by the Package and edited without a republish.
crate::package::ensure_config_default(ctx, package, key, default) inserts only when
the row is absent, so a Package calls it from its own ensure path on every run. The Operator edits
with the set_package_config reducer.
A Package fires its own event and reads back a Script Answer with
crate::script_binding::ask(ctx, event, actor_guid, target_guid) -> Option<f64>. The answer is the
first number a bound script returned, in dispatch order; later scripts still run. No answer means the
caller keeps its own fallback, which is what makes a Runtime Script an override rather than a
dependency. Core hook events reach bound scripts on their own; a Package fires only its own Package
Events.
crate::package::fixture exists only in a Module built with debug_reducers. It holds the setup and
observation steps a Package's own debug fixtures need. A release build has no such root.
| operation | does |
|---|---|
apply_damage(ctx, target_guid, amount, attacker_guid) |
deals main-hand damage through the real Core damage pipeline, capped one below the target's health |
remove_live_character(ctx, character_guid) |
removes a live Character from the world as a logout does; on_logout fires and the Character row stays |
require_no_imported_content(ctx) |
refuses when the Shard holds imported content; the temporary weather seed a fresh Module stamps does not count |
top_threat_target(ctx, creature_guid) |
reads the highest-threat living source on the creature's map and instance |
client_cast(ctx, caster_guid, spell_id, target_guid) |
casts through the same Gates a client cast passes |
admit_to_instance(ctx, character_guid, map_id, instance_id, party_id, request_actor) |
stages the party's instance as a dungeon entry leaves it and binds the Character to it; refuses when entry resolves to another instance |
record_completed_transfer(ctx, character_guid, map_id, instance_id) |
records a finished Transfer in Realm-core's character-to-shard index |
declare_next_movement_tick(ctx, delay) |
makes the next creature movement tick fire once, delay from now; refuses unless the catch-all tick is the only movement schedule |
A fixture reads the navigation revision through actor::movement::inputs(ctx, map_id).imported_revision.
Name this root only from a Package file whose first non-blank line is
#![cfg(feature = "debug_reducers")]. The lint refuses it anywhere else, and no exemption clears
it, because a release build would compile that file without the root.
crate::package::test exists only in a Module test build. It holds what a Package's own unit tests
need.
| operation | does |
|---|---|
ask_offline(event, actor, target, scripts) |
runs scripts in order on a fresh Runtime Script Host and returns the Script Answer as script_binding::ask reads it; discards Staged Effects and returns any Script Diagnostic as an error |
ask_artifact_offline(event, actor, target, artifact_json) |
parses a Script Artifact, selects its enabled Event Bindings in dispatch order, and runs them through ask_offline |
EntityView, RuntimeScript |
the event entity and script values ask_offline takes |
read_scanned(rel) |
reads a repository-relative source file; None when its optional directory is not installed |
code_of(src, signature) |
the body after signature, comments removed |
shape_of(src, signature) |
code_of with whitespace collapsed, for an exact comparison |
A source scan pins a chokepoint that no unit test can reach. Prefer a pure function and assert on it wherever one exists.
Name this root only from a Package file whose first non-blank line is #![cfg(test)]. The lint
refuses it anywhere else, and no exemption clears it, because an ordinary build would compile that
file without the root.
A Package declares its own tables with #[table(accessor = pkg_<package>_<name>, ..)], the naming
rule docs/schema.md states. The Package name in the accessor is what keeps two Packages from
colliding. The accessor is also the table name Package Teardown empties, so the build refuses a
Package table that sets its own name.
Core table accessors and row types are named under crate::tables, for example
use crate::tables::{game_world_entity, WorldEntity};. The catalog in
module/src/tables.rs lists them explicitly. Crate-root row aliases remain for Core and schema
checks; a Package uses the catalog path. crate::CHARACTER_OWNED_TABLES is the generated manifest
of character-keyed tables.
A Package may name these five roots. Each operation and type is a named declaration or re-export.
actor hooks package tables script_binding
actor holds Actor operations and observations. actor::movement holds navigation and movement
operations. hooks holds event payloads. package holds Package-owned Character setup, Package
Config, authorization and encounter operations. tables holds table accessors and row types.
script_binding holds Runtime Script dispatch.
package::fixture requires a file gated on debug_reducers. package::test requires a file gated
on #![cfg(test)]. These child paths retain the gates described above.
The marker macros listed above, CHARACTER_OWNED_TABLES, and generated pkg_* modules also remain
available at the crate root. Arbitrary game_* or UpperCamelCase names do not qualify.
The former roots chat, combat, creatures, encounter, faction, gameobject, group,
helpers, items, loot, nav, package_account, package_config, quest, spell, stats,
terrain, transfer, world, and xp are internal. Migrate calls to the named Actor or Package
operation and table imports to crate::tables. There are no compatibility aliases for those
roots in the Package contract.
cargo test -p lyracore-module --test package_api_lint reads every Rust file under
packages/*/src/ and fails when a path
reaches a crate root outside the list above. The failure names the Package, file, line and path. Core
src/ is never linted because the Package API is a promise core makes to Packages, not to itself.
The scanner recognizes crate::, $crate::, and a super:: chain that leaves the Package module.
Raw identifiers have their ordinary meaning, so crate::r#actor names the documented actor
root.
The scanner uses the file path and inline modules to distinguish a crate-root escape from a
Package's own sibling or submodule. A crate-root glob such as use crate::* also fails because it
imports roots that the Package API does not name.
Whole-crate aliases such as use crate as core, use crate::{self as core}, and
extern crate self as core fail at their declaration. Spell every core dependency as
crate::<Package API root> so the lint can check it where it appears. #[path] and a cfg_attr
that supplies path also fail. include! fails because it can add unscanned Rust source;
include_str! and include_bytes! remain available for data. Package modules use Rust's normal
mod.rs, <name>.rs, or <name>/mod.rs layout, which keeps filesystem depth and super depth
equal.
This remains a lexical compatibility check, not Rust name resolution. It does not expand macros or follow a re-export declared in another file. The compiler and the lint run separately; these limits do not turn the Package API into a sandbox.
Comments and string literals are stripped before the scan, so a path quoted in a doc example is inert.
A Package that genuinely needs a path off the surface writes the reason on the line that names it:
crate::realm_core::record_shard(ctx, ..) // package-api: exempt fixture models a completed Realm locator crossingThe marker clears that line and no other. There is no global toggle, and the reason is required — a bare marker does not clear. Every exemption is a gap in this document; raise it with the maintainers so the surface can grow or the Package can move off it.
An exemption cannot enable a whole-crate alias or #[path]. Either spelling can hide dependencies
on other lines or in files the lint cannot locate, so the lint always refuses it.
actor::request_attack returns AttackStart::Armed or AlreadyArmed. The latter keeps the current
melee swing timer. After either result, an in-range Character turns toward the exact admitted target
when Core's facing Gate would block its swing, unless an active movement leg still owns its position
and facing. Both results mean an engagement was accepted; neither proves a hit. The existing
actor::attack and client operations retain their re-arm behavior.
actor::request_accept_quest and request_turn_in_quest complete synchronously. They return
Result<(), ActionRefusal>, with the same core Gates and effects as the client operations. They
have no waiting, cancellation, or expiry phase. Attack requests also return ActionRefusal.
Its kind identifies actionable Gates and detail preserves the client message. Other is an
opaque refusal; Package policy must not classify its text.
actor::movement::route_step accepts the same arguments as nav_step and returns a RouteStep. Its status
is Complete, Partial, Blocked, or Direct when navigation is disabled. Complete and Partial
describe the planned route. first_waypoint, expansions, and clipping retain search and commit
Gate evidence. endpoint == from means no movement was approved, even if a complete path was
planned. A blocked search holds position. Creature callers retain nav_step and its existing
collision-gated fallback.
actor::movement::coverage_generation(ctx, map_id) returns the active complete coverage generation when its
Gate is enabled, or None. Include it with map and instance in deferred destination keys. A changed
generation invalidates a retained route decision; None preserves unknown coverage.
coverage is Unknown unless every consulted navigation cell matches the active complete
manifest. VerifiedCells names that generation and the number of checked cells. It proves only
those cells' derivation, not world coverage or the presence of imported terrain and client
geometry. A Package must measure actual position on later observations to establish advancement
or arrival; the proposed endpoint cannot establish either.
actor::sessionless_action_gate(ctx, character_guid) checks current Account Claim and Fence ownership, Character availability, and World Session status before Package gameplay. It permits a missing live entity so Legacy can restore a body. Group admission also requires a live entity and current controller consent.
actor::sessionless_movement_gate(ctx, character_guid) adds current controller consent and a live,
living body to that authority check. A pending cast or movement-suppressing crowd control refuses
movement. Call it before issuing a movement leg, including continuation between decision turns. The Gate reads
current state and does not cancel casts or change position.
actor::stop_movement(ctx, actor_guid) stops the current leg where the client renders the Actor
and writes the resulting position. If the body is absent, it removes the stale leg. The caller
must check its movement ownership before stopping a leg.
actor::restore_sessionless_body(ctx, character_guid) returns an existing body or rebuilds a
missing one from an available Character row. It preserves released ghost state and removes the
old movement leg when it rebuilds. With no body, an absent or Transfer-fenced Character returns
None. The Package checks controller consent and pending Transfer Intent policy before calling.
actor::movement::emit_creature_path(ctx, mover, points, run) sends the mover along a checked Route
Path and writes the row. A mover on a leg starts the path from the point that
actor::stop_movement would stop it at, so a renewal between two advance firings does not move the
client back.
actor::area_trigger_route(ctx, trigger_id) reads one exact imported AreaTrigger source volume and
target map. It exposes the source center and containment rule for Candidate movement while keeping
the landing coordinates private. actor::enter_sessionless_areatrigger rechecks the current body,
volume, exact existing dungeon instance lease, and a Realm-certified party member in the expected
partition before it applies the imported landing and writes one Transfer Intent. A Refusal leaves
the Character, instance binding, and intent unchanged.
actor::movement::LEG_MAX_EXPANSIONS is the expansion cap used by actor::movement::route_step. A Package can reserve that work before selecting movement.
A Runtime Script is Lua supplied from outside the core rather than compiled into it. It reaches a Shard only through a Package's Script Artifact; there is no upload path. Its name lets a diagnostic identify it.
Package Events. A Package Event runs the same dispatch a core hook event runs, so a Package exposes one of its own decisions to a Runtime Script without a new core seam. A Package may only bind events it fires. The artifact parser enforces this against the artifact's own Package identity.
Event Binding and order. A script binds to a name from the Module's hook catalogue or to a Package Event of the shipping Package. The author-time build refuses anything else. Several scripts may bind to one event. Lower priority runs first and the script identifier breaks a tie, so every Shard runs one plan in one order.
Script Answer. The first number returned in dispatch order is the answer. Later scripts still run and still stage what they stage. With no answer (nothing bound, nothing returning a number, or every script failing) the caller keeps its own fallback. That is what makes a Runtime Script an override rather than a dependency.
The Host. The Runtime Script Host holds one compiler cache, gives each Invocation a fresh environment and a Fuel Budget, and contains every failure. An Invocation starts from an environment that holds only the allowlisted standard library, the event with its Entity Handles, and the Host Operations. It ends by committing its Staged Effects or by producing a Script Diagnostic.
Fuel Budget. Running out of fuel is a failure, so a script that overruns changes nothing.
Entity Handles. A handle carries the identity the Host acts on and the curated fields the script may read. It carries no guid and no row, so a script can neither forge one nor name an entity the Host did not resolve for that Invocation.
Host Operations. Today these are heal, send_chat and grant_xp. Each takes an Entity Handle
and records a Staged Effect. A misuse is refused with a Script Diagnostic that names the call and the
fault.
Staged Effects. A successful Invocation commits its Staged Effects through core operations. Any failure discards all of them.
Script Diagnostics. A Script Diagnostic records the Runtime Script, the event, the failure kind (syntax, runtime or fuel) and a truncated message.
Script Artifacts. A Script Artifact holds the Package identity, the source revision, and one
whole row per script: identifier, name, Event Binding, priority, enabled state and Lua. A Runtime
Script has no base import, so the Package owns the whole row, and two Packages meeting on one row is
a collision rather than a merge. Script Artifacts and Package Deltas both live in
packages/<name>/data/.generated/, told apart by a top-level kind.
Event Bindings. Each source file binds one named function to an event, for example
events.player.onLogin(welcome). The function takes the event payload and may return a numeric
Script Answer. TypeScript and Lua use the same Event Binding names. TypeScript declarations and
Lua editor definitions come from the catalogue in datascripts/runtime-scripts/events.json.
Login and level-up guarantee event.player; level-up carries its attained level in newLevel.
Other fields follow each event's declaration. Entity Handles keep their existing constraints.
Script Identities. The toolchain records numeric IDs in the Package-root script-ids.json,
keyed by source-file stem. Commit it with the sources and Script Artifact. Existing artifacts and
legacy @event/@id directives supply migration IDs; the compiler refuses a conflict. Renaming a function
keeps its identity. A source-file rename creates a new identity. Deleted entries remain reserved.
Migration reads every prior Script Artifact in data/.generated/, including noncanonical
filenames. For a new identity, the toolchain resolves a hash collision by choosing an unused ID.
It reserves recorded and legacy IDs across the enabled Package Inventory before allocating new
ones. A conflict between previously recorded IDs names both owners and requires reconciling their
published identities; the compiler never silently renumbers them.
Runtime Script Toolchain. Bun, typescript-to-lua, the Lua parser, event catalogue, generated
declarations and emitter live in datascripts/runtime-scripts/. They run only at author time.
The emitted Lua captures the declared function and calls it with the event in one Invocation.
Top-level source runs on each Invocation before the handler. Any Host Operations there share the
handler's Staged Effects and failure boundary.
An Operator can install the prebuilt Script Artifact. See
Runtime Script authoring for examples and migration.
Version 2 includes this authoring form. The collection's api-v1 tag remains available to older
checkouts whose toolchain requires Script Directives.
Each Import Family that lets a Package insert rows owns one Package Identifier Range.
crates/lyracore-package-delta/src/ids.rs holds the numbers. A band's floor sits two decimal orders
above the highest identifier a real client holds for its tables, clear of every reserved band. An
apply clears the whole band before it writes.
| Range | Family | Band | Checked against |
|---|---|---|---|
| Package Script Range | script | 100,000 to 999,999 | game_script identifier |
| Package Spell Range | spell | 6,000,000 to 6,999,999 | spell id, for game_spell and game_spell_effect |
| Package Item Range | items | 7,000,000 to 7,999,999 | game_item_template.entry |
| Package Quest Range | quest | 8,000,000 to 8,999,999 | quest_entry |
| Package Loot Range | loot | 9,000,000 to 9,999,999 | the loot row's own identifier |
| Package Cast Range | casts | 10,000,000 to 10,999,999 | game_creature_spell.id |
| Package Trainer Range | trainers | 11,000,000 to 11,999,999 | game_trainer_spell.id |
| Package Gossip Range | gossip | 12,000,000 to 12,999,999 | each insertable gossip table's own key |
| Package Globals Range | globals | 13,000,000 to 13,999,999 | the surrogate key of three tables |
| Package Spell Metadata Range | spellmeta | 14,000,000 to 14,999,999 | game_spell_learn.id |
| Package Creature Range | creatures | 15,000,000 to 15,999,999 | template entry and spawn spawn_id |
| Package Gameobject Range | gameobjects | 16,000,000 to 16,999,999 | template entry, trap entry and spawn spawn_id |
| Package EventAI Range | Creature-AI | 17,000,000 to 17,999,999 | three catalogue tables' id |
Each band after the spell band sits one whole decade above the one before it, so the millions column says which family invented a row.
- Script. No client and no import holds a Runtime Script identifier, so the band has no real data
to clear and sits below every reserved band rather than above one. It is the whole of
game_scriptby construction, which makes a script apply a total reconciliation. - Spell. Two decimal orders above the highest real client spell and above every reserved band, so an inserted spell never collides with imported or fixture data. It is the worked example the other bands follow.
- Quest.
game_quest_templateand every child table (game_quest_textand the rest) are Package-owned exactly when their quest is, so one band covers the whole family. - Loot. No loot table's owning entity (a creature, a gameobject or a zone) is ever Package-invented, so the band checks a loot row's own identifier. The four loot tables (pickpocket, gameobject or chest, skinning, fishing) share the band; each has its own primary-key space, so they cannot collide.
- Casts. The loot shape: the owning creature is never Package-invented.
game_creature_casthas no band. Its key names a creature template, which no Package may invent, so every insert on it is refused. - Trainers. The loot shape. The curated trainer overrides at 5,200,000
(
CURATED_TRAINER_ID_BASE) are a reserved band this range clears, not a Package range. - Gossip. One band covers
game_npc_text,game_npc_text_slot,game_gossip_option,game_gossip_menu_profileandgame_gossip_menu_profile_option.game_gossip_menuhas no band: its key names a creature template, so every insert on it is refused. - Globals. Covers
game_graveyard_zone,game_createinfo_spellandgame_createinfo_action. The other four tables have no band because no Package may invent their keys:game_class_level_stats,game_level_statsandgame_start_positionkey on a race, class and level the client fixes, andgame_areatrigger_teleportkeys on anAreaTrigger.dbctrigger id. - Spell metadata.
game_spell_chainandgame_spell_proc_eventkey on a spell identifier, so an insert there takes the Package Spell Range. A metadata row cannot outlive thegame_spellrow it describes. - Creatures. A creature spawn's durable guid packs the template entry and the spawn identifier
into 24-bit fields, so the whole band must fit inside one. The seeded creature fixtures at 51,000
to 51,999 are Fixture-Reserved Identifiers no Package may tune.
game_creature_waypointis not claimable: it names its creature by spawn guid and carries no map, so a Spatial Claim on it could not be routed. - Gameobjects. Template
entryand trapentryshare one identifier space on purpose: a trap row describes the template of the same entry. The two gameobject pool tables are not claimable, because no base import writes either, so a claim on one would have no family reload to replay after. - EventAI. Covers
game_creature_ai_broadcast_text.id,game_creature_ai_summon.idandgame_quest_event_requirement.id. The family's scripted definitions are not claimable: a definition carries a creature's whole rule set as a nested payload, which no claimed column can state. Reaching a creature's rules from a Package remains a named gap.