I’m happy to present yet the other milestone achieved in Penal Engineer.

Past 2 months were dedicated to an extensible framework of input handling: mouse and keyboard.

Apart from that (and to demonstrate the power of it), I made a whole new framework for sprites outlines (contours) dynamic generation and making them drawn on mouse hover and selection of entity.

Architecture revamp

Composition

I was building Penal Engineer as a modular project from the very beginning. However, I discovered that quite a lot of user interaction was handled by an uber-module (.exe), while a role of .dll-s libraries got to stay serving ECS and some gameplay. This used to block modders from potential expansion of the ecosystem, not letting them replace parts of the game which could be granularized even more.

What I also made bad before was relying upon Microsoft DI instead of writing an own dependency layer which could serve the purpose of contract enforcing much better.

All that old architecture got finally revamped.

Module composition as of v0.2
Module composition as of v0.3

The main architectural change visible in v0.3 is the extraction of Contracts as the implementation-neutral base layer, followed by the decomposition of input and interaction behavior into independently replaceable modules.

Lifecycle

In the previous version, the plugin lifecycle wasn’t thought the best, unfortunately. I used to believe that it would be enough to have the plugins ordered in a dependency graph. But I failed to realize earlier that separate phases of plugin bootstrapping are really necessary to ensure configuration schema set freeze, custom services registration freeze and so on.

Thereof I have now introduced a new plugin lifecycle, with explicit guaranteed at each phase.

Area v0.2 v0.3 Why it matters
Lifecycle contract A plugin implemented one method: Initialize(ILoggerFactory, World). It returned a bool, although the host did not act on that result. A plugin implements Initialize, RegisterSchemas, RegisterServices, Load, and OnGameLoadContent. Failures surface through exceptions instead of an ignored success flag. Each kind of setup now has a clearly defined and enforceable place.
Execution model The host constructed and initialized each plugin individually. One plugin completed all its work before the next plugin began. The host keeps an ordered collection of bootstrappers and completes one phase for all plugins before beginning the next phase. A plugin can no longer gain accidental privileges simply because it was initialized first.
API boundary Plugins received the concrete Core World and commonly accessed concrete implementations through World.Services. Plugins receive the implementation-neutral IGameContext, whose contracts live in the separate Contracts assembly. Plugins can depend on stable public APIs rather than the internal shape of Core or the game host.
Configuration schemas A plugin could register a schema and immediately read configuration inside Initialize. That first read could load the configuration before later plugins had registered their schemas. Every plugin runs RegisterSchemas first; the host then freezes path-resolver and configuration-schema registration. Configuration access is unavailable until that complete schema set has been collected. Enabling another configured plugin no longer makes configuration validity depend on bootstrap order.
Plugin services The host built a Microsoft DI container before constructing the world. Plugins had no dedicated phase for publishing shared services. Plugins publish typed instances or lazy singleton factories during RegisterServices. Resolution is blocked until every plugin has registered and the service catalog has been frozen. Replaceable facilities such as keyboard handling, pointer selection, atlas building, and outline generation can be provided behind shared contracts.
Service safety Availability and conflicts largely depended on the host’s DI setup and initialization order. Duplicate providers, invalid implementation types, premature resolution, late registration, null factories, and circular singleton construction receive explicit diagnostics. Integration mistakes fail predictably instead of silently selecting whichever implementation happened to load first.
Runtime setup Schema registration, configuration access, prefab creation, hook subscriptions, system installation, and content registration were mixed together in Initialize. Load runs only after schemas and services are frozen. Plugins can safely read configuration, resolve services, and register prefabs, hooks, semantic actions, bindings, or update callbacks there. Mod initialization becomes easier to reason about and less sensitive to incidental call order.
Graphics and content There was no plugin content callback; the host performed MonoGame content and HUD setup directly. Presentation-oriented extensions had to be wired into the host or initialized too early. OnGameLoadContent runs after fonts, rendering facilities, the texture atlas, the demo world, and other host content resources are ready. A debug overlay or context-menu presenter can create graphics resources without becoming part of Game.
Per-frame behavior Recurring plugin behavior generally required adding an ECS system or modifying the host update loop. Plugins can register non-ECS update callbacks with explicit priorities and unregistration tokens. The host dispatches these callbacks before the ECS world update. Input capture, target selection, semantic-action dispatch, and UI handling can run in a deterministic order without being disguised as world simulation systems.
Extensibility status The bootstrapper was effectively a one-shot content initializer. No later lifecycle participation or unloading model existed. The new lifecycle provides clearer ownership boundaries and several token-based runtime registries, establishing groundwork for eventual unloading. Dynamic assembly discovery, dependency-graph validation, automatic cleanup, and a shutdown/unload phase are still future work. v0.3 is a substantial plugin-platform foundation, but not yet a complete hot-loadable mod runtime.

Comparison between v0.2 and v0.3

Accomplishments

For players

  • More consistent mouse and keyboard controls. Mouse input now distinguishes UI, entities, and empty world space, while keyboard input supports presses, releases, held keys, and modifier combinations. This provides reliable interactions without UI clicks accidentally selecting something in the world underneath.
  • Configurable keybindings. Actions such as camera movement, zooming, changing floors, even cycling through hovered objects, as well as toggling debug overlay and exiting are no longer hard-wired directly into the game loop. They made out as so-called semantic actions, and their keyboard bindings can be overridden through configuration. On-screen hints display the effective keys rather than assuming the defaults. This flexibility is what many modern games lack. Mine doesn’t.
  • Clear highlighting of interactable objects. The game now generates sprite contours automatically and draws a light-blue outline around the object currently under consideration beneath the mouse. When several objects overlap, Tab and Shift+Tab (default key bindings which can be overridden by a custom configuration, as outlined in the previous point) cycle through them, with a small pointer-side hint showing both the actual configured controls and the current object number.
  • Persistent entity selection and camera following. Left-clicking an interactable entity selects it, gives it a distinct green outline, and makes the camera follow its position. Clicking empty world space clears both the selection and the follow target, while simply moving the pointer does not disturb the selected entity.
  • Entity context menus. Characters can now expose right-click menus whose available commands belong to that specific entity; prisoners and guards currently demonstrate this with a sample Move to... entry. (The command is presently a framework placeholder rather than completed movement or pathfinding, but opening, replacing, activating, and dismissing menus — including with Escape — is fully handled.)

For modders

  • Replaceable input-handling modules. Mouse observation, pointer-target selection, pointer-event dispatch, keyboard recognition, and camera controls have been separated into plugin-style services with explicit update ordering. A mod can consume the shared pointer snapshot, register new behavior, or replace a default provider without depending on the MonoGame host or rewriting unrelated input systems.
  • Declarative mouse interaction for ECS entities. PointerTarget component defines whether and where an entity can be hit, while MouseInteraction maps left-click, right-click, pointer-enter, and pointer-leave events to semantic action IDs. This lets a mod give a door an Open action or a machine an Inspect action through prefab/component data, while empty-space actions are supported without fabricating invisible ECS entities.
  • Extensible semantic actions and keybindings. Operations now have stable namespaced IDs, typed contexts, discovery metadata, lifecycle-aware registration, and explicit Handled, Ignored, or Failed results, allowing the same action to be invoked from a key, mouse binding, or context-menu item. Mods can provide default bindings in namespaced keyboard contexts, including pressed, released, and continuously held chords, while player overrides remain dormant rather than being discarded if the corresponding mod is temporarily absent.
  • Runtime-generated sprite contours. The new replaceable outline generator can derive inset or outset contours from any RGBA sprite, with configurable color, thickness, and alpha threshold. A content mod can therefore obtain hover and selection artwork automatically — or supply hand-drawn alternatives — and versioned texture dependencies ensure that only affected generated atlas entries need rebuilding.
  • Composable entity context menus. The new ContextMenu ECS component stores ordered data-only entries containing stable item IDs, captions, semantic action IDs, and enabled states; mods can add, remove, or replace those entries at runtime. An action receives both the owning entity and selected item ID, making additions such as Inspect, Assign, Arrest, or Repair possible without embedding callbacks or UI objects inside prefab data.
  • Safer extensible UI interaction. Context-menu dismissal uses the real pointer snapshot and explicit click consumption, with no invisible full-screen backdrops or temporary world entities. As a result, a mod-provided popup can close on an outside click without that same click leaking through and selecting an entity or triggering an empty-world action.

Demo video

Hovering entities: dynamic sprite contours, walls corners & TAB to cycle, and context menu

What’s next?

I’m now facing the funniest part of the development: to make entities live.

I’m going to introduce the actual movement by Move to... context menu item call first. I’ll build the necessary ECS systems and game clock integrator.

The initial endeavour is to have an entity moving directly, ignoring obstacles and walls. Pathfinding is a later step.

Stay tuned — now you can do so with RSS! (Top-right corner of the page is up to serve your RSS reader.)