You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Batch / Group: Batch 8.2 — Gameplay services, Group A
Runbook spec:docs/src/runbook/phases/phase-8.md (committed with the roadmap)
Summary
Add a generic, type-erased engine event bus that SDK code can subscribe to for custom (game-defined) event types, with FFI publish/subscribe exports, unifying the pattern that today is reinvented per subsystem. Closes #547.
Architecture Context
Layer: Layer 3 (Services, ecs/ — the bus lives alongside other ECS resources) building on the existing Layer 1 core/event/ primitives; Layer 5 (FFI) for the subscription surface. Modules/types touched:
goud_engine/src/core/event/ — reuse EventQueue, EventReader, EventWriter, Events<E> (queue.rs, reader.rs, writer.rs, resource.rs) as the underlying double-buffered storage; do not reimplement double-buffering.
goud_engine/src/ffi/event_bus.rs (new) — a type-erased publish/subscribe surface: register a named/typed channel, publish raw bytes, drain/poll subscribed events.
goud_engine/src/ecs/ — bus registration as a world resource so systems and FFI share the same event storage.
Events cross the FFI boundary as raw byte payloads plus a type/tag identifier (mirroring how component_ops already crosses typed Rust data across FFI as bytes + type_id_hash — see .agents/rules/component-ops.md); it does not know concrete SDK-side types at compile time.
Pattern to follow:goud_engine/src/core/event/mod.rs design (Event/EventQueue<E>/EventReader<E>/EventWriter<E>/Events<E>) is the correct underlying primitive — reuse it. Do NOT extend the bespoke per-subsystem patterns already in the tree (ffi/ui/events.rs's callback_registry keyed by usize, or ffi/physics/physics2d_events.rs's CollisionCallback + deterministic pull API) — this issue replaces the need for new instances of that pattern going forward, though it does not need to migrate the existing three call sites.
Scope
Type-erased publish/subscribe core: game code (via FFI) registers a named event channel (or type-id hash, mirroring component_ops's type_id_hash convention), publishes raw byte payloads, and other code drains them per-frame.
FFI exports: goud_event_bus_publish(context, channel_id, data_ptr, len), goud_event_bus_drain(context, channel_id) -> count + accessor, or a callback-registration variant — decide one consistent model (poll-based, matching Events<E>'s existing double-buffer semantics, is preferred over callbacks per the codebase's general avoidance of callback re-entrancy into engine state).
Wire the bus as a per-GoudContext resource so multiple contexts don't cross-publish events.
Tests: spec test + unit tests for publish/drain ordering, double-buffer semantics (published this frame, readable next frame per existing Events<E> contract), and multi-subscriber fan-out.
Docs/rules updates: mdBook page + a note in .agents/rules/ecs-patterns.md establishing the bus as the sanctioned path for new cross-boundary event needs, so future subsystems don't add a fourth bespoke callback pattern.
Acceptance Criteria
A custom event type published from one SDK call is observable via drain/poll from another FFI call within the same frame-boundary contract as Events<E> (published now, readable starting next read cycle).
FFI event-bus exports have wrappers in all 10 SDKs and pass python codegen/validate_coverage.py.
The engine already has a real, generic, double-buffered typed event system at goud_engine/src/core/event/ (queue.rs, reader.rs, writer.rs, resource.rs, plus traits.rs for the Event marker trait) — any Send + Sync + 'static type auto-implements Event per the doc comment in core/event/mod.rs. core/events/ (plural, separate directory: window.rs, frame.rs, app.rs) uses this to define built-in engine lifecycle events (AppStarted, WindowResized, FrameStarted, etc.), entirely Rust-side.
None of this generic machinery is reachable from FFI. Instead, three independent, mutually-inconsistent bridging patterns already exist in the tree, confirming the "reinvented per subsystem" framing: ffi/animation/events.rs polls crate::core::event::Events<AnimationEventFired> directly with a payload-type discriminant (PAYLOAD_NONE/PAYLOAD_INT/PAYLOAD_FLOAT constants at the top of the file); ffi/ui/events.rs instead uses a hand-rolled callback_registry() (Mutex<HashMap<usize, CallbackRegistration>>) with function-pointer callbacks; ffi/physics/physics2d_events.rs uses yet a third shape — a CollisionCallback plus a separate deterministic "capture during step, pull after" API (capture_step_collision_events, collision_event_at, collision_event_count). No generic named/typed channel exists that arbitrary SDK-side game code can publish or subscribe to for its own custom event types.
Decide the publish/subscribe transport shape carefully: the codebase already leans toward poll/drain over callbacks for anything touching engine state re-entrancy (physics uses "capture then pull", not a live callback into the physics step) — prefer a poll-based FFI model here too rather than the ffi/ui/events.rs callback style, to avoid re-entrancy into the same per-context lock a publish call might be holding.
Parent
eng2-p8-capabilities)docs/src/runbook/phases/phase-8.md(committed with the roadmap)Summary
Add a generic, type-erased engine event bus that SDK code can subscribe to for custom (game-defined) event types, with FFI publish/subscribe exports, unifying the pattern that today is reinvented per subsystem. Closes #547.
Architecture Context
Layer: Layer 3 (Services,
ecs/— the bus lives alongside other ECS resources) building on the existing Layer 1core/event/primitives; Layer 5 (FFI) for the subscription surface.Modules/types touched:
goud_engine/src/core/event/— reuseEventQueue,EventReader,EventWriter,Events<E>(queue.rs,reader.rs,writer.rs,resource.rs) as the underlying double-buffered storage; do not reimplement double-buffering.goud_engine/src/ffi/event_bus.rs(new) — a type-erased publish/subscribe surface: register a named/typed channel, publish raw bytes, drain/poll subscribed events.goud_engine/src/ecs/— bus registration as a world resource so systems and FFI share the same event storage.Boundary constraints (only those that apply):
#[no_mangle] extern "C",#[repr(C)], null checks,// SAFETY:comments.component_opsalready crosses typed Rust data across FFI as bytes +type_id_hash— see.agents/rules/component-ops.md); it does not know concrete SDK-side types at compile time.Pattern to follow:
goud_engine/src/core/event/mod.rsdesign (Event/EventQueue<E>/EventReader<E>/EventWriter<E>/Events<E>) is the correct underlying primitive — reuse it. Do NOT extend the bespoke per-subsystem patterns already in the tree (ffi/ui/events.rs'scallback_registrykeyed byusize, orffi/physics/physics2d_events.rs'sCollisionCallback+ deterministic pull API) — this issue replaces the need for new instances of that pattern going forward, though it does not need to migrate the existing three call sites.Scope
component_ops'stype_id_hashconvention), publishes raw byte payloads, and other code drains them per-frame.goud_event_bus_publish(context, channel_id, data_ptr, len),goud_event_bus_drain(context, channel_id) -> count+ accessor, or a callback-registration variant — decide one consistent model (poll-based, matchingEvents<E>'s existing double-buffer semantics, is preferred over callbacks per the codebase's general avoidance of callback re-entrancy into engine state).GoudContextresource so multiple contexts don't cross-publish events.Events<E>contract), and multi-subscriber fan-out..agents/rules/ecs-patterns.mdestablishing the bus as the sanctioned path for new cross-boundary event needs, so future subsystems don't add a fourth bespoke callback pattern.Acceptance Criteria
Events<E>(published now, readable starting next read cycle).python codegen/validate_coverage.py.cargo check && cargo fmt --all -- --check && cargo clippy -- -D warningsclean;cargo testgreen;./codegen.sh && git diff --exit-code(drift gate)Breaking Change & Throne Follow-up
None — additive/internal (new FFI exports only; existing per-subsystem event bridges in
ffi/ui/events.rs,ffi/animation/events.rs,ffi/physics/physics2d_events.rsare untouched).Blocked By
None.
Files Likely Touched
goud_engine/src/ffi/event_bus.rs, possiblygoud_engine/src/ecs/resource/event_bus.rssdks/*/for the new event-bus FFI exportsgoud_engine/src/ffi/mod.rs(register module),codegen/goud_sdk.schema.jsonAgent Notes
goud_engine/src/core/event/(queue.rs,reader.rs,writer.rs,resource.rs, plustraits.rsfor theEventmarker trait) — anySend + Sync + 'statictype auto-implementsEventper the doc comment incore/event/mod.rs.core/events/(plural, separate directory:window.rs,frame.rs,app.rs) uses this to define built-in engine lifecycle events (AppStarted,WindowResized,FrameStarted, etc.), entirely Rust-side.ffi/animation/events.rspollscrate::core::event::Events<AnimationEventFired>directly with a payload-type discriminant (PAYLOAD_NONE/PAYLOAD_INT/PAYLOAD_FLOATconstants at the top of the file);ffi/ui/events.rsinstead uses a hand-rolledcallback_registry()(Mutex<HashMap<usize, CallbackRegistration>>) with function-pointer callbacks;ffi/physics/physics2d_events.rsuses yet a third shape — aCollisionCallbackplus a separate deterministic "capture during step, pull after" API (capture_step_collision_events,collision_event_at,collision_event_count). No generic named/typed channel exists that arbitrary SDK-side game code can publish or subscribe to for its own custom event types.ffi/ui/events.rscallback style, to avoid re-entrancy into the same per-context lock a publish call might be holding.Verification