Skip to content

feat(plugins): Add a plugin hook framework for observer overlays - #585

Draft
doopey655 wants to merge 1 commit into
GeneralsOnlineDevelopmentTeam:mainfrom
nathan-soul:pr/plugin-framework
Draft

feat(plugins): Add a plugin hook framework for observer overlays#585
doopey655 wants to merge 1 commit into
GeneralsOnlineDevelopmentTeam:mainfrom
nathan-soul:pr/plugin-framework

Conversation

@doopey655

Copy link
Copy Markdown

Description

Adds a host-side plugin loader and a C ABI so an observer overlay can live outside the engine as a
DLL. The engine loads every *.goplugin.dll under plugins\ at startup; each plugin opts into the
hook categories it wants and receives events through an ABI that never exposes engine types.

With no plugin present nothing changes. Every call site is a guarded call whose condition is an
inlined !empty() on a static vector.

What it adds

  • PluginABI.h — the host/plugin contract. Pure C (POD structs, function pointers,
    primitives), no STL and no engine types, so a plugin compiles against this one header. Versioned
    (GO_PLUGIN_ABI_VERSION 5) with a structSize guard; a mismatch fails the load rather than risk
    misreading the table. Exports GO_Plugin_GetInfo / _Initialize / _Shutdown, plus an optional
    _Tick.
  • GOPluginManager — loads one folder per plugin, validates the ABI version, dispatches events.
    A plugin that fails to load is logged and skipped, never fatal.
  • Gameplay hooks — unit and upgrade queued / cancelled / completed, building destroyed, special
    power triggered, object damaged and healed. Payloads carry template names as strings and object
    ids as uint32, never engine pointers.
  • Render hooks — a per-frame overlay draw, plus raw key and mouse input delivered as a side
    channel that never consumes or gates the engine's own input handling.
  • Host API — 2D text / rect / line / icon primitives, screen size, world-to-screen, object
    screen bounds, logic frame and FPS, the player roster with colours, general-power recharge state,
    viewport teleport, the per-user data path, and logging.

Observer-only gate

Callbacks carry information about other players, so the whole framework is gated on
IsLocalPlayerObserver() — local player is an observer or dead, which is also true during replay
playback. While that gate is closed no callback is delivered and GO_Plugin_Tick is not called, so
a match participant cannot gain an information advantage from a plugin. Same condition
InGameUI::drawObserverStats already uses.

Engine impact

17 files, ~2460 insertions, 0 deletions. No existing engine function is restructured — every
touch point is an inserted guarded call, so git diff main -U0 | grep '^-[^-]' is empty.

Three quarters of the diff is the two new self-contained files. The rest:

File Added What
PluginABI.h, PluginManager.h/.cpp 2076 new files
ProductionUpdate.cpp 82 queue / cancel / complete / destroy events
WindowXlat.cpp 70 mouse passthrough
InGameUI.cpp/.h 70 draw hook at the end of postWindowDraw(), plus two text primitives
ActiveBody.cpp 41 damage / heal events
View.h, W3DView.h/.cpp 40 worldToScreenTriReturnAllowFarClip(), below
CommandXlat.cpp 18 raw key-up passthrough
SpecialPowerModule.cpp 16 special-power event
W3DDisplay.cpp 16 registers the D3D device and HWND with the framework
GameEngine.cpp 10 load in init(), tick in update(), unload in the destructor
CMakeLists.txt 3 the new files

Two additions worth a reviewer's attention:

  • View::worldToScreenTriReturnAllowFarClip() — a new virtual with a non-pure default, so
    existing View subclasses are unaffected. worldToScreenTriReturn rejects anything past the far
    clip plane, but CameraClass::Project has already written a valid perspective-divided position by
    then (only OUTSIDE_NEAR_CLIP zeroes its output), and an off-screen edge indicator needs it. It
    is deliberately standalone rather than sharing a body with the existing function, which is
    therefore untouched. A downcast from PluginManager was rejected: ViewDummy is installed as
    TheTacticalView in headless builds.
  • InGameUI::drawPluginText2D[Scaled] — on InGameUI only because m_messageFont,
    m_messagePointSize and m_messageBold are protected with no getters. The rect and line
    primitives, which only forward to TheDisplay, live in the plugin framework instead.

Adds a host-side plugin loader and a C ABI so an observer overlay can
live outside the engine as a DLL. The engine loads every *.goplugin.dll
under plugins\ at startup; each plugin opts into the hook categories it
wants and receives events through an ABI that never exposes engine types.

With no plugin present nothing changes: every call site is a guarded call
whose condition is an inlined !empty() on a static vector.

PluginABI.h is the host/plugin contract - pure C, no STL and no engine
types, so a plugin compiles against that one header. It is versioned
(GO_PLUGIN_ABI_VERSION 5) with a structSize guard, and a mismatch fails
the load rather than risk misreading the table.

Callbacks carry information about other players, so the whole framework
is gated on the local player being an observer or dead - the same
condition InGameUI::drawObserverStats uses, and one that also holds
during replay playback. While that gate is closed no callback is
delivered and GO_Plugin_Tick is not called, so a match participant
cannot gain an information advantage from a plugin.

No existing engine function is restructured. Every touch point is an
inserted guarded call, so this diff removes no upstream line.

Two additions are worth a reviewer's attention:

- View::worldToScreenTriReturnAllowFarClip(), a new virtual with a
  non-pure default so existing View subclasses are unaffected.
  worldToScreenTriReturn rejects anything past the far clip plane, but
  CameraClass::Project has already written a valid perspective-divided
  position by then - only OUTSIDE_NEAR_CLIP zeroes its output - and an
  off-screen edge indicator needs it. It is deliberately standalone
  rather than sharing a body with the existing function, which is
  therefore untouched. A downcast from the plugin framework was rejected
  because ViewDummy is installed as TheTacticalView in headless builds.

- InGameUI::drawPluginText2D and drawPluginText2DScaled, on InGameUI
  only because m_messageFont, m_messagePointSize and m_messageBold are
  protected with no getters. The rect and line primitives, which only
  forward to TheDisplay, live in the plugin framework instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant