KeyEngine is a lightweight, plugin-driven runtime host. Systems are the main execution unit: the engine discovers system types, builds their metadata, resolves their instances through dependency injection, and invokes lifecycle, command, and event methods through cached invokers.
The high-level flow is:
EngineBuilder
-> EngineOptions
-> Engine.Initialize
-> load and configure plugins
-> scan registered systems and assemblies
-> register commands and event listeners
-> build the service resolver
-> invoke startup methods
-> Engine.Tick
-> update input sources and aggregate input state
-> update timers
-> invoke update methods
-> invoke due fixed updates
-> Engine.Shutdown
-> invoke shutdown methods
EngineBuilder collects assemblies, scheduler options, application metadata,
and the plugin directory. Build creates the engine and its core services.
Initialize performs discovery and registration, builds the service resolver,
invokes startup methods, starts the scheduler, and transitions the engine to
running. Tick advances one frame. Stop requests graceful shutdown by moving
the engine into its shutting-down state. Shutdown invokes shutdown methods and
marks the engine stopped.
Lifecycle methods are declared with OnStart, OnUpdate, OnFixedUpdate, and
OnShutdown. Supported methods return void and accept either no parameters or
one UpdateContext parameter.
Disk-based plugins implement IPlugin and provide an embedded plugin.json
manifest. The plugin loader discovers assemblies in the configured plugin
directory, reads manifests, constructs plugin contexts, and invokes each
plugin's Configure method.
IPluginBuilder is the plugin registration boundary. Plugins can register
systems, configuration objects, and singleton or transient services. Plugin
contexts provide manifest information and plugin-specific configuration, data,
cache, and log locations.
Plugin loading currently occurs for the lifetime of the process. Hot reload, unloading, and version constraints are not implemented. Required dependencies and optional load-order hints are validated and sorted during loading.
Systems implement IEngineSystem. TypeScanner examines registered system
types for lifecycle, command, and event-listener attributes. It validates
supported signatures and produces cached metadata and method invokers.
The engine scans explicitly registered application assemblies and systems declared by plugins. Discovery occurs during initialization, not on every frame.
KeyEngine includes a small constructor-injection container. Services are registered as singleton instances, singleton types, or transient types. The resolver chooses a public constructor and recursively resolves its parameters.
Engine systems are resolved through the same service infrastructure, which ensures registered singleton systems are shared by lifecycle, command, and event dispatch. Core services include the engine, event bus, command manager, timer manager, resource manager, serializer, network manager, input manager, and filesystem implementation.
The container is intentionally limited and is not intended to reproduce a full-featured external DI framework.
Commands are methods marked with CommandAttribute. Discovery records names,
aliases, descriptions, usage, categories, and signatures. The current text
command contract supports methods with no parameters or one string parameter.
CommandManager resolves overloads and invokes methods on DI-resolved systems.
Command invocation publishes cancellable before-events and completion events.
Events implement IEvent. Listener methods use EventListenerAttribute and
accept one event parameter. EventBus dispatches listeners by exact event type,
priority, and explicit order. For CancellableEvent instances, listeners with
IgnoreCancelled are skipped after cancellation.
Commands and events currently use synchronous dispatch.
- Scheduler: tracks frame time, elapsed time, and fixed-update accumulation.
- Timers: tracks one-shot and repeating timers advanced by engine ticks.
- Configuration: caches typed configuration objects and persists them through
IFileSystemandISerializer. - Resources: maps resource types to loaders and caches loaded resources by
type and provider-neutral
ResourceLocation. - Serialization: converts values to and from text through
ISerializer; the default implementation uses indented JSON. - Networking: manages synchronous TCP clients, servers, and connections using built-in .NET socket APIs.
- Parameters: stores typed runtime settings, publishes change events, and can explicitly save or replace them from a JSON file. Hosts decide whether and when persistence occurs.
- Runtime logs: retain a bounded in-memory activity feed without replacing the existing console logging path.
- Web: provides a small synchronous HTTP server with route registration, simple path parameters, route metadata, and static-file fallback. Explicit API routes take precedence, and canonical path checks confine files to their configured static root.
- Input: aggregates snapshot state from application-provided keyboard and pointer sources once per frame without depending on a windowing platform.
- Numerics: contains vector types, interpolation helpers, and two-dimensional graph data.
- Drawing: currently provides an immutable color value and ANSI text color translation; it is not a rendering system.
EngineDiagnostics is a read-only façade over authoritative engine state. It
reports lifecycle, scheduler, registration, timer, and plugin information
without exposing PluginManager to consumers such as the Console plugin.
Diagnostics should remain derived from subsystem state. KeyEngine should avoid creating a second mutable diagnostics registry that can drift from the runtime.
The sample server composes KeyEngine.Web with engine diagnostics, parameters,
serialization, and runtime logs. Its admin API exposes read-only runtime data
and protects mutation routes with an optional X-KeyEngine-Admin-Token value
from admin.token. This is a local/development foundation, not a complete
authentication or authorization system.
KeyEngine.AdminClient contains reusable HTTP DTOs and client operations and
does not reference server internals. KeyEngine.AdminApp is a console shell,
while KeyEngine.AdminDashboard is an early Avalonia desktop shell. Both use
AdminClient as the API boundary. The dashboard can inspect status, plugins,
parameters, logs, and routes; mutate and persist parameters; clear logs; and
configure the sample static website root. Tokens and server URLs are not
persisted by the dashboard.
TestApp optionally loads parameters.json beside its executable before engine
initialization. This allows startup-only sample settings such as
web.static.root and admin.token to survive restarts when explicitly saved.
UI, Audio, Windowing, Rendering, and Physics are future optional packages or plugins, not required core dependencies. Integrations should feed existing boundaries, such as input sources and resource loaders, without coupling the core runtime to a specific platform stack.
A future game editor may be built as a KeyEngine application rather than as a core runtime feature. It would compose editor plugins, game-framework plugins, runtime plugins, and project plugins through the same model used by other KeyEngine hosts.
An exported game would also be a KeyEngine application. Its build would omit editor-only plugins and retain only the runtime, game-framework, and project plugins required by the game. This keeps editor execution and exported runtime execution on the same lifecycle, dependency-injection, and plugin foundations.
This model is future planning only. User-created game plugins should eventually have clear packaging and distribution conventions, while UI, Audio, Windowing, Rendering, Physics, and game-specific systems remain outside the core package.
The xUnit contract suite exercises lifecycle transitions, event cancellation, resources, input, timers, plugin ordering and diagnostics, parameters, web/admin routes, static-file confinement, and sample startup loading. It is a focused alpha baseline rather than comprehensive production-readiness coverage.
- Prefer explicit registration and APIs over hidden behavior.
- Keep the engine responsible for sequencing, not every subsystem detail.
- Cache reflection metadata during initialization.
- Resolve one system instance consistently across dispatch paths.
- Keep public contracts small and implementation types internal where possible.
- Preserve synchronous, deterministic behavior until concurrency is designed explicitly.
- Add abstractions only at real ownership or integration boundaries.
- Favor clear failure messages during startup over deferred runtime failures.