diff --git a/Core/GameEngine/Include/GameClient/View.h b/Core/GameEngine/Include/GameClient/View.h index 33ab4edd7fa..6bdeab72501 100644 --- a/Core/GameEngine/Include/GameClient/View.h +++ b/Core/GameEngine/Include/GameClient/View.h @@ -247,6 +247,10 @@ class View : public Snapshot Bool worldToScreen( const Coord3D *w, ICoord2D *s ) { return worldToScreenTriReturn( w, s ) == WTS_INSIDE_FRUSTUM; } ///< Transform world coordinate "w" into screen coordinate "s" virtual WorldToScreenReturn worldToScreenTriReturn(const Coord3D *w, ICoord2D *s ) = 0; ///< Like worldToScreen(), but with a more informative return value + /// Like worldToScreenTriReturn(), but a point beyond the far clip plane still projects instead of + /// returning WTS_INVALID, so an off-screen indicator can still be placed for it. Such a point is + /// reported as WTS_OUTSIDE_FRUSTUM even when its x/y land on screen. + virtual WorldToScreenReturn worldToScreenTriReturnAllowFarClip(const Coord3D *w, ICoord2D *s ) { return worldToScreenTriReturn( w, s ); } /// Transform screen point to the viewed world position on the 3D terrain. Returns true when intersection exists. virtual Bool screenToTerrain( const ICoord2D *screen, Coord3D *world ) = 0; diff --git a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index ab8a8c5380b..be7917c7344 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -60,6 +60,7 @@ #include "GameClient/GameClient.h" #include "GameClient/GameWindowManager.h" #include "GameClient/GameText.h" +#include "GameClient/Keyboard.h" #include "GameClient/ParticleSys.h" #include "GameClient/GUICallbacks.h" #include "GameClient/Shell.h" @@ -89,6 +90,9 @@ #include "GameNetwork/GameInfo.h" #include "GameNetwork/GameSpyOverlay.h" #include "GameNetwork/GameSpy/BuddyThread.h" +#if defined(GENERALS_ONLINE) +#include "GameNetwork/GeneralsOnline/Plugins/PluginManager.h" +#endif #include "WW3D2/ww3d.h" #include "../OnlineServices_Init.h" @@ -3934,6 +3938,20 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage disp = DESTROY_MESSAGE; } + // Forwarded even when the engine already handled this key; plugin hotkeys are the + // plugin's own choice and are not expected to collide with F11/F10/F5/INS. + if (GOPluginManager::HasRenderHooks()) + { + uint32_t modifierFlags = 0; + if (TheKeyboard != nullptr) + { + if (TheKeyboard->isCtrl()) modifierFlags |= 1; + if (TheKeyboard->isShift()) modifierFlags |= 2; + if (TheKeyboard->isAlt()) modifierFlags |= 4; + } + GOPluginManager::DispatchRawKeyUp((uint32_t)key, modifierFlags); + } + break; } #endif diff --git a/Core/GameEngine/Source/GameClient/MessageStream/WindowXlat.cpp b/Core/GameEngine/Source/GameClient/MessageStream/WindowXlat.cpp index 79553645b43..38324cd7c99 100644 --- a/Core/GameEngine/Source/GameClient/MessageStream/WindowXlat.cpp +++ b/Core/GameEngine/Source/GameClient/MessageStream/WindowXlat.cpp @@ -55,6 +55,10 @@ #include "GameClient/WindowXlat.h" #include "GameClient/Shell.h" #include "GameClient/Display.h" +#include "GameClient/Keyboard.h" +#if defined(GENERALS_ONLINE) +#include "GameNetwork/GeneralsOnline/Plugins/PluginManager.h" +#endif // DEFINES //////////////////////////////////////////////////////////////////// @@ -172,6 +176,72 @@ GameMessageDisposition WindowTranslator::translateGameMessage(const GameMessage Bool forceKeepMessage = FALSE; WinInputReturnCode returnCode = WIN_INPUT_NOT_USED; +#if defined(GENERALS_ONLINE) + // Deliberately ahead of the mouse-lock early return below. + if (GOPluginManager::HasRenderHooks()) + { + // buttonIndex stays -1 for anything that is not a click. + Int buttonIndex = -1; + Bool buttonDown = FALSE; + Bool isMouseMove = FALSE; + switch (msg->getType()) + { + case GameMessage::MSG_RAW_MOUSE_POSITION: + isMouseMove = TRUE; + break; + case GameMessage::MSG_RAW_MOUSE_LEFT_BUTTON_DOWN: + case GameMessage::MSG_RAW_MOUSE_LEFT_DOUBLE_CLICK: + buttonIndex = 0; buttonDown = TRUE; + break; + case GameMessage::MSG_RAW_MOUSE_LEFT_BUTTON_UP: + buttonIndex = 0; + break; + case GameMessage::MSG_RAW_MOUSE_MIDDLE_BUTTON_DOWN: + case GameMessage::MSG_RAW_MOUSE_MIDDLE_DOUBLE_CLICK: + buttonIndex = 1; buttonDown = TRUE; + break; + case GameMessage::MSG_RAW_MOUSE_MIDDLE_BUTTON_UP: + buttonIndex = 1; + break; + case GameMessage::MSG_RAW_MOUSE_RIGHT_BUTTON_DOWN: + case GameMessage::MSG_RAW_MOUSE_RIGHT_DOUBLE_CLICK: + buttonIndex = 2; buttonDown = TRUE; + break; + case GameMessage::MSG_RAW_MOUSE_RIGHT_BUTTON_UP: + buttonIndex = 2; + break; + default: + break; + } + + // Only the messages above carry a cursor position in argument 0. + if (isMouseMove || buttonIndex >= 0) + { + const ICoord2D& pos = msg->getArgument(0)->pixel; + if (isMouseMove) + { + GOPluginManager::DispatchMouseMove((int32_t)pos.x, (int32_t)pos.y); + } + else + { + // Bit order as documented by GORenderCallbacks. + uint32_t modifierFlags = 0; + if (TheKeyboard != nullptr) + { + if (TheKeyboard->isCtrl()) modifierFlags |= 1; + if (TheKeyboard->isShift()) modifierFlags |= 2; + if (TheKeyboard->isAlt()) modifierFlags |= 4; + } + + if (buttonDown) + GOPluginManager::DispatchMouseButtonDown((uint8_t)buttonIndex, (int32_t)pos.x, (int32_t)pos.y, modifierFlags); + else + GOPluginManager::DispatchMouseButtonUp((uint8_t)buttonIndex, (int32_t)pos.x, (int32_t)pos.y, modifierFlags); + } + } + } +#endif + if (TheTacticalView && TheTacticalView->isMouseLocked()) { //Kris: Aug 15, 2003 diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h index d37e41e86e8..b82b3f80433 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h @@ -224,6 +224,7 @@ class W3DView : public View, public SubsystemInterface virtual void setFieldOfView( Real angle ) override; ///< Set the horizontal field of view angle virtual WorldToScreenReturn worldToScreenTriReturn( const Coord3D *w, ICoord2D *s ) override; ///< Transform world coordinate "w" into screen coordinate "s" + virtual WorldToScreenReturn worldToScreenTriReturnAllowFarClip( const Coord3D *w, ICoord2D *s ) override; ///< As above, but a point beyond the far clip plane still projects virtual Bool screenToTerrain( const ICoord2D *screen, Coord3D *world ) override; virtual PlaneClass::IntersectionResType screenToWorldAtZ( const ICoord2D *screen, Coord3D *world, Real z ) override; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp index 5a784375675..fbe3c08ece9 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp @@ -2395,6 +2395,41 @@ View::WorldToScreenReturn W3DView::worldToScreenTriReturn( const Coord3D *w, ICo return WTS_INVALID; } +//------------------------------------------------------------------------------------------------- +/** As worldToScreenTriReturn, but a point beyond the far clip plane still yields a usable screen + position. CameraClass::Project only zeroes its output for OUTSIDE_NEAR_CLIP; past the far plane + it has already written the perspective-divided position before it returns. */ +//------------------------------------------------------------------------------------------------- +View::WorldToScreenReturn W3DView::worldToScreenTriReturnAllowFarClip( const Coord3D *w, ICoord2D *s ) +{ + // sanity + if( w == nullptr || s == nullptr || m_3DCamera == nullptr ) + return WTS_INVALID; + + Vector3 world; + Vector3 screen; + + world.Set( w->x, w->y, w->z ); + enum CameraClass::ProjectionResType projection = m_3DCamera->Project( screen, world ); + if( projection == CameraClass::OUTSIDE_NEAR_CLIP ) + { + s->x = 0; + s->y = 0; + return WTS_INVALID; + } + + W3DLogicalScreenToPixelScreen( screen.X, screen.Y, + &s->x, &s->y, + getWidth(), getHeight()); + s->x += m_originX; //convert viewport coordinates to full screen coordinates + s->y += m_originY; + + if( projection != CameraClass::INSIDE_FRUSTUM ) + return WTS_OUTSIDE_FRUSTUM; + + return WTS_INSIDE_FRUSTUM; +} + //------------------------------------------------------------------------------------------------- /** all the drawables in the view, that fall within the 2D screen region * will call the callback function. The number of drawables that passed diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index f49819dc3f4..2cadbd9a1f7 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -1160,6 +1160,8 @@ set(GAMEENGINE_SRC Include/GameNetwork/GeneralsOnline/HTTP/HTTPRequest.h Include/GameNetwork/GeneralsOnline/NextGenTransport.h Include/GameNetwork/GeneralsOnline/PluginInterfaces.h + Include/GameNetwork/GeneralsOnline/Plugins/PluginABI.h + Include/GameNetwork/GeneralsOnline/Plugins/PluginManager.h Include/GameNetwork/GeneralsOnline/Vendor/ValveNetworkingSockets/steam/isteamnetworkingmessages.h Include/GameNetwork/GeneralsOnline/Vendor/ValveNetworkingSockets/steam/isteamnetworkingsockets.h Include/GameNetwork/GeneralsOnline/Vendor/ValveNetworkingSockets/steam/isteamnetworkingutils.h @@ -1202,6 +1204,7 @@ set(GAMEENGINE_SRC Source/GameNetwork/GeneralsOnline/NextGenTransport.cpp Source/GameNetwork/GeneralsOnline/NetworkBitstream.cpp Source/GameNetwork/GeneralsOnline/PluginInterfaces.cpp + Source/GameNetwork/GeneralsOnline/Plugins/PluginManager.cpp ) if(RTS_GAMEMEMORY_ENABLE) diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h index af61c9b0355..66bb9674064 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h @@ -485,6 +485,12 @@ class InGameUI : public SubsystemInterface, public Snapshot virtual void postDraw(); ///< Logic which needs to occur after the UI renders virtual void postWindowDraw(); ///< Logic which needs to occur after the WindowManager has repainted the menus + // Text primitives backing GOPluginHostAPI (see PluginABI.h); here rather than in the plugin + // framework because they need the message font members below. Only ever called from within + // DispatchDrawOverlay(), i.e. during postWindowDraw() where 2D drawing is already set up. + void drawPluginText2D(Int x, Int y, const char* asciiText, Color color); + void drawPluginText2DScaled(Int x, Int y, const char* asciiText, Color color, Real sizeScale, Bool bold); + /// Ingame video playback virtual void playMovie(const AsciiString& movieName); virtual void stopMovie(); diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/Plugins/PluginABI.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/Plugins/PluginABI.h new file mode 100644 index 00000000000..c45e6592c9a --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/Plugins/PluginABI.h @@ -0,0 +1,433 @@ +#pragma once + +// The contract between the game client (host) and any plugin DLL. Must stay pure C - POD structs, +// function pointers and primitives only, no STL or engine types - so a plugin compiles against it +// without engine headers and both sides always agree on layout. The host loads any number of +// plugins; each opts into hook categories at GO_Plugin_Initialize via the table it is handed. + +#include + +#ifdef _WIN32 +#define GO_PLUGIN_API extern "C" __declspec(dllexport) +#define GO_PLUGIN_IMPORT extern "C" __declspec(dllimport) +#else +#define GO_PLUGIN_API extern "C" +#define GO_PLUGIN_IMPORT extern "C" +#endif + +// Bump when the layout of any struct below changes. GOPluginInfo::abiVersion and +// GOPluginHostAPI::abiVersion are checked against this on load; a mismatch fails the plugin load +// rather than risk silently misreading a function-pointer table with a different layout. +#define GO_PLUGIN_ABI_VERSION 5 + +// Compile-time layout check, spelled without static_assert/_Static_assert so this header still +// compiles as C89 as well as C++. Both sides fail to build the moment a struct below lays out +// differently from what this contract says, which is the one failure GO_PLUGIN_ABI_VERSION cannot +// catch. The expected sizes are the 32-bit (x86) ones - host and plugin are both Win32 builds. +#define GO_ABI_ASSERT_CONCAT_(a, b) a##b +#define GO_ABI_ASSERT_CONCAT(a, b) GO_ABI_ASSERT_CONCAT_(a, b) +#define GO_ABI_ASSERT_LAYOUT(cond) typedef char GO_ABI_ASSERT_CONCAT(goAbiLayoutAssert_, __LINE__)[(cond) ? 1 : -1] + +// Hook categories. GOPluginInfo::hookCategories is informational only; actual use requires the +// matching register* call during GO_Plugin_Initialize. Only the categories plugins consume are +// exposed, keeping the surface the engine must guarantee small. Carried as uint32_t, never as the +// enum type - the enum has no base-type specifier. +enum EGOPluginHookCategory +{ + GO_HOOK_NONE = 0, + GO_HOOK_GAMEPLAY_EVENTS = 1 << 0, // unit/upgrade/power/building gameplay events + GO_HOOK_RENDER = 1 << 1, // per-frame overlay draw + raw hotkey passthrough +}; + +// Production/power/building events. Payloads are plain data rather than engine pointers, since +// they cross a DLL boundary and must not depend on the host's C++ class layout. +struct GOUnitEvent +{ + uint32_t playerIndex; + const char* templateName; // ThingTemplate name, e.g. "AmericaVehicleCrusader" + uint32_t producerObjectId; // producing building's ObjectID (0 if unavailable) + float percentComplete; // 0..100, valid for queued/completed; ignored for cancelled + int32_t productionID; // engine ProductionID, stable per queue entry until completion/cancel + float producerPositionX; // producing building's world position at the moment of this event + float producerPositionY; + float producerPositionZ; +}; +GO_ABI_ASSERT_LAYOUT(sizeof(GOUnitEvent) == 32); + +struct GOUpgradeEvent +{ + uint32_t playerIndex; + const char* templateName; // UpgradeTemplate name + uint32_t producerObjectId; + float percentComplete; + float producerPositionX; + float producerPositionY; + float producerPositionZ; +}; +GO_ABI_ASSERT_LAYOUT(sizeof(GOUpgradeEvent) == 28); + +struct GOBuildingEvent +{ + uint32_t objectId; // the destroyed production structure's ObjectID + uint32_t playerIndex; + float positionX; // world position at the moment of destruction + float positionY; + float positionZ; +}; +GO_ABI_ASSERT_LAYOUT(sizeof(GOBuildingEvent) == 20); + +struct GOSpecialPowerEvent +{ + uint32_t playerIndex; + const char* powerTemplateName; + float locationX; + float locationY; + float locationZ; + float rechargeTimeSeconds; // static reload duration for this power template (0 if unknown) +}; +GO_ABI_ASSERT_LAYOUT(sizeof(GOSpecialPowerEvent) == 24); + +// Shared payload for onObjectDamaged and onObjectHealed; which callback fired implies the +// direction. See plans\plugin-framework\design-notes.md for where it is raised and why. +// All four-byte members come first so the three flags pack contiguously at the end. +struct GOCombatEvent +{ + uint32_t objectId; // the object whose health changed + uint32_t sourceObjectId; // attacker/healer ObjectID (0 if unavailable, e.g. environmental damage) + uint32_t playerIndex; // owner of objectId + int32_t amount; // always positive; onObjectDamaged vs onObjectHealed implies the sign + float positionX; // objectId's world position at the moment of this event + float positionY; + float positionZ; + uint8_t isBuilding; // KINDOF_STRUCTURE at the moment of this event + uint8_t isUnit; // KINDOF_INFANTRY || KINDOF_VEHICLE - deliberately not "!isBuilding" + uint8_t isFlame; // DAMAGE_FLAME, a continuous per-frame tick source (onObjectDamaged only) +}; +GO_ABI_ASSERT_LAYOUT(sizeof(GOCombatEvent) == 32); + +struct GOGameplayEventCallbacks +{ + void (*onUnitQueued)(const GOUnitEvent* ev); + void (*onUnitCancelled)(const GOUnitEvent* ev); + void (*onUnitCompleted)(const GOUnitEvent* ev); + void (*onUpgradeQueued)(const GOUpgradeEvent* ev); + void (*onUpgradeCancelled)(const GOUpgradeEvent* ev); + void (*onUpgradeCompleted)(const GOUpgradeEvent* ev); + // Production structures only - the hook lives in ProductionUpdate, so a destroyed power plant + // or defensive structure does not raise it. Enough to retire a building's queue entries. + void (*onBuildingDestroyed)(const GOBuildingEvent* ev); + void (*onSpecialPowerTriggered)(const GOSpecialPowerEvent* ev); + void (*onObjectDamaged)(const GOCombatEvent* ev); + void (*onObjectHealed)(const GOCombatEvent* ev); +}; + +// One entry in a player's general-power roster (see getPlayerGeneralPowers). templateName points +// into engine-owned static storage and is valid for the duration of the call. +struct GOGeneralPowerInfo +{ + const char* templateName; // SpecialPowerTemplate name, e.g. "AmericaSuperweaponParticleCannon" + uint32_t rechargeFrames; // full reload duration in logic frames (0 = recharges instantly) + uint32_t framesUntilReady; // logic frames until it can be triggered again (0 = ready now) + uint32_t buildingObjectId; // ObjectID of the building carrying this power's module (0 if none) +}; +GO_ABI_ASSERT_LAYOUT(sizeof(GOGeneralPowerInfo) == 16); + +struct GOContainedObjectInfo +{ + uint32_t objectId; + const char* templateName; + uint32_t playerIndex; +}; +GO_ABI_ASSERT_LAYOUT(sizeof(GOContainedObjectInfo) == 12); + +// Per-frame overlay draw plus raw input passthrough. onDrawOverlay runs once a frame from +// InGameUI's draw path, in 2D screen space with the HUD's ortho projection already active, so a +// plugin can issue the host's 2D primitives without touching 3D state. +struct GORenderCallbacks +{ + void (*onDrawOverlay)(); + // scanCode is a DirectInput scan code (DIK_*, e.g. DIK_F9 == 0x43), which is what the engine + // itself keys off - NOT a Windows virtual-key code. Comparing against VK_* silently never + // matches. modifierFlags: bit0 = CTRL, bit1 = SHIFT, bit2 = ALT. + void (*onRawKeyUp)(uint32_t scanCode, uint32_t modifierFlags); + + // Side-channel notification like onRawKeyUp: never gates or consumes the input. Fires more often + // than any other hook here, so keep handlers cheap. Screen-space pixels, as drawRect2D. + void (*onMouseMove)(int32_t x, int32_t y); + // Mouse-button passthrough for clickable plugin UI. Same side-channel + // discipline as onMouseMove. buttonIndex: 0 = left, 1 = middle, 2 = right. modifierFlags: bit0 = + // CTRL, bit1 = SHIFT, bit2 = ALT. The mouse-button cases exist so a plugin can act on clicks on + // its own drawn UI (e.g. jump the viewport to what was clicked). + void (*onMouseButtonDown)(uint8_t buttonIndex, int32_t x, int32_t y, uint32_t modifierFlags); + void (*onMouseButtonUp)(uint8_t buttonIndex, int32_t x, int32_t y, uint32_t modifierFlags); +}; + +// Production-building purposes a plugin might treat differently. Deliberately closed: the five +// there was a need to distinguish, not a general taxonomy. Returned as uint8_t, never as the enum. +enum EGOBuildingCategory +{ + GO_BUILDING_CATEGORY_NONE = 0, // not a building, or a building outside the categories below + GO_BUILDING_CATEGORY_COMMAND_CENTER = 1, + GO_BUILDING_CATEGORY_WAR_FACTORY = 2, + GO_BUILDING_CATEGORY_BARRACKS = 3, + GO_BUILDING_CATEGORY_AIRFIELD = 4, + GO_BUILDING_CATEGORY_SUPPLY_STASH = 5, +}; + +// Handed to the plugin at GO_Plugin_Initialize. Registration functions are independent and +// optional, and may each be called more than once. LIFETIME: the pointer has process lifetime, so a +// plugin may retain it - but copying the struct by value costs nothing and does not rely on that. +struct GOPluginHostAPI +{ + uint32_t abiVersion; + + // sizeof(GOPluginHostAPI) as the host compiled it, at a fixed offset so a plugin can read it + // before touching anything further down the table. A plugin must refuse to initialize when this + // is smaller than its own sizeof - see plans\plugin-framework\design-notes.md. + uint32_t structSize; + + void (*log)(const char* msg); + + void (*registerGameplayEventHooks)(const GOGameplayEventCallbacks* cb); + void (*registerRenderHooks)(const GORenderCallbacks* cb); + + // --- Player roster queries. Pure queries, no side effects. playerIndex matches the + // GOUnitEvent/GOUpgradeEvent/GOBuildingEvent/GOSpecialPowerEvent field of the same name. Return + // 0 / empty string / false if playerIndex doesn't currently resolve to an active player (no + // match currently loaded, index out of range, observer slot, etc). --- + uint32_t (*getPlayerColor)(uint32_t playerIndex); // colorARGB, same packing as drawText2D/drawRect2D + + // Active, non-observer participants; returns how many were written, never more than maxCount. + // Excludes the neutral/civilian player, and the values are the same playerIndex the event + // structs carry. Returns 0 when no match is loaded. + uint32_t (*getActivePlayers)(uint32_t* outPlayerIndices, uint32_t maxCount); + + // TRUE if the local client is spectating rather than playing (dead counts as spectating; replay + // playback is always true). A plugin showing other players' information MUST gate on this: not + // doing so exposes it to a live participant, which is a cheat vector, not a display bug. The host + // also enforces it - while false, no callbacks are delivered and GO_Plugin_Tick is not called. + uint8_t (*isLocalPlayerObserver)(); + + // General powers the player owns: required science plus a live object carrying the module, so a + // superweapon only counts once its building exists. The event hooks fire only on use, so this is + // the only way to show owned-but-unused powers. Cooldowns come from the module's own clock and + // stay correct across pause and fast-forward. Returns the number written, at most maxCount. + uint32_t (*getPlayerGeneralPowers)(uint32_t playerIndex, GOGeneralPowerInfo* outPowers, uint32_t maxCount); + + // --- Icon drawing. Looks up the named template server-side (where the engine's ThingTemplate/ + // UpgradeTemplate/CommandButton data safely lives) and draws its button art; the plugin never + // needs the Image/ThingTemplate/CommandButton types themselves. Does nothing if + // the name doesn't resolve. --- + + // Draws a unit or player-upgrade's button icon. Tries a ThingTemplate lookup by templateName + // first, then an UpgradeTemplate lookup if that fails - matches GOUnitEvent::templateName or + // GOUpgradeEvent::templateName respectively, so you don't need to track which kind it was. + void (*drawTemplateIcon2D)(const char* templateName, int32_t x, int32_t y, int32_t width, int32_t height); + + // Draws a special power's button icon for the given player (a power's icon is defined on the + // CommandButton that exposes it, which is per-faction - hence needing playerIndex, unlike + // drawTemplateIcon2D). Matches GOSpecialPowerEvent::powerTemplateName. + void (*drawPowerIcon2D)(uint32_t playerIndex, const char* powerTemplateName, int32_t x, int32_t y, int32_t width, int32_t height); + + // Minimal 2D draw primitives for render-hook plugins, screen-space pixels, top-left origin. + // Valid only from onDrawOverlay. Text is 7-bit ASCII: the host widens it byte by byte, so any + // multi-byte sequence renders as one garbage character per byte. + void (*drawText2D)(int32_t x, int32_t y, const char* asciiText, uint32_t colorARGB); + void (*drawRect2D)(int32_t x, int32_t y, int32_t width, int32_t height, uint32_t colorARGB, uint8_t filled); + + // Like drawText2D, but sizeScale multiplies the host's own message font point size (1.0 matches + // drawText2D exactly, values outside the host's supported range are clamped) and bold is + // explicit. See plans\plugin-framework\design-notes.md for why this is a separate function. + void (*drawText2DScaled)(int32_t x, int32_t y, const char* asciiText, uint32_t colorARGB, float sizeScale, uint8_t bold); + + // Current render target size in pixels, same coordinate space as drawText2D/drawRect2D/ + // drawTemplateIcon2D/drawPowerIcon2D/onMouseMove. Lets a render-hook plugin anchor drawn UI to + // a screen edge (bottom/right) instead of only a fixed top-left-relative offset. Safe to call + // from onDrawOverlay every frame. + void (*getScreenSize)(int32_t* outWidth, int32_t* outHeight); + + // Simulation clock, for ageing anything recorded from the events - wall-clock time drifts against + // a paused, delayed or fast-forwarded simulation. Return 0 when no game is running. + uint32_t (*getLogicFrame)(); + uint32_t (*getLogicFramesPerSecond)(); + + // Live production progress, read from the engine's ProductionEntry on demand. The event structs + // carry percentComplete only as of the moment they fire - essentially always 0 for a queued + // event - and nothing reports progress changing. Return 0..100, or -1 if the entry is gone. + float (*getUnitProductionProgress)(uint32_t producerObjectId, int32_t productionID); + float (*getUpgradeProductionProgress)(uint32_t producerObjectId, const char* upgradeTemplateName); + + // --- World-space anchoring. GOUnitEvent/GOUpgradeEvent/GOBuildingEvent carry world + // positions, but there was no way to turn one into a screen position, so world-anchored UI was + // unreachable from a plugin. worldToScreen returns 0 (and leaves the outputs untouched) when + // the point is outside the view frustum. --- + uint8_t (*worldToScreen)(float worldX, float worldY, float worldZ, int32_t* outX, int32_t* outY); + + // Screen-space bounding box of a live object, derived from its world position and bounding + // radius - for sizing/placing UI relative to the thing it belongs to, rather than guessing a + // scale from the screen resolution. Returns 0 if the object no longer exists or is off screen. + uint8_t (*getObjectScreenBounds)(uint32_t objectId, int32_t* outX, int32_t* outY, int32_t* outWidth, int32_t* outHeight); + + // Same point the engine projects its own health bar onto (Object::getHealthBoxPosition: + // top-of-model + 10 world units + the object's healthBoxOffset), projected via worldToScreen. + // Returns 0 if the object no longer exists or the point is off screen. + uint8_t (*getObjectHealthBarScreenPosition)(uint32_t objectId, int32_t* outX, int32_t* outY); + + // Moves the observer's viewport to a world position, by the same camera path the engine's own + // observer look-at actions use - for jumping to where a gameplay event happened. + void (*teleportViewportTo)(float worldX, float worldY, float worldZ); + + // The engine's own radial "pie" fill, as the command bar uses for build progress and recharge. + // percent is 0..100; drawRectClock2D fills the elapsed wedge, the other the remaining one. + void (*drawRectClock2D)(int32_t x, int32_t y, int32_t width, int32_t height, int32_t percent, uint32_t colorARGB); + void (*drawRemainingRectClock2D)(int32_t x, int32_t y, int32_t width, int32_t height, int32_t percent, uint32_t colorARGB); + + // D3D8 handles for a plugin with its own UI backend, opaque so no engine types cross the + // boundary. The device is the engine's own live IDirect3DDevice8, not a copy: a plugin MUST save + // and restore every state it touches. The engine assumes its state survives and cannot recover + // from a dirty device. + void* (*getD3DDevice8)(); + void* (*getGameWindow)(); + uint32_t (*enumeratePlayerObjects)(uint32_t playerIndex, + void (*callback)(uint32_t objectId, float posX, float posY, float posZ, void* userData), + void* userData); + uint32_t (*getContainedObjects)(uint32_t containerObjectId, + GOContainedObjectInfo* outObjects, uint32_t maxCount); + + // Target tracking: an object's current attack/move target position (current victim's position + // when attacking an object, otherwise the AI state machine's goal position). Returns 0 when the + // object is idle or has no target. isObjectAirborne reports Object::isAirborneTarget. + uint8_t (*getObjectTargetPosition)(uint32_t objectId, float* outTargetX, float* outTargetY, float* outTargetZ); + uint8_t (*isObjectAirborne)(uint32_t objectId); + + // 1 for a KINDOF_VEHICLE. Identifies an upgrade queued on an already-built vehicle, which + // getObjectBuildingCategory cannot - a vehicle always reports GO_BUILDING_CATEGORY_NONE. + uint8_t (*isObjectVehicle)(uint32_t objectId); + + // Like worldToScreen, but clamps an off-viewport point to just inside the screen edge instead of + // dropping it; returns 0 only when no projection is possible at all. For anything that should + // survive as an edge indicator when its world anchor pans off-screen. + uint8_t (*worldToScreenClamped)(float worldX, float worldY, float worldZ, int32_t* outX, int32_t* outY); + + // Which of the five categories in EGOBuildingCategory a producing building belongs to (or + // GO_BUILDING_CATEGORY_NONE if it isn't a building, or isn't one of the five). For per-category + // UI treatment - e.g. a different queue-panel offset for airfields vs war factories. + uint8_t (*getObjectBuildingCategory)(uint32_t objectId); + + // Player card queries. Pure, same playerIndex semantics as getPlayerColor, returning 0 / empty + // string / -1 when it does not resolve. Each string-returning function owns one buffer, so only + // its own most recent return value is live - copy it out immediately. Not thread-safe; the whole + // ABI runs on the engine's single thread. + + // Player display name. Returns empty string if the player slot is not active. + const char* (*getPlayerName)(uint32_t playerIndex); + + // Faction template name (e.g. "America", "China", "GLA", or sub-faction variants like + // "AmericaAirForce"). Returns empty string on failure. + const char* (*getPlayerFactionTemplate)(uint32_t playerIndex); + + // Current credits (money). Returns 0 if unavailable. + uint32_t (*getPlayerMoney)(uint32_t playerIndex); + + // General rank (0-based: 0 == the player's first rank) and skill-point progress toward the + // next rank. outCurrentXP is the player's cumulative skill points; outNextXP is the total + // skill points required for the next rank (0 at max rank - there is no "next"). Returns -1 if + // playerIndex doesn't resolve or the player has no rank yet (rank 0/uninitialized). + int32_t (*getPlayerRank)(uint32_t playerIndex, uint32_t* outCurrentXP, uint32_t* outNextXP); + + // Power state: outPowerGenerated / outPowerDrain, in the engine's own energy units. Returns 1 + // if power data is available, 0 otherwise (outputs left untouched). + uint8_t (*getPlayerPowerState)(uint32_t playerIndex, uint32_t* outPowerGenerated, uint32_t* outPowerDrain); + + // Count of the player's live construction units - USA/China Dozer, GLA Worker - identified by + // the shared DozerAIInterface (AIUpdateInterface::getDozerAIInterface() != nullptr) rather than + // a per-faction template/KindOf list, so it stays correct across all three factions uniformly. + // Returns 0 if playerIndex doesn't resolve. + uint32_t (*getPlayerBuilderCount)(uint32_t playerIndex); + + // Supply units actually ferrying right now, not merely idle or empty-moving - backed by + // SupplyTruckAIInterface::isCurrentlyFerryingSupplies(). + uint32_t (*getPlayerActiveGathererCount)(uint32_t playerIndex); + + // Cumulative gross money earned this match, not net of spending - the score screen's own figure. + // Deliberately the raw counter rather than a rate, so a plugin picks its own averaging window. + uint32_t (*getPlayerTotalMoneyEarned)(uint32_t playerIndex); + + // Display name for a template ("AmericaInfantryRifle" -> "Ranger"), via the same lookup the + // game's own tooltips use. Empty string if it does not resolve. + const char* (*getTemplateDisplayName)(const char* templateName); + + // A sample template name from the same qualifying checks as getPlayerBuilderCount / + // getPlayerActiveGathererCount, so a plugin can draw the player's actual builder icon rather + // than guess a per-faction template. Empty string if no qualifying unit exists. + const char* (*getPlayerBuilderTemplateName)(uint32_t playerIndex); + + // Per-type builder breakdown, which the summed count and the single sample name above cannot + // give - neither tells a native Dozer from a captured GLA Worker. Writes up to maxCount parallel + // entries and returns how many distinct templates were found. outNames is call-lifetime only. + uint32_t (*getPlayerBuilderTemplateCounts)(uint32_t playerIndex, const char** outNames, uint32_t* outCounts, uint32_t maxCount); + + // The gatherer counterpart of getPlayerBuilderTemplateName above, sampling one of the player's + // currently supply-ferrying units instead. Same return and storage discipline. + const char* (*getPlayerGathererTemplateName)(uint32_t playerIndex); + + // Lobby-configured alliance/team number (GameSlot::getTeamNumber - the same value the skirmish/ + // lobby "Team" dropdown sets, and what the game's own alliance/enemy checks are based on), NOT a + // display-position or player-count index. Returns -1 if playerIndex doesn't resolve or the + // player has no team assigned (free-for-all / no alliance). + int32_t (*getPlayerTeamNumber)(uint32_t playerIndex); + + // Every real participant including defeated and resigned ones: filters on isPlayerObserver() but + // not isPlayerActive(), so the roster does not shrink or reindex when someone dies. Use this + // rather than getActivePlayers for roster tracking. + uint32_t (*getMatchPlayers)(uint32_t* outPlayerIndices, uint32_t maxCount); + + // 1 if the player is no longer playing (dead or resigned - !Player::isPlayerActive()), 0 if + // they're still an active match participant or playerIndex doesn't resolve. Covers both causes + // of "no longer playing" with one boolean rather than requiring a plugin to distinguish them. + uint8_t (*getPlayerIsDefeated)(uint32_t playerIndex); + + // The engine's per-user data directory, trailing backslash included - where options.ini, Replays + // and Maps live, and so where a plugin's user-owned files belong. The plugin's own folder is + // under Program Files and is not writable. Process lifetime, never null; empty means unresolved. + const char* (*getUserDataPath)(); + + // One straight line as a single rotated quad (Render2DClass::Add_Line), same coordinate space and + // colour packing as drawRect2D. drawRect2D is axis-aligned, so faking a diagonal through it costs + // a quad every few pixels; this costs one whatever the length. + void (*drawLine2D)(int32_t x1, int32_t y1, int32_t x2, int32_t y2, float thickness, uint32_t colorARGB); + + // 1 for a KINDOF_PROJECTILE. enumeratePlayerObjects walks everything a player owns, and a missile + // or bomb in flight owns a target like any attacker does, so target-tracking overlays draw a line + // for every shot unless they filter these out. + uint8_t (*isObjectProjectile)(uint32_t objectId); + + // The id of the object this one is inside (Object::getContainedBy), or 0 when it is not contained. + // enumeratePlayerObjects returns garrisoned occupants alongside everything else, and each of them + // carries its own target from the same world position, so a full building emits one identical line + // per occupant. Collapse on this id to draw the container once. + uint32_t (*getObjectContainerId)(uint32_t objectId); +}; + +struct GOPluginInfo +{ + uint32_t abiVersion; + const char* name; + const char* version; + uint32_t hookCategories; // bitmask of EGOPluginHookCategory, informational only +}; +GO_ABI_ASSERT_LAYOUT(sizeof(GOPluginInfo) == 16); + +// ------------------------------------------------------------------------------------------------ +// Fixed export names every plugin DLL must implement, resolved via GetProcAddress so the DLL +// interface is plain C - no link-time coupling between host and plugin. +// ------------------------------------------------------------------------------------------------ +typedef void (*GOPluginGetInfoFunc)(GOPluginInfo* outInfo); +typedef bool (*GOPluginInitializeFunc)(const GOPluginHostAPI* hostAPI); +typedef void (*GOPluginShutdownFunc)(); +typedef void (*GOPluginTickFunc)(); // optional; may be null + +#define GO_PLUGIN_EXPORT_GETINFO_NAME "GO_Plugin_GetInfo" +#define GO_PLUGIN_EXPORT_INITIALIZE_NAME "GO_Plugin_Initialize" +#define GO_PLUGIN_EXPORT_SHUTDOWN_NAME "GO_Plugin_Shutdown" +#define GO_PLUGIN_EXPORT_TICK_NAME "GO_Plugin_Tick" diff --git a/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/Plugins/PluginManager.h b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/Plugins/PluginManager.h new file mode 100644 index 00000000000..dc463bed4a3 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameNetwork/GeneralsOnline/Plugins/PluginManager.h @@ -0,0 +1,90 @@ +#pragma once + +// Host-side loader and dispatcher for the plugin ABI (see PluginABI.h). Engine call sites call the +// Dispatch* functions below and this fans each one out to every plugin registered for that +// category, so the call sites stay plugin-agnostic. + +#include "GameNetwork/GeneralsOnline/Plugins/PluginABI.h" +#include +#include +#include + +class GOPluginManager +{ +public: + // Loads every *.goplugin.dll found (non-recursive) in directoryPath. Safe to call once at + // startup; failures to load an individual plugin are logged and skipped, not fatal. + static void LoadPluginsFromDirectory(const char* directoryPath); + + static bool LoadPlugin(const char* dllPath); + static void UnloadAll(); + + // Calls GO_Plugin_Tick on every plugin that exported it. Call once per frame from + // GameEngine::update(), unconditionally (not gated behind GameLogic pause state). + static void Tick(); + + // TRUE when the local client is spectating rather than playing - observer, dead, or replay + // playback. Every Dispatch* and Tick() is gated on it, so a plugin never receives gameplay data + // during a live match: that would be an information advantage for a participant. + static bool IsLocalPlayerObserver(); + + // ---- IGameplayEventHooks dispatch (called from ProductionUpdate/SpecialPowerModule/etc) ---- + static bool HasGameplayEventHooks() { return !s_gameplayEventHooks.empty(); } + static void DispatchUnitQueued(const GOUnitEvent& ev); + static void DispatchUnitCancelled(const GOUnitEvent& ev); + static void DispatchUnitCompleted(const GOUnitEvent& ev); + static void DispatchUpgradeQueued(const GOUpgradeEvent& ev); + static void DispatchUpgradeCancelled(const GOUpgradeEvent& ev); + static void DispatchUpgradeCompleted(const GOUpgradeEvent& ev); + static void DispatchBuildingDestroyed(const GOBuildingEvent& ev); + static void DispatchSpecialPowerTriggered(const GOSpecialPowerEvent& ev); + static void DispatchObjectDamaged(const GOCombatEvent& ev); + static void DispatchObjectHealed(const GOCombatEvent& ev); + + // ---- IRenderHooks dispatch (called from InGameUI / CommandXlat / WindowXlat) ---- + static bool HasRenderHooks() { return !s_renderHooks.empty(); } + static void DispatchDrawOverlay(); + static void DispatchRawKeyUp(uint32_t scanCode, uint32_t modifierFlags); + static void DispatchMouseMove(int32_t x, int32_t y); + static void DispatchMouseButtonDown(uint8_t buttonIndex, int32_t x, int32_t y, uint32_t modifierFlags); + static void DispatchMouseButtonUp(uint8_t buttonIndex, int32_t x, int32_t y, uint32_t modifierFlags); + + // Registration callbacks, invoked from GOPluginHostAPI function pointers during plugin + // Initialize. Public because the free functions backing the host API function-pointer table + // (in PluginManager.cpp) call them; not intended to be called directly by engine code. + static void RegisterGameplayEventHooks(const GOGameplayEventCallbacks* cb); + static void RegisterRenderHooks(const GORenderCallbacks* cb); + static void Log(const char* msg); + + // Native-handle seam: this class is device-independent, so GameEngineDevice supplies the D3D + // device and window handle at display init - see plans\plugin-framework\design-notes.md. + // Both stay null in a headless or non-W3D build, as do the matching host API entries. + typedef void* (*NativeHandleProvider)(); + static void SetNativeHandleProviders(NativeHandleProvider d3dDevice8, NativeHandleProvider gameWindow); + static void* GetD3DDevice8(); + static void* GetGameWindow(); + +private: + struct LoadedPlugin + { + HMODULE module; + std::string path; + GOPluginInfo info; + std::string name; // owns GOPluginInfo::name's lifetime on the host side + std::string version; + GOPluginShutdownFunc shutdown; + GOPluginTickFunc tick; + }; + + static std::vector s_plugins; + static std::vector s_gameplayEventHooks; + static std::vector s_renderHooks; + + static NativeHandleProvider s_d3dDevice8Provider; + static NativeHandleProvider s_gameWindowProvider; + + static GOPluginHostAPI BuildHostAPI(); + + // The one table every plugin is handed. Process lifetime - see the definition. + static const GOPluginHostAPI& GetHostAPI(); +}; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index d39ea9bee76..89bbd785ec0 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -114,6 +114,7 @@ #include "../OnlineServices_Init.h" #include "GameNetwork/GeneralsOnline/DiscordRichPresence.h" #include "GameNetwork/GeneralsOnline/OnlineServices_LobbyInterface.h" +#include "GameNetwork/GeneralsOnline/Plugins/PluginManager.h" #include "GameNetwork/GameSpyOverlay.h" #include #include "WW3D2/ww3d.h" @@ -310,6 +311,8 @@ GameEngine::GameEngine() //------------------------------------------------------------------------------------------------- GameEngine::~GameEngine() { + GOPluginManager::UnloadAll(); + delete m_discordRichPresence; m_discordRichPresence = nullptr; @@ -834,6 +837,9 @@ void GameEngine::init() HideControlBar(); + // One folder per plugin under the plugins directory; a failed load is logged, never fatal. + GOPluginManager::LoadPluginsFromDirectory("plugins"); + m_discordRichPresence = new GeneralsOnlineDiscordRPC(); m_discordRichPresence->Initialize(); } @@ -991,6 +997,10 @@ void GameEngine::update() TheAudio->UPDATE(); TheGameClient->UPDATE(); + + // Deliberately outside the GameLogic pause gate, so plugin polling survives a pause. + GOPluginManager::Tick(); + TheMessageStream->propagateMessages(); if (TheNetwork != nullptr) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index 899f0254005..65f802d7f44 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -90,6 +90,7 @@ #include "GameNetwork/GameInfo.h" #include "GameNetwork/NetworkInterface.h" +#include "GameNetwork/GeneralsOnline/Plugins/PluginManager.h" #include "Common/UnitTimings.h" //Contains the DO_UNIT_TIMINGS define jba. @@ -3880,6 +3881,69 @@ void InGameUI::postWindowDraw() if (m_observerNotificationPointSize > 0) drawObserverNotifications(hudOffsetX, hudOffsetY); + + // Last, so plugins share the screen-space 2D context the draw* calls above set up. + if (GOPluginManager::HasRenderHooks()) + GOPluginManager::DispatchDrawOverlay(); +} + +//------------------------------------------------------------------------------------------------- +/** Backs GOPluginHostAPI::drawText2D, in the same font as InGameUI's own messages. */ +//------------------------------------------------------------------------------------------------- +void InGameUI::drawPluginText2D(Int x, Int y, const char* asciiText, Color color) +{ + if (asciiText == nullptr || asciiText[0] == '\0' || TheDisplayStringManager == nullptr || TheFontLibrary == nullptr) + return; + + DisplayString* displayString = TheDisplayStringManager->newDisplayString(); + if (displayString == nullptr) + return; + + displayString->setFont(TheFontLibrary->getFont(m_messageFont, + TheGlobalLanguageData ? TheGlobalLanguageData->adjustFontSize(m_messagePointSize) : m_messagePointSize, m_messageBold)); + + UnicodeString text; + text.translate(AsciiString(asciiText)); + displayString->setText(text); + + displayString->draw(x, y, color, GameMakeColor(0, 0, 0, 255)); + + TheDisplayStringManager->freeDisplayString(displayString); +} + +//------------------------------------------------------------------------------------------------- +/** Backs GOPluginHostAPI::drawText2DScaled: as above, at a scaled point size. */ +//------------------------------------------------------------------------------------------------- +void InGameUI::drawPluginText2DScaled(Int x, Int y, const char* asciiText, Color color, Real sizeScale, Bool bold) +{ + if (asciiText == nullptr || asciiText[0] == '\0' || TheDisplayStringManager == nullptr || TheFontLibrary == nullptr) + return; + + // FontLibrary::getFont returns null outside this range, so clamp before asking. + const Int minPointSize = 1; + const Int maxPointSize = 100; + const Int baseSize = TheGlobalLanguageData ? TheGlobalLanguageData->adjustFontSize(m_messagePointSize) : m_messagePointSize; + Int effectiveSize = (Int)((Real)baseSize * sizeScale + 0.5f); + if (effectiveSize < minPointSize) effectiveSize = minPointSize; + if (effectiveSize > maxPointSize) effectiveSize = maxPointSize; + + GameFont* font = TheFontLibrary->getFont(m_messageFont, effectiveSize, bold); + if (font == nullptr) + return; + + DisplayString* displayString = TheDisplayStringManager->newDisplayString(); + if (displayString == nullptr) + return; + + displayString->setFont(font); + + UnicodeString text; + text.translate(AsciiString(asciiText)); + displayString->setText(text); + + displayString->draw(x, y, color, GameMakeColor(0, 0, 0, 255)); + + TheDisplayStringManager->freeDisplayString(displayString); } //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp index 21ce7bd81ae..122b09883d7 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp @@ -59,6 +59,7 @@ #include "GameLogic/Module/ContainModule.h" #include "GameLogic/Module/DamageModule.h" #include "GameLogic/Module/DieModule.h" +#include "GameNetwork/GeneralsOnline/Plugins/PluginManager.h" @@ -619,6 +620,28 @@ void ActiveBody::attemptDamage( DamageInfo *damageInfo ) d->onDamage( damageInfo ); } + + // !alreadyHandled matters: the kill-pilot, kill-garrisoned, status and subdual paths above + // skip internalChangeHealth, so m_prevHealth/m_currentHealth still hold the previous call's + // values and m_actualDamageClipped is that call's number. Without this the plugin gets a + // second, phantom event repeating damage it was already told about. + if (!alreadyHandled && GOPluginManager::HasGameplayEventHooks()) + { + GOCombatEvent ev = {}; + ev.objectId = (uint32_t)obj->getID(); + ev.sourceObjectId = (damageInfo->in.m_sourceID != INVALID_ID) ? (uint32_t)damageInfo->in.m_sourceID : 0; + Player* owner = obj->getControllingPlayer(); + ev.playerIndex = (owner != nullptr) ? (uint32_t)owner->getPlayerIndex() : 0; + ev.amount = (int32_t)(damageInfo->out.m_actualDamageClipped + 0.5f); + ev.isBuilding = obj->isKindOf(KINDOF_STRUCTURE) ? 1 : 0; + ev.isUnit = (obj->isKindOf(KINDOF_INFANTRY) || obj->isKindOf(KINDOF_VEHICLE)) ? 1 : 0; + ev.isFlame = (damageInfo->in.m_damageType == DAMAGE_FLAME) ? 1 : 0; + const Coord3D* pos = obj->getPosition(); + ev.positionX = (pos != nullptr) ? pos->x : 0.0f; + ev.positionY = (pos != nullptr) ? pos->y : 0.0f; + ev.positionZ = (pos != nullptr) ? pos->z : 0.0f; + GOPluginManager::DispatchObjectDamaged(ev); + } } if (m_curDamageState != oldState) @@ -858,6 +881,24 @@ void ActiveBody::attemptHealing( DamageInfo *damageInfo ) d->onHealing( damageInfo ); } + + if (GOPluginManager::HasGameplayEventHooks()) + { + GOCombatEvent ev = {}; + ev.objectId = (uint32_t)obj->getID(); + ev.sourceObjectId = (damageInfo->in.m_sourceID != INVALID_ID) ? (uint32_t)damageInfo->in.m_sourceID : 0; + Player* owner = obj->getControllingPlayer(); + ev.playerIndex = (owner != nullptr) ? (uint32_t)owner->getPlayerIndex() : 0; + // m_actualDamageClipped is negative for healing; the ABI wants a positive amount. + ev.amount = (int32_t)(-damageInfo->out.m_actualDamageClipped + 0.5f); + ev.isBuilding = obj->isKindOf(KINDOF_STRUCTURE) ? 1 : 0; + ev.isUnit = (obj->isKindOf(KINDOF_INFANTRY) || obj->isKindOf(KINDOF_VEHICLE)) ? 1 : 0; + const Coord3D* pos = obj->getPosition(); + ev.positionX = (pos != nullptr) ? pos->x : 0.0f; + ev.positionY = (pos != nullptr) ? pos->y : 0.0f; + ev.positionZ = (pos != nullptr) ? pos->z : 0.0f; + GOPluginManager::DispatchObjectHealed(ev); + } } if (m_curDamageState != oldState) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp index 34d3ba6a126..d4f049063f3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/SpecialPower/SpecialPowerModule.cpp @@ -54,6 +54,8 @@ #include "GameClient/InGameUI.h" #include "GameClient/ControlBar.h" +#include "GameNetwork/GeneralsOnline/Plugins/PluginManager.h" + //------------------------------------------------------------------------------------------------- @@ -542,6 +544,20 @@ void SpecialPowerModule::markSpecialPowerTriggered( const Coord3D *location ) //------------------------------------------------------------------------------------------------- void SpecialPowerModule::aboutToDoSpecialPower( const Coord3D *location ) { + if (GOPluginManager::HasGameplayEventHooks()) + { + Player* player = getObject()->getControllingPlayer(); + GOSpecialPowerEvent ev = {}; + ev.playerIndex = (player != nullptr) ? (uint32_t)player->getPlayerIndex() : 0; + ev.powerTemplateName = getSpecialPowerModuleData()->m_specialPowerTemplate->getName().str(); + ev.locationX = (location != nullptr) ? location->x : getObject()->getPosition()->x; + ev.locationY = (location != nullptr) ? location->y : getObject()->getPosition()->y; + ev.locationZ = (location != nullptr) ? location->z : getObject()->getPosition()->z; + UnsignedInt reloadFrames = getSpecialPowerModuleData()->m_specialPowerTemplate->getReloadTime(); + ev.rechargeTimeSeconds = (reloadFrames > 0) ? ((float)reloadFrames / (float)LOGICFRAMES_PER_SECOND) : 0.0f; + GOPluginManager::DispatchSpecialPowerTriggered(ev); + } + // Tell the scripting engine! TheScriptEngine->notifyOfTriggeredSpecialPower( getObject()->getControllingPlayer()->getPlayerIndex(), diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp index e540ac948d9..27da696fc52 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ProductionUpdate.cpp @@ -55,9 +55,41 @@ #include "GameLogic/Object.h" #include "GameLogic/ScriptEngine.h" +#include "GameNetwork/GeneralsOnline/Plugins/PluginManager.h" + // PUBLIC ///////////////////////////////////////////////////////////////////////////////////////// +// C-ABI event payloads for the GOPluginManager::Dispatch* calls below. +static GOUnitEvent buildUnitEvent(Player* player, const ThingTemplate* unitType, Object* producer, Real percentComplete, ProductionID productionID) +{ + GOUnitEvent ev = {}; + ev.playerIndex = (player != nullptr) ? (uint32_t)player->getPlayerIndex() : 0; + ev.templateName = (unitType != nullptr) ? unitType->getName().str() : ""; + ev.producerObjectId = (producer != nullptr) ? (uint32_t)producer->getID() : 0; + ev.percentComplete = (float)percentComplete; + ev.productionID = (int32_t)productionID; + const Coord3D* pos = (producer != nullptr) ? producer->getPosition() : nullptr; + ev.producerPositionX = (pos != nullptr) ? pos->x : 0.0f; + ev.producerPositionY = (pos != nullptr) ? pos->y : 0.0f; + ev.producerPositionZ = (pos != nullptr) ? pos->z : 0.0f; + return ev; +} + +static GOUpgradeEvent buildUpgradeEvent(Player* player, const UpgradeTemplate* upgrade, Object* producer, Real percentComplete) +{ + GOUpgradeEvent ev = {}; + ev.playerIndex = (player != nullptr) ? (uint32_t)player->getPlayerIndex() : 0; + ev.templateName = (upgrade != nullptr) ? upgrade->getUpgradeName().str() : ""; + ev.producerObjectId = (producer != nullptr) ? (uint32_t)producer->getID() : 0; + ev.percentComplete = (float)percentComplete; + const Coord3D* pos = (producer != nullptr) ? producer->getPosition() : nullptr; + ev.producerPositionX = (pos != nullptr) ? pos->x : 0.0f; + ev.producerPositionY = (pos != nullptr) ? pos->y : 0.0f; + ev.producerPositionZ = (pos != nullptr) ? pos->z : 0.0f; + return ev; +} + static const ModelConditionFlagType theOpeningFlags[DOOR_COUNT_MAX] = { MODELCONDITION_DOOR_1_OPENING, @@ -311,6 +343,12 @@ Bool ProductionUpdate::queueUpgrade( const UpgradeTemplate *upgrade ) // tie to the end of the production queue addToProductionQueue( production ); + if (GOPluginManager::HasGameplayEventHooks()) + { + GOUpgradeEvent ev = buildUpgradeEvent(player, upgrade, getObject(), production->getPercentComplete()); + GOPluginManager::DispatchUpgradeQueued(ev); + } + // add this upgrade as in progress in the player player->addUpgrade( upgrade, UPGRADE_STATUS_IN_PRODUCTION ); @@ -359,6 +397,12 @@ void ProductionUpdate::cancelUpgrade( const UpgradeTemplate *upgrade ) Money *money = player->getMoney(); money->deposit( production->m_upgradeToResearch->calcCostToBuild( player ), TRUE, FALSE ); + if (GOPluginManager::HasGameplayEventHooks()) + { + GOUpgradeEvent ev = buildUpgradeEvent(player, production->m_upgradeToResearch, getObject(), production->getPercentComplete()); + GOPluginManager::DispatchUpgradeCancelled(ev); + } + // remove this production from the queue removeFromProductionQueue( production ); @@ -451,6 +495,12 @@ Bool ProductionUpdate::queueCreateUnit( const ThingTemplate *unitType, Productio // tie to the end of the production queue addToProductionQueue( production ); + if (GOPluginManager::HasGameplayEventHooks()) + { + GOUnitEvent ev = buildUnitEvent(player, unitType, getObject(), production->getPercentComplete(), productionID); + GOPluginManager::DispatchUnitQueued(ev); + } + return TRUE; // unit queued } @@ -475,6 +525,12 @@ void ProductionUpdate::cancelUnitCreate( ProductionID productionID ) Money *money = player->getMoney(); money->deposit( production->m_objectToProduce->calcCostToBuild( player ), TRUE, FALSE ); + if (GOPluginManager::HasGameplayEventHooks()) + { + GOUnitEvent ev = buildUnitEvent(player, production->m_objectToProduce, getObject(), production->getPercentComplete(), production->getProductionID()); + GOPluginManager::DispatchUnitCancelled(ev); + } + // remove from queue list removeFromProductionQueue( production ); @@ -852,6 +908,12 @@ UpdateSleepTime ProductionUpdate::update() creationBuilding->getControllingPlayer()->getAcademyStats()->recordProduction( newObj, creationBuilding ); + if (GOPluginManager::HasGameplayEventHooks()) + { + GOUnitEvent ev = buildUnitEvent(creationBuilding->getControllingPlayer(), production->m_objectToProduce, creationBuilding, 100.0f, production->getProductionID()); + GOPluginManager::DispatchUnitCompleted(ev); + } + //We created one guy, but we may want to do more so we should stay in this node of production. // This is last so the voice check can easily check for "first" guy production->oneProductionSuccessful(); @@ -967,6 +1029,12 @@ UpdateSleepTime ProductionUpdate::update() } } + if (GOPluginManager::HasGameplayEventHooks()) + { + GOUpgradeEvent ev = buildUpgradeEvent(player, upgrade, us, 100.0f); + GOPluginManager::DispatchUpgradeCompleted(ev); + } + // remove this production entry so we can go on to the next removeFromProductionQueue( production ); @@ -1126,6 +1194,20 @@ UnsignedInt ProductionUpdate::countUnitTypeInQueue( const ThingTemplate *unitTyp // ------------------------------------------------------------------------------------------------ void ProductionUpdate::onDie( const DamageInfo *damageInfo ) { + if (GOPluginManager::HasGameplayEventHooks()) + { + Object* us = getObject(); + GOBuildingEvent ev = {}; + ev.objectId = (us != nullptr) ? (uint32_t)us->getID() : 0; + Player* player = (us != nullptr) ? us->getControllingPlayer() : nullptr; + ev.playerIndex = (player != nullptr) ? (uint32_t)player->getPlayerIndex() : 0; + const Coord3D* pos = (us != nullptr) ? us->getPosition() : nullptr; + ev.positionX = (pos != nullptr) ? pos->x : 0.0f; + ev.positionY = (pos != nullptr) ? pos->y : 0.0f; + ev.positionZ = (pos != nullptr) ? pos->z : 0.0f; + GOPluginManager::DispatchBuildingDestroyed(ev); + } + // we need to cancel all of our production on death cancelAndRefundAllProduction(); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/Plugins/PluginManager.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/Plugins/PluginManager.cpp new file mode 100644 index 00000000000..1bed9f8cb63 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GeneralsOnline/Plugins/PluginManager.cpp @@ -0,0 +1,1557 @@ +#include "GameNetwork/GeneralsOnline/Plugins/PluginManager.h" +#include "GameNetwork/GeneralsOnline/NGMP_include.h" +#include "Common/GlobalData.h" +#include "Common/NameKeyGenerator.h" +#include "Common/Player.h" +#include "Common/PlayerList.h" +#include "Common/PlayerTemplate.h" +#include "Common/Science.h" +#include "Common/SpecialPower.h" +#include "Common/ThingTemplate.h" +#include "Common/ThingFactory.h" +#include "Common/Upgrade.h" +#include "Common/Geometry.h" +#include "GameClient/InGameUI.h" +#include "GameClient/ControlBar.h" +#include "GameClient/Display.h" +#include "GameClient/View.h" +#include "GameLogic/GameLogic.h" +#include "GameLogic/Object.h" +#include "GameLogic/Module/AIUpdate.h" +#include "GameLogic/Module/SpecialPowerModule.h" +#include "GameLogic/Module/ProductionUpdate.h" +#include "GameLogic/Module/ContainModule.h" +#include "GameLogic/Module/SupplyTruckAIUpdate.h" +#include "GameNetwork/NetworkDefs.h" // MAX_SLOTS +#include "GameNetwork/GameInfo.h" // GameSlot::getTeamNumber + +#include +#include // INT_MAX +#include +#include + +std::vector GOPluginManager::s_plugins; +std::vector GOPluginManager::s_gameplayEventHooks; +std::vector GOPluginManager::s_renderHooks; +GOPluginManager::NativeHandleProvider GOPluginManager::s_d3dDevice8Provider = nullptr; +GOPluginManager::NativeHandleProvider GOPluginManager::s_gameWindowProvider = nullptr; + +// The free functions the host API table points at. All dispatch is gated on +// IsLocalPlayerObserver(), so plugins only ever run for observers. +namespace +{ + void HostAPI_Log(const char* msg) + { + GOPluginManager::Log(msg); + } + + void HostAPI_RegisterGameplayEventHooks(const GOGameplayEventCallbacks* cb) + { + GOPluginManager::RegisterGameplayEventHooks(cb); + } + + void HostAPI_RegisterRenderHooks(const GORenderCallbacks* cb) + { + GOPluginManager::RegisterRenderHooks(cb); + } + + // ---- Player roster queries. Matches playerIndex against Player::getPlayerIndex(), not list + // position - those aren't guaranteed to be the same thing. ---- + + Player* FindPlayerByIndex(uint32_t playerIndex) + { + if (ThePlayerList == nullptr) + return nullptr; + Int count = ThePlayerList->getPlayerCount(); + for (Int i = 0; i < count; ++i) + { + Player* p = ThePlayerList->getNthPlayer(i); + if (p != nullptr && (uint32_t)p->getPlayerIndex() == playerIndex) + return p; + } + return nullptr; + } + + uint32_t HostAPI_GetPlayerColor(uint32_t playerIndex) + { + Player* p = FindPlayerByIndex(playerIndex); + return (p != nullptr) ? (uint32_t)p->getPlayerColor() : 0; + } + + // Walks the engine's player-slot name keys; the callback returns false to stop early. Not free + // per call - see plans\plugin-framework\design-notes.md. + typedef bool (*SlotPlayerCallback)(Int slot, Player* player, void* userData); + + void ForEachSlotPlayer(SlotPlayerCallback callback, void* userData) + { + if (callback == nullptr || ThePlayerList == nullptr || TheNameKeyGenerator == nullptr) + return; + + for (Int slot = 0; slot < MAX_SLOTS; ++slot) + { + AsciiString nameKeyStr; + nameKeyStr.format("player%d", slot); + + Player* p = ThePlayerList->findPlayerWithNameKey(TheNameKeyGenerator->nameToKey(nameKeyStr)); + if (p == nullptr) + continue; + if (!callback(slot, p, userData)) + return; + } + } + + struct SlotRosterContext + { + uint32_t* outPlayerIndices; + uint32_t maxCount; + uint32_t count; + bool activeOnly; // false keeps defeated/resigned players in the roster + }; + + bool CollectSlotPlayer(Int slot, Player* player, void* userData) + { + SlotRosterContext* ctx = (SlotRosterContext*)userData; + if (player->isPlayerObserver()) + return true; + if (ctx->activeOnly && !player->isPlayerActive()) + return true; + + ctx->outPlayerIndices[ctx->count++] = (uint32_t)player->getPlayerIndex(); + return ctx->count < ctx->maxCount; + } + + uint32_t HostAPI_GetActivePlayers(uint32_t* outPlayerIndices, uint32_t maxCount) + { + if (outPlayerIndices == nullptr || maxCount == 0) + return 0; + + SlotRosterContext ctx = { outPlayerIndices, maxCount, 0, true }; + ForEachSlotPlayer(CollectSlotPlayer, &ctx); + return ctx.count; + } + + // Mirrors the same check the engine's own observer-only UI uses (e.g. + // InGameUI's observer stats/notifications gating) - dead also counts as observing, since a dead + // player becomes spectator-like. Plugins that display information about other players must gate + // on this. + uint8_t HostAPI_IsLocalPlayerObserver() + { + return GOPluginManager::IsLocalPlayerObserver() ? 1 : 0; + } + + // General-power roster. A power counts as owned when the player has the required science and at + // least one live object carries its module - the same test the engine's own observer UI uses. + + struct PowerModuleFind + { + const SpecialPowerTemplate* powerTemplate; + SpecialPowerModuleInterface* module; + Object* object; // the STRUCTURE carrying the module, for GOGeneralPowerInfo::buildingObjectId + }; + + // Prefers a structure as the anchor but accepts any carrier - see + // plans\plugin-framework\design-notes.md. + static void FindPowerModule(Object* obj, void* userData) + { + if (obj == nullptr) + return; + PowerModuleFind* find = (PowerModuleFind*)userData; + // Step 1: a structure is the best possible anchor, so stop once one has been found. + if (find->object != nullptr) + return; + + SpecialPowerModuleInterface* module = obj->getSpecialPowerModule(find->powerTemplate); + if (module == nullptr) + return; + + // Step 2: a structure wins outright and also becomes the reported building. + if (obj->isKindOf(KINDOF_STRUCTURE)) + { + find->module = module; + find->object = obj; + return; + } + + // Step 3: otherwise keep the first carrier of any kind, with no building to anchor to. + if (find->module == nullptr) + find->module = module; + } + + uint32_t HostAPI_GetPlayerGeneralPowers(uint32_t playerIndex, GOGeneralPowerInfo* outPowers, uint32_t maxCount) + { + if (outPowers == nullptr || maxCount == 0) + return 0; + if (ThePlayerList == nullptr || TheControlBar == nullptr) + return 0; + + Player* player = FindPlayerByIndex(playerIndex); + if (player == nullptr) + return 0; + + const PlayerTemplate* pt = player->getPlayerTemplate(); + if (pt == nullptr) + return 0; + AsciiString cmdSetName = pt->getSpecialPowerShortcutCommandSet(); + if (cmdSetName.isEmpty()) + return 0; + + const CommandSet* cmdSet = TheControlBar->findCommandSet(cmdSetName); + if (cmdSet == nullptr) + return 0; + + uint32_t count = 0; + for (Int i = 0; i < MAX_COMMANDS_PER_SET && count < maxCount; ++i) + { + const CommandButton* btn = cmdSet->getCommandButton(i); + if (btn == nullptr) + continue; + const SpecialPowerTemplate* sp = btn->getSpecialPowerTemplate(); + if (sp == nullptr) + continue; + + ScienceType required = sp->getRequiredScience(); + if (required != SCIENCE_INVALID && !player->hasScience(required)) + continue; + + PowerModuleFind find; + find.powerTemplate = sp; + find.module = nullptr; + find.object = nullptr; + player->iterateObjects(FindPowerModule, &find); + if (find.module == nullptr) + continue; + + GOGeneralPowerInfo& info = outPowers[count]; + info.templateName = sp->getName().str(); + info.rechargeFrames = sp->getReloadTime(); + info.buildingObjectId = (find.object != nullptr) ? (uint32_t)find.object->getID() : 0; + const UnsignedInt readyFrame = find.module->getReadyFrame(); + const UnsignedInt now = (TheGameLogic != nullptr) ? (UnsignedInt)TheGameLogic->getFrame() : 0; + info.framesUntilReady = (readyFrame > now) ? (readyFrame - now) : 0; + ++count; + } + return count; + } + + // ---- Icon drawing. Only valid to call from within a GORenderCallbacks::onDrawOverlay + // callback - same 2D-context requirement as drawText2D/drawRect2D. ---- + + void HostAPI_DrawTemplateIcon2D(const char* templateName, int32_t x, int32_t y, int32_t width, int32_t height) + { + if (templateName == nullptr || TheDisplay == nullptr) + return; + + const Image* img = nullptr; + if (TheThingFactory != nullptr) + { + const ThingTemplate* tmpl = TheThingFactory->findTemplate(AsciiString(templateName), FALSE); + if (tmpl != nullptr) + img = tmpl->getButtonImage(); + } + if (img == nullptr && TheUpgradeCenter != nullptr) + { + const UpgradeTemplate* upgrade = TheUpgradeCenter->findUpgrade(templateName); + if (upgrade != nullptr) + img = upgrade->getButtonImage(); + } + if (img != nullptr) + TheDisplay->drawImage(img, x, y, x + width, y + height); + } + + void HostAPI_DrawPowerIcon2D(uint32_t playerIndex, const char* powerTemplateName, int32_t x, int32_t y, int32_t width, int32_t height) + { + if (powerTemplateName == nullptr || TheDisplay == nullptr || TheControlBar == nullptr) + return; + + Player* p = FindPlayerByIndex(playerIndex); + if (p == nullptr) + return; + + const PlayerTemplate* pt = p->getPlayerTemplate(); + if (pt == nullptr) + return; + + AsciiString cmdSetName = pt->getSpecialPowerShortcutCommandSet(); + if (cmdSetName.isEmpty()) + return; + + const CommandSet* cmdSet = TheControlBar->findCommandSet(cmdSetName); + if (cmdSet == nullptr) + return; + + for (Int i = 0; i < MAX_COMMANDS_PER_SET; ++i) + { + const CommandButton* btn = cmdSet->getCommandButton(i); + if (btn == nullptr || btn->getSpecialPowerTemplate() == nullptr) + continue; + if (strcmp(btn->getSpecialPowerTemplate()->getName().str(), powerTemplateName) != 0) + continue; + + const Image* img = btn->getButtonImage(); + if (img != nullptr) + TheDisplay->drawImage(img, x, y, x + width, y + height); + return; + } + } + + void HostAPI_DrawText2D(int32_t x, int32_t y, const char* asciiText, uint32_t colorARGB) + { + if (TheInGameUI != nullptr) + TheInGameUI->drawPluginText2D((Int)x, (Int)y, asciiText, (Color)colorARGB); + } + + void HostAPI_DrawText2DScaled(int32_t x, int32_t y, const char* asciiText, uint32_t colorARGB, float sizeScale, uint8_t bold) + { + if (TheInGameUI != nullptr) + TheInGameUI->drawPluginText2DScaled((Int)x, (Int)y, asciiText, (Color)colorARGB, (Real)sizeScale, bold != 0); + } + + void HostAPI_DrawRect2D(int32_t x, int32_t y, int32_t width, int32_t height, uint32_t colorARGB, uint8_t filled) + { + if (TheDisplay == nullptr) + return; + + if (filled != 0) + TheDisplay->drawFillRect((Int)x, (Int)y, (Int)width, (Int)height, (Color)colorARGB); + else + TheDisplay->drawOpenRect((Int)x, (Int)y, (Int)width, (Int)height, 1.0f, (Color)colorARGB); + } + + // One rotated quad however long the line is, unlike DrawRect2D which is axis-aligned. + void HostAPI_DrawLine2D(int32_t x1, int32_t y1, int32_t x2, int32_t y2, float thickness, uint32_t colorARGB) + { + if (TheDisplay == nullptr) + return; + + TheDisplay->drawLine((Int)x1, (Int)y1, (Int)x2, (Int)y2, (Real)thickness, (Color)colorARGB); + } + + void HostAPI_GetScreenSize(int32_t* outWidth, int32_t* outHeight) + { + UnsignedInt w = (TheDisplay != nullptr) ? TheDisplay->getWidth() : 0; + UnsignedInt h = (TheDisplay != nullptr) ? TheDisplay->getHeight() : 0; + if (outWidth != nullptr) + *outWidth = (int32_t)w; + if (outHeight != nullptr) + *outHeight = (int32_t)h; + } + + // ---- Simulation clock. ---- + + uint32_t HostAPI_GetLogicFrame() + { + return (TheGameLogic != nullptr) ? (uint32_t)TheGameLogic->getFrame() : 0; + } + + uint32_t HostAPI_GetLogicFramesPerSecond() + { + return (uint32_t)LOGICFRAMES_PER_SECOND; + } + + // ---- Live production progress. Reads the producer's own queue, so the value is whatever the + // simulation currently believes rather than a plugin-side reconstruction. ---- + + ProductionUpdateInterface* FindProducer(uint32_t producerObjectId) + { + if (TheGameLogic == nullptr || producerObjectId == 0) + return nullptr; + Object* producer = TheGameLogic->findObjectByID((ObjectID)producerObjectId); + return (producer != nullptr) ? producer->getProductionUpdateInterface() : nullptr; + } + + float HostAPI_GetUnitProductionProgress(uint32_t producerObjectId, int32_t productionID) + { + ProductionUpdateInterface* production = FindProducer(producerObjectId); + if (production == nullptr) + return -1.0f; + + for (const ProductionEntry* entry = production->firstProduction(); entry != nullptr; entry = production->nextProduction(entry)) + { + if (entry->getProductionType() == PRODUCTION_UNIT && (int32_t)entry->getProductionID() == productionID) + return (float)entry->getPercentComplete(); + } + return -1.0f; + } + + float HostAPI_GetUpgradeProductionProgress(uint32_t producerObjectId, const char* upgradeTemplateName) + { + if (upgradeTemplateName == nullptr) + return -1.0f; + + ProductionUpdateInterface* production = FindProducer(producerObjectId); + if (production == nullptr) + return -1.0f; + + for (const ProductionEntry* entry = production->firstProduction(); entry != nullptr; entry = production->nextProduction(entry)) + { + if (entry->getProductionType() != PRODUCTION_UPGRADE) + continue; + const UpgradeTemplate* upgrade = entry->getProductionUpgrade(); + if (upgrade != nullptr && strcmp(upgrade->getUpgradeName().str(), upgradeTemplateName) == 0) + return (float)entry->getPercentComplete(); + } + return -1.0f; + } + + // ---- World-space anchoring. ---- + + uint8_t HostAPI_WorldToScreen(float worldX, float worldY, float worldZ, int32_t* outX, int32_t* outY) + { + if (TheTacticalView == nullptr) + return 0; + + Coord3D world; + world.x = worldX; + world.y = worldY; + world.z = worldZ; + + ICoord2D screen; + if (!TheTacticalView->worldToScreen(&world, &screen)) + return 0; + + if (outX != nullptr) + *outX = (int32_t)screen.x; + if (outY != nullptr) + *outY = (int32_t)screen.y; + return 1; + } + + // Edge inset for clamped off-screen indicators, as a fraction of view height so it scales with + // resolution (0.0333 is the original 24 pixels at 720p). + const float kEdgeIndicatorMarginFraction = 0.0333f; + + // Like HostAPI_WorldToScreen, but clamps an off-screen point to the view edge instead of + // dropping it - see plans\plugin-framework\design-notes.md. + uint8_t HostAPI_WorldToScreenClamped(float worldX, float worldY, float worldZ, int32_t* outX, int32_t* outY) + { + if (TheTacticalView == nullptr) + return 0; + + Coord3D world; + world.x = worldX; + world.y = worldY; + world.z = worldZ; + + // Step 1: project, accepting a point beyond the far clip plane as well as an off-frustum one. + ICoord2D screen; + const View::WorldToScreenReturn result = TheTacticalView->worldToScreenTriReturnAllowFarClip(&world, &screen); + if (result == View::WTS_INVALID) + return 0; + + if (result == View::WTS_OUTSIDE_FRUSTUM) + { + // Step 2: clamp against the tactical view's own rectangle, which is not always the + // whole display - the projected position is already in full-display coordinates. + Int originX = 0; + Int originY = 0; + TheTacticalView->getOrigin(&originX, &originY); + const float viewW = (float)TheTacticalView->getWidth(); + const float viewH = (float)TheTacticalView->getHeight(); + const float margin = viewH * kEdgeIndicatorMarginFraction; + if (viewW > margin * 2.0f && viewH > margin * 2.0f) + { + // Step 3: clamp along the ray from the view's centre to the raw (possibly far + // off-screen) projected point, so the indicator sits on the nearest edge. + const float cx = (float)originX + viewW * 0.5f; + const float cy = (float)originY + viewH * 0.5f; + float dx = (float)screen.x - cx; + float dy = (float)screen.y - cy; + const float halfW = viewW * 0.5f - margin; + const float halfH = viewH * 0.5f - margin; + const float absDx = (dx < 0.0f) ? -dx : dx; + const float absDy = (dy < 0.0f) ? -dy : dy; + float scale = 1.0f; + if (absDx > halfW && absDx > 0.0f) + scale = halfW / absDx; + if (absDy > halfH && absDy > 0.0f) + { + const float scaleY = halfH / absDy; + if (scaleY < scale) + scale = scaleY; + } + if (scale < 1.0f) + { + dx *= scale; + dy *= scale; + } + screen.x = (Int)(cx + dx); + screen.y = (Int)(cy + dy); + } + } + + if (outX != nullptr) + *outX = (int32_t)screen.x; + if (outY != nullptr) + *outY = (int32_t)screen.y; + return 1; + } + + // Classifies by the same KindOf flags the engine's own faction-agnostic code uses, so no + // per-faction cases are needed - see plans\plugin-framework\design-notes.md. + uint8_t HostAPI_GetObjectBuildingCategory(uint32_t objectId) + { + if (TheGameLogic == nullptr || objectId == 0) + return GO_BUILDING_CATEGORY_NONE; + + Object* obj = TheGameLogic->findObjectByID((ObjectID)objectId); + if (obj == nullptr || !obj->isKindOf(KINDOF_STRUCTURE)) + return GO_BUILDING_CATEGORY_NONE; + + if (obj->isKindOf(KINDOF_COMMANDCENTER)) + return GO_BUILDING_CATEGORY_COMMAND_CENTER; + if (obj->isKindOf(KINDOF_FS_WARFACTORY)) + return GO_BUILDING_CATEGORY_WAR_FACTORY; + if (obj->isKindOf(KINDOF_FS_BARRACKS)) + return GO_BUILDING_CATEGORY_BARRACKS; + if (obj->isKindOf(KINDOF_FS_AIRFIELD)) + return GO_BUILDING_CATEGORY_AIRFIELD; + if (obj->isKindOf(KINDOF_FS_SUPPLY_CENTER)) + return GO_BUILDING_CATEGORY_SUPPLY_STASH; + return GO_BUILDING_CATEGORY_NONE; + } + + uint8_t HostAPI_GetObjectScreenBounds(uint32_t objectId, int32_t* outX, int32_t* outY, int32_t* outWidth, int32_t* outHeight) + { + if (TheGameLogic == nullptr || TheTacticalView == nullptr || objectId == 0) + return 0; + + Object* obj = TheGameLogic->findObjectByID((ObjectID)objectId); + if (obj == nullptr) + return 0; + + const Coord3D* pos = obj->getPosition(); + if (pos == nullptr) + return 0; + + ICoord2D centre; + if (!TheTacticalView->worldToScreen(pos, ¢re)) + return 0; + + // Project a point one bounding radius to the side of the object as well; the horizontal + // distance between the two projections is the object's on-screen size, which already + // accounts for camera zoom and pitch without exposing any camera state to the plugin. + const Real radius = obj->getGeometryInfo().getBoundingCircleRadius(); + Coord3D edgeWorld = *pos; + edgeWorld.x += radius; + + ICoord2D edge; + Int halfWidth = 0; + if (TheTacticalView->worldToScreen(&edgeWorld, &edge)) + halfWidth = abs(edge.x - centre.x); + if (halfWidth <= 0) + halfWidth = 1; + + if (outX != nullptr) + *outX = (int32_t)(centre.x - halfWidth); + if (outY != nullptr) + *outY = (int32_t)(centre.y - halfWidth); + if (outWidth != nullptr) + *outWidth = (int32_t)(halfWidth * 2); + if (outHeight != nullptr) + *outHeight = (int32_t)(halfWidth * 2); + return 1; + } + + // Projects the same point the engine draws its own health bar at - see plans\plugin-framework\design-notes.md. + uint8_t HostAPI_GetObjectHealthBarScreenPosition(uint32_t objectId, int32_t* outX, int32_t* outY) + { + if (TheGameLogic == nullptr || TheTacticalView == nullptr || objectId == 0) + return 0; + + Object* obj = TheGameLogic->findObjectByID((ObjectID)objectId); + if (obj == nullptr) + return 0; + + Coord3D pos; + obj->getHealthBoxPosition(pos); + + ICoord2D screen; + if (!TheTacticalView->worldToScreen(&pos, &screen)) + return 0; + + if (outX != nullptr) + *outX = (int32_t)screen.x; + if (outY != nullptr) + *outY = (int32_t)screen.y; + return 1; + } + + // Camera control. Same look-at path the engine's own observer actions + // use, so a plugin can jump the viewport to where a gameplay event happened when the user + // clicks on it. + + void HostAPI_TeleportViewportTo(float worldX, float worldY, float worldZ) + { + if (TheTacticalView == nullptr) + return; + Coord3D target; + target.x = worldX; + target.y = worldY; + target.z = worldZ; + TheTacticalView->userLookAt(&target); + } + + // ---- Clock-wedge draw primitives. Same 2D-context requirement as drawText2D/drawRect2D. ---- + + void HostAPI_DrawRectClock2D(int32_t x, int32_t y, int32_t width, int32_t height, int32_t percent, uint32_t colorARGB) + { + if (TheDisplay != nullptr) + TheDisplay->drawRectClock((Int)x, (Int)y, (Int)width, (Int)height, (Int)percent, (UnsignedInt)colorARGB); + } + + void HostAPI_DrawRemainingRectClock2D(int32_t x, int32_t y, int32_t width, int32_t height, int32_t percent, uint32_t colorARGB) + { + if (TheDisplay != nullptr) + TheDisplay->drawRemainingRectClock((Int)x, (Int)y, (Int)width, (Int)height, (Int)percent, (UnsignedInt)colorARGB); + } + + // Both handles come from the device layer through GOPluginManager's provider seam. + + void* HostAPI_GetD3DDevice8() + { + return GOPluginManager::GetD3DDevice8(); + } + + void* HostAPI_GetGameWindow() + { + return GOPluginManager::GetGameWindow(); + } + + struct EnumerateObjectsContext + { + void (*callback)(uint32_t, float, float, float, void*); + void* userData; + uint32_t count; + }; + + void EnumerateObjectsCallback(Object* object, void* userData) + { + EnumerateObjectsContext* context = (EnumerateObjectsContext*)userData; + if (object == nullptr || context->callback == nullptr) + return; + const Coord3D* position = object->getPosition(); + if (position == nullptr) + return; + context->callback((uint32_t)object->getID(), position->x, position->y, position->z, context->userData); + ++context->count; + } + + uint32_t HostAPI_EnumeratePlayerObjects(uint32_t playerIndex, + void (*callback)(uint32_t, float, float, float, void*), void* userData) + { + Player* player = FindPlayerByIndex(playerIndex); + if (player == nullptr || callback == nullptr) + return 0; + EnumerateObjectsContext context = { callback, userData, 0 }; + player->iterateObjects(EnumerateObjectsCallback, &context); + return context.count; + } + + uint32_t HostAPI_GetContainedObjects(uint32_t containerObjectId, + GOContainedObjectInfo* outObjects, uint32_t maxCount) + { + if (TheGameLogic == nullptr || containerObjectId == 0 || outObjects == nullptr || maxCount == 0) + return 0; + Object* container = TheGameLogic->findObjectByID((ObjectID)containerObjectId); + if (container == nullptr || container->getContain() == nullptr) + return 0; + const ContainedItemsList* items = container->getContain()->getContainedItemsList(); + if (items == nullptr) + return 0; + uint32_t count = 0; + for (ContainedItemsList::const_iterator it = items->begin(); it != items->end() && count < maxCount; ++it) + { + Object* object = *it; + if (object == nullptr || object->getTemplate() == nullptr) + continue; + GOContainedObjectInfo& info = outObjects[count++]; + info.objectId = (uint32_t)object->getID(); + info.templateName = object->getTemplate()->getName().str(); + info.playerIndex = object->getControllingPlayer() != nullptr + ? (uint32_t)object->getControllingPlayer()->getPlayerIndex() : 0; + } + return count; + } + + uint8_t HostAPI_GetObjectTargetPosition(uint32_t objectId, float* outX, float* outY, float* outZ) + { + if (TheGameLogic == nullptr || objectId == 0) + return 0; + + Object* obj = TheGameLogic->findObjectByID((ObjectID)objectId); + if (obj == nullptr) + return 0; + + AIUpdateInterface* ai = obj->getAIUpdateInterface(); + if (ai == nullptr) + return 0; + + // Attacking a specific object: point at that object's current position. + Object* victim = ai->getCurrentVictim(); + if (victim != nullptr) + { + const Coord3D* pos = victim->getPosition(); + if (pos != nullptr) + { + if (outX != nullptr) *outX = pos->x; + if (outY != nullptr) *outY = pos->y; + if (outZ != nullptr) *outZ = pos->z; + return 1; + } + } + + // A goal position only counts for an explicit attack order, never a move-to or guard - see + // plans\plugin-framework\design-notes.md. + const StateID state = ai->getCurrentStateID(); + if (state != AI_ATTACK_MOVE_TO && state != AI_ATTACK_POSITION) + return 0; + + const Coord3D* goal = ai->getGoalPosition(); + if (goal == nullptr) + return 0; + + if (outX != nullptr) *outX = goal->x; + if (outY != nullptr) *outY = goal->y; + if (outZ != nullptr) *outZ = goal->z; + return 1; + } + + uint8_t HostAPI_IsObjectAirborne(uint32_t objectId) + { + if (TheGameLogic == nullptr || objectId == 0) + return 0; + Object* obj = TheGameLogic->findObjectByID((ObjectID)objectId); + return (obj != nullptr && obj->isAirborneTarget()) ? 1 : 0; + } + + uint8_t HostAPI_IsObjectVehicle(uint32_t objectId) + { + if (TheGameLogic == nullptr || objectId == 0) + return 0; + Object* obj = TheGameLogic->findObjectByID((ObjectID)objectId); + return (obj != nullptr && obj->isKindOf(KINDOF_VEHICLE)) ? 1 : 0; + } + + uint8_t HostAPI_IsObjectProjectile(uint32_t objectId) + { + if (TheGameLogic == nullptr || objectId == 0) + return 0; + Object* obj = TheGameLogic->findObjectByID((ObjectID)objectId); + return (obj != nullptr && obj->isKindOf(KINDOF_PROJECTILE)) ? 1 : 0; + } + + uint32_t HostAPI_GetObjectContainerId(uint32_t objectId) + { + if (TheGameLogic == nullptr || objectId == 0) + return 0; + Object* obj = TheGameLogic->findObjectByID((ObjectID)objectId); + if (obj == nullptr) + return 0; + const Object* container = obj->getContainedBy(); + return (container != nullptr) ? (uint32_t)container->getID() : 0; + } + + // ---- Player card queries (Plan 5). Same FindPlayerByIndex resolution as every other + // per-player query above. ---- + + // Each of the string-returning queries below owns one distinct function-local buffer, so only + // its own most recent return value is live - see plans\plugin-framework\design-notes.md. + const char* HostAPI_GetPlayerName(uint32_t playerIndex) + { + Player* p = FindPlayerByIndex(playerIndex); + if (p == nullptr) + return ""; + // getPlayerDisplayName() is a UnicodeString; the ABI only carries UTF-8/ASCII. + static AsciiString s_playerName; + s_playerName.translate(p->getPlayerDisplayName()); + return s_playerName.str(); + } + + const char* HostAPI_GetPlayerFactionTemplate(uint32_t playerIndex) + { + Player* p = FindPlayerByIndex(playerIndex); + if (p == nullptr) + return ""; + // getSide() is the template's "Side" INI field ("AmericaLaserGeneral"), not + // getPlayerTemplate()->getName(), which carries an unreadable "Faction" prefix. + return p->getSide().str(); + } + + uint32_t HostAPI_GetPlayerMoney(uint32_t playerIndex) + { + Player* p = FindPlayerByIndex(playerIndex); + return (p != nullptr) ? (uint32_t)p->getMoney()->countMoney() : 0; + } + + // Both skill-point figures are absolute totals, so they are already a progress bar's numerator + // and denominator - see plans\plugin-framework\design-notes.md. + int32_t HostAPI_GetPlayerRank(uint32_t playerIndex, uint32_t* outCurrentXP, uint32_t* outNextXP) + { + Player* p = FindPlayerByIndex(playerIndex); + if (p == nullptr) + return -1; + const Int rankLevel = p->getRankLevel(); + if (rankLevel <= 0) + return -1; + if (outCurrentXP != nullptr) + *outCurrentXP = (uint32_t)p->getSkillPoints(); + if (outNextXP != nullptr) + { + // At the rank cap there is no next rank, and the threshold is INT_MAX - report 0 + // rather than a meaningless huge denominator. + const Int levelUp = p->getSkillPointsLevelUp(); + *outNextXP = (levelUp >= INT_MAX) ? 0 : (uint32_t)levelUp; + } + return (int32_t)(rankLevel - 1); + } + + // getEnergy(), like getMoney() and getScoreKeeper(), returns the address of a Player member and + // is never null once the Player itself resolved, so none of the three is null-checked. + uint8_t HostAPI_GetPlayerPowerState(uint32_t playerIndex, uint32_t* outPowerGenerated, uint32_t* outPowerDrain) + { + Player* p = FindPlayerByIndex(playerIndex); + if (p == nullptr) + return 0; + const Energy* energy = p->getEnergy(); + if (outPowerGenerated != nullptr) + *outPowerGenerated = (uint32_t)energy->getProduction(); + if (outPowerDrain != nullptr) + *outPowerDrain = (uint32_t)energy->getConsumption(); + return 1; + } + + struct CountContext { uint32_t count; }; + + // A builder is anything carrying a DozerAIInterface, which covers all three factions - see + // plans\plugin-framework\design-notes.md. + void CountBuilderCallback(Object* obj, void* userData) + { + if (obj == nullptr) + return; + AIUpdateInterface* ai = obj->getAIUpdateInterface(); + if (ai != nullptr && ai->getDozerAIInterface() != nullptr) + ((CountContext*)userData)->count++; + } + + uint32_t HostAPI_GetPlayerBuilderCount(uint32_t playerIndex) + { + Player* p = FindPlayerByIndex(playerIndex); + if (p == nullptr) + return 0; + CountContext ctx = { 0 }; + p->iterateObjects(CountBuilderCallback, &ctx); + return ctx.count; + } + + // Counts units actually working the supply line right now, not every supply unit - see + // plans\plugin-framework\design-notes.md. + void CountActiveGathererCallback(Object* obj, void* userData) + { + if (obj == nullptr) + return; + AIUpdateInterface* ai = obj->getAIUpdateInterface(); + if (ai == nullptr) + return; + SupplyTruckAIInterface* supplyAI = ai->getSupplyTruckAIInterface(); + if (supplyAI != nullptr && supplyAI->isCurrentlyFerryingSupplies()) + ((CountContext*)userData)->count++; + } + + uint32_t HostAPI_GetPlayerActiveGathererCount(uint32_t playerIndex) + { + Player* p = FindPlayerByIndex(playerIndex); + if (p == nullptr) + return 0; + CountContext ctx = { 0 }; + p->iterateObjects(CountActiveGathererCallback, &ctx); + return ctx.count; + } + + // The score screen's cumulative figure, deliberately raw rather than a rate - see plans\plugin-framework\design-notes.md. + uint32_t HostAPI_GetPlayerTotalMoneyEarned(uint32_t playerIndex) + { + Player* p = FindPlayerByIndex(playerIndex); + if (p == nullptr) + return 0; + return (uint32_t)p->getScoreKeeper()->getTotalMoneyEarned(); + } + + // Looks up by raw template name rather than a live object, so it also resolves a template a + // plugin only knows the name of (e.g. GOContainedObjectInfo::templateName from a garrison query). + const char* HostAPI_GetTemplateDisplayName(const char* templateName) + { + if (templateName == nullptr || templateName[0] == '\0' || TheThingFactory == nullptr) + return ""; + const ThingTemplate* tt = TheThingFactory->findTemplate(AsciiString(templateName)); + if (tt == nullptr) + return ""; + static AsciiString s_templateDisplayName; + s_templateDisplayName.translate(tt->getDisplayName()); + return s_templateDisplayName.str(); + } + + struct SampleTemplateContext { AsciiString templateName; bool found; }; + + // Same qualifying check as CountBuilderCallback, but stops recording once one match is found - + // this only needs a representative icon, not an exact count. + void SampleBuilderTemplateCallback(Object* obj, void* userData) + { + SampleTemplateContext* ctx = (SampleTemplateContext*)userData; + if (ctx->found || obj == nullptr) + return; + AIUpdateInterface* ai = obj->getAIUpdateInterface(); + if (ai != nullptr && ai->getDozerAIInterface() != nullptr && obj->getTemplate() != nullptr) + { + ctx->templateName = obj->getTemplate()->getName(); + ctx->found = true; + } + } + + // Same qualifying check as CountActiveGathererCallback (isCurrentlyFerryingSupplies, not merely + // "is a supply unit"), so the sampled icon matches what getPlayerActiveGathererCount is counting. + void SampleGathererTemplateCallback(Object* obj, void* userData) + { + SampleTemplateContext* ctx = (SampleTemplateContext*)userData; + if (ctx->found || obj == nullptr) + return; + AIUpdateInterface* ai = obj->getAIUpdateInterface(); + if (ai == nullptr) + return; + SupplyTruckAIInterface* supplyAI = ai->getSupplyTruckAIInterface(); + if (supplyAI != nullptr && supplyAI->isCurrentlyFerryingSupplies() && obj->getTemplate() != nullptr) + { + ctx->templateName = obj->getTemplate()->getName(); + ctx->found = true; + } + } + + const char* HostAPI_GetPlayerBuilderTemplateName(uint32_t playerIndex) + { + Player* p = FindPlayerByIndex(playerIndex); + if (p == nullptr) + return ""; + SampleTemplateContext ctx = {}; + p->iterateObjects(SampleBuilderTemplateCallback, &ctx); + static AsciiString s_builderTemplateName; + s_builderTemplateName = ctx.templateName; + return s_builderTemplateName.str(); + } + + // The base game has three distinct DozerAIInterface-carrying templates (USA Dozer, China Dozer, + // GLA Worker); the extra headroom is for mods that add their own - see + // plans\plugin-framework\design-notes.md. + const uint32_t kMaxBuilderTypeBuckets = 8; + + struct BuilderTypeBucket { AsciiString name; uint32_t count; }; + struct BuilderTypeCountContext + { + BuilderTypeBucket buckets[kMaxBuilderTypeBuckets]; + uint32_t bucketCount = 0; + }; + + // As CountBuilderCallback, but keyed by template so a captured Worker is not collapsed into a + // native Dozer - see plans\plugin-framework\design-notes.md. + void CountBuilderTypeCallback(Object* obj, void* userData) + { + if (obj == nullptr) + return; + AIUpdateInterface* ai = obj->getAIUpdateInterface(); + if (ai == nullptr || ai->getDozerAIInterface() == nullptr) + return; + const ThingTemplate* tt = obj->getTemplate(); + if (tt == nullptr) + return; + + BuilderTypeCountContext* ctx = (BuilderTypeCountContext*)userData; + const AsciiString& name = tt->getName(); + for (uint32_t i = 0; i < ctx->bucketCount; ++i) + { + if (ctx->buckets[i].name == name) + { + ctx->buckets[i].count++; + return; + } + } + if (ctx->bucketCount < kMaxBuilderTypeBuckets) + { + ctx->buckets[ctx->bucketCount].name = name; + ctx->buckets[ctx->bucketCount].count = 1; + ctx->bucketCount++; + } + } + + // Per-type breakdown of the player's live builder units - see PluginABI.h for the contract. + uint32_t HostAPI_GetPlayerBuilderTemplateCounts(uint32_t playerIndex, const char** outNames, uint32_t* outCounts, uint32_t maxCount) + { + if (outNames == nullptr || outCounts == nullptr || maxCount == 0) + return 0; + Player* p = FindPlayerByIndex(playerIndex); + if (p == nullptr) + return 0; + + BuilderTypeCountContext ctx; + p->iterateObjects(CountBuilderTypeCallback, &ctx); + + // This function's own backing storage for the returned pointers. + static AsciiString s_builderTemplateNames[kMaxBuilderTypeBuckets]; + const uint32_t n = (ctx.bucketCount < maxCount) ? ctx.bucketCount : maxCount; + for (uint32_t i = 0; i < n; ++i) + { + s_builderTemplateNames[i] = ctx.buckets[i].name; + outNames[i] = s_builderTemplateNames[i].str(); + outCounts[i] = ctx.buckets[i].count; + } + return n; + } + + const char* HostAPI_GetPlayerGathererTemplateName(uint32_t playerIndex) + { + Player* p = FindPlayerByIndex(playerIndex); + if (p == nullptr) + return ""; + SampleTemplateContext ctx = {}; + p->iterateObjects(SampleGathererTemplateCallback, &ctx); + static AsciiString s_gathererTemplateName; + s_gathererTemplateName = ctx.templateName; + return s_gathererTemplateName.str(); + } + + struct SlotLookupContext { uint32_t playerIndex; Int slot; }; + + bool MatchSlotByPlayerIndex(Int slot, Player* player, void* userData) + { + SlotLookupContext* ctx = (SlotLookupContext*)userData; + if ((uint32_t)player->getPlayerIndex() != ctx->playerIndex) + return true; + ctx->slot = slot; + return false; + } + + // playerIndex (Player::getPlayerIndex()) and the lobby slot index GameSlot is keyed by are two + // different numbering spaces, so the slot has to be found first and then looked up in TheGameInfo. + int32_t HostAPI_GetPlayerTeamNumber(uint32_t playerIndex) + { + if (TheGameInfo == nullptr) + return -1; + + SlotLookupContext ctx = { playerIndex, -1 }; + ForEachSlotPlayer(MatchSlotByPlayerIndex, &ctx); + if (ctx.slot < 0) + return -1; + + const GameSlot* gameSlot = TheGameInfo->getConstSlot(ctx.slot); + return (gameSlot != nullptr) ? gameSlot->getTeamNumber() : -1; + } + + // As HostAPI_GetActivePlayers but without the isPlayerActive() filter, so the roster is stable + // for the whole match - see plans\plugin-framework\design-notes.md. + uint32_t HostAPI_GetMatchPlayers(uint32_t* outPlayerIndices, uint32_t maxCount) + { + if (outPlayerIndices == nullptr || maxCount == 0) + return 0; + + SlotRosterContext ctx = { outPlayerIndices, maxCount, 0, false }; + ForEachSlotPlayer(CollectSlotPlayer, &ctx); + return ctx.count; + } + + uint8_t HostAPI_GetPlayerIsDefeated(uint32_t playerIndex) + { + Player* p = FindPlayerByIndex(playerIndex); + return (p != nullptr && !p->isPlayerActive()) ? 1 : 0; + } + + const char* HostAPI_GetUserDataPath() + { + // Cached in a function-local static because the ABI promises a pointer that stays valid for + // the process lifetime, while getPath_UserData() hands back a temporary. + static std::string s_userDataPath; + if (s_userDataPath.empty() && TheGlobalData != nullptr) + s_userDataPath = TheGlobalData->getPath_UserData().str(); + return s_userDataPath.c_str(); + } +} // anonymous namespace + +// ------------------------------------------------------------------------------------------------ + +void GOPluginManager::Log(const char* msg) +{ + NetworkLog(ELogVerbosity::LOG_RELEASE, "[Plugin] %s", msg); +} + +// Called by GameEngineDevice once its display is up, before any plugin is loaded. +void GOPluginManager::SetNativeHandleProviders(NativeHandleProvider d3dDevice8, NativeHandleProvider gameWindow) +{ + s_d3dDevice8Provider = d3dDevice8; + s_gameWindowProvider = gameWindow; +} + +void* GOPluginManager::GetD3DDevice8() +{ + return (s_d3dDevice8Provider != nullptr) ? s_d3dDevice8Provider() : nullptr; +} + +void* GOPluginManager::GetGameWindow() +{ + return (s_gameWindowProvider != nullptr) ? s_gameWindowProvider() : nullptr; +} + +GOPluginHostAPI GOPluginManager::BuildHostAPI() +{ + GOPluginHostAPI api = {}; + api.abiVersion = GO_PLUGIN_ABI_VERSION; + api.structSize = (uint32_t)sizeof(GOPluginHostAPI); + api.log = HostAPI_Log; + api.registerGameplayEventHooks = HostAPI_RegisterGameplayEventHooks; + api.registerRenderHooks = HostAPI_RegisterRenderHooks; + api.getPlayerColor = HostAPI_GetPlayerColor; + api.getActivePlayers = HostAPI_GetActivePlayers; + api.isLocalPlayerObserver = HostAPI_IsLocalPlayerObserver; + api.getPlayerGeneralPowers = HostAPI_GetPlayerGeneralPowers; + api.drawTemplateIcon2D = HostAPI_DrawTemplateIcon2D; + api.drawPowerIcon2D = HostAPI_DrawPowerIcon2D; + api.drawText2D = HostAPI_DrawText2D; + api.drawText2DScaled = HostAPI_DrawText2DScaled; + api.drawRect2D = HostAPI_DrawRect2D; + api.getScreenSize = HostAPI_GetScreenSize; + api.getLogicFrame = HostAPI_GetLogicFrame; + api.getLogicFramesPerSecond = HostAPI_GetLogicFramesPerSecond; + api.getUnitProductionProgress = HostAPI_GetUnitProductionProgress; + api.getUpgradeProductionProgress = HostAPI_GetUpgradeProductionProgress; + api.worldToScreen = HostAPI_WorldToScreen; + api.worldToScreenClamped = HostAPI_WorldToScreenClamped; + api.getObjectBuildingCategory = HostAPI_GetObjectBuildingCategory; + api.getObjectScreenBounds = HostAPI_GetObjectScreenBounds; + api.getObjectHealthBarScreenPosition = HostAPI_GetObjectHealthBarScreenPosition; + api.teleportViewportTo = HostAPI_TeleportViewportTo; + api.drawRectClock2D = HostAPI_DrawRectClock2D; + api.drawRemainingRectClock2D = HostAPI_DrawRemainingRectClock2D; + api.getD3DDevice8 = HostAPI_GetD3DDevice8; + api.getGameWindow = HostAPI_GetGameWindow; + api.enumeratePlayerObjects = HostAPI_EnumeratePlayerObjects; + api.getContainedObjects = HostAPI_GetContainedObjects; + api.getObjectTargetPosition = HostAPI_GetObjectTargetPosition; + api.isObjectAirborne = HostAPI_IsObjectAirborne; + api.isObjectVehicle = HostAPI_IsObjectVehicle; + api.getPlayerName = HostAPI_GetPlayerName; + api.getPlayerFactionTemplate = HostAPI_GetPlayerFactionTemplate; + api.getPlayerMoney = HostAPI_GetPlayerMoney; + api.getPlayerRank = HostAPI_GetPlayerRank; + api.getPlayerPowerState = HostAPI_GetPlayerPowerState; + api.getPlayerBuilderCount = HostAPI_GetPlayerBuilderCount; + api.getPlayerActiveGathererCount = HostAPI_GetPlayerActiveGathererCount; + api.getPlayerTotalMoneyEarned = HostAPI_GetPlayerTotalMoneyEarned; + api.getTemplateDisplayName = HostAPI_GetTemplateDisplayName; + api.getPlayerBuilderTemplateName = HostAPI_GetPlayerBuilderTemplateName; + api.getPlayerBuilderTemplateCounts = HostAPI_GetPlayerBuilderTemplateCounts; + api.getPlayerGathererTemplateName = HostAPI_GetPlayerGathererTemplateName; + api.getPlayerTeamNumber = HostAPI_GetPlayerTeamNumber; + api.getMatchPlayers = HostAPI_GetMatchPlayers; + api.getPlayerIsDefeated = HostAPI_GetPlayerIsDefeated; + api.getUserDataPath = HostAPI_GetUserDataPath; + api.drawLine2D = HostAPI_DrawLine2D; + api.isObjectProjectile = HostAPI_IsObjectProjectile; + api.getObjectContainerId = HostAPI_GetObjectContainerId; + return api; +} + +// The one table handed to every plugin. A function-local static because plugins retain the pointer +// indefinitely, so it must not be a temporary; every entry is a free function, so one instance is +// correct for all of them. +const GOPluginHostAPI& GOPluginManager::GetHostAPI() +{ + static const GOPluginHostAPI s_hostAPI = BuildHostAPI(); + return s_hostAPI; +} + +void GOPluginManager::RegisterGameplayEventHooks(const GOGameplayEventCallbacks* cb) +{ + if (cb != nullptr) + s_gameplayEventHooks.push_back(*cb); +} + +void GOPluginManager::RegisterRenderHooks(const GORenderCallbacks* cb) +{ + if (cb != nullptr) + s_renderHooks.push_back(*cb); +} + +// Scans for a single flat "key": "value" string field rather than +// pulling the full json.hpp parser into this file for two optional log-line fields. Not a general +// JSON parser - no nested objects/arrays, only \" and \\ are unescaped - but the manifest below is +// purely informational, so anything this doesn't understand just yields an empty value. +static std::string ExtractJsonStringField(const std::string& json, const char* key) +{ + const std::string quotedKey = std::string("\"") + key + "\""; + size_t pos = json.find(quotedKey); + if (pos == std::string::npos) + return std::string(); + + pos = json.find(':', pos + quotedKey.size()); + if (pos == std::string::npos) + return std::string(); + ++pos; + + while (pos < json.size() && isspace((unsigned char)json[pos])) + ++pos; + if (pos >= json.size() || json[pos] != '"') + return std::string(); + ++pos; + + std::string value; + while (pos < json.size() && json[pos] != '"') + { + if (json[pos] == '\\' && pos + 1 < json.size()) + ++pos; + value += json[pos]; + ++pos; + } + return value; +} + +// Optional sidecar manifest (foo.goplugin.dll -> foo.goplugin.json) with author info for the load +// log. Purely informational - GOPluginInfo stays authoritative, so a missing or malformed manifest +// never refuses a plugin. Returns a log fragment, or "". +static std::string ReadManifestSummary(const char* dllPath) +{ + std::string path(dllPath); + const size_t dot = path.rfind(".dll"); + if (dot == std::string::npos) + return std::string(); + path.replace(dot, 4, ".json"); + + FILE* f = fopen(path.c_str(), "rb"); + if (f == nullptr) + return std::string(); + + std::string text; + char buffer[512]; + size_t got; + while ((got = fread(buffer, 1, sizeof(buffer), f)) > 0) + text.append(buffer, got); + fclose(f); + + const std::string author = ExtractJsonStringField(text, "plugin_author"); + const std::string website = ExtractJsonStringField(text, "website"); + if (author.empty() && website.empty()) + return std::string(); + + std::string summary = " [by " + (author.empty() ? std::string("unknown") : author); + if (!website.empty()) + summary += ", " + website; + return summary + "]"; +} + +bool GOPluginManager::LoadPlugin(const char* dllPath) +{ + if (dllPath == nullptr) + return false; + + HMODULE hModule = LoadLibraryA(dllPath); + if (hModule == nullptr) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[Plugin] Failed to load %s (err=%u)", dllPath, GetLastError()); + return false; + } + + GOPluginGetInfoFunc fnGetInfo = (GOPluginGetInfoFunc)GetProcAddress(hModule, GO_PLUGIN_EXPORT_GETINFO_NAME); + GOPluginInitializeFunc fnInitialize = (GOPluginInitializeFunc)GetProcAddress(hModule, GO_PLUGIN_EXPORT_INITIALIZE_NAME); + GOPluginShutdownFunc fnShutdown = (GOPluginShutdownFunc)GetProcAddress(hModule, GO_PLUGIN_EXPORT_SHUTDOWN_NAME); + GOPluginTickFunc fnTick = (GOPluginTickFunc)GetProcAddress(hModule, GO_PLUGIN_EXPORT_TICK_NAME); // optional + + if (fnGetInfo == nullptr || fnInitialize == nullptr || fnShutdown == nullptr) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[Plugin] %s is missing required exports (GetInfo/Initialize/Shutdown)", dllPath); + FreeLibrary(hModule); + return false; + } + + GOPluginInfo info = {}; + fnGetInfo(&info); + + if (info.abiVersion != GO_PLUGIN_ABI_VERSION) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[Plugin] %s ABI version mismatch (plugin=%u, host=%u)", + dllPath, info.abiVersion, (uint32_t)GO_PLUGIN_ABI_VERSION); + FreeLibrary(hModule); + return false; + } + + LoadedPlugin loaded; + loaded.module = hModule; + loaded.path = dllPath; + loaded.name = (info.name != nullptr) ? info.name : "(unnamed)"; + loaded.version = (info.version != nullptr) ? info.version : "(unknown)"; + loaded.shutdown = fnShutdown; + loaded.tick = fnTick; + loaded.info = info; + + // A plugin may register hooks during Initialize() and then still return false. Snapshot both + // vector sizes and roll back on failure, or those callbacks keep pointing into the DLL we are + // about to FreeLibrary() and the next dispatch calls through freed memory. + const size_t gameplayHooksBefore = s_gameplayEventHooks.size(); + const size_t renderHooksBefore = s_renderHooks.size(); + if (!fnInitialize(&GetHostAPI())) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[Plugin] %s Initialize() returned failure", dllPath); + s_gameplayEventHooks.resize(gameplayHooksBefore); + s_renderHooks.resize(renderHooksBefore); + FreeLibrary(hModule); + return false; + } + + NetworkLog(ELogVerbosity::LOG_RELEASE, "[Plugin] Loaded %s v%s (%s) hooks=0x%X%s", + loaded.name.c_str(), loaded.version.c_str(), dllPath, info.hookCategories, + ReadManifestSummary(dllPath).c_str()); + + s_plugins.push_back(loaded); + return true; +} + +// Loads every *.goplugin.dll directly inside pluginDir. Returns how many loaded. +static int LoadPluginsInFolder(const std::string& pluginDir) +{ + const std::string searchPattern = pluginDir + "\\*.goplugin.dll"; + + WIN32_FIND_DATAA findData; + HANDLE hFind = FindFirstFileA(searchPattern.c_str(), &findData); + if (hFind == INVALID_HANDLE_VALUE) + return 0; + + int loaded = 0; + do + { + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + continue; + + const std::string fullPath = pluginDir + "\\" + findData.cFileName; + if (GOPluginManager::LoadPlugin(fullPath.c_str())) + ++loaded; + } + while (FindNextFileA(hFind, &findData) != 0); + + FindClose(hFind); + return loaded; +} + +void GOPluginManager::LoadPluginsFromDirectory(const char* directoryPath) +{ + if (directoryPath == nullptr) + return; + + const std::string root(directoryPath); + + // One folder per plugin, holding its DLL plus an optional .json manifest, so each plugin's + // files (DLL, manifest, any of its own data) stay together and the plugins directory does not + // become a pile of loose DLLs. + int loaded = 0; + int folders = 0; + + WIN32_FIND_DATAA findData; + HANDLE hFind = FindFirstFileA((root + "\\*").c_str(), &findData); + if (hFind != INVALID_HANDLE_VALUE) + { + do + { + if ((findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) + continue; + if (strcmp(findData.cFileName, ".") == 0 || strcmp(findData.cFileName, "..") == 0) + continue; + + ++folders; + loaded += LoadPluginsInFolder(root + "\\" + findData.cFileName); + } + while (FindNextFileA(hFind, &findData) != 0); + + FindClose(hFind); + } + + // A DLL dropped loose in plugins\ is not picked up. Say so loudly rather than silently doing + // nothing: a plugin that never runs looks exactly like a plugin that runs and draws nothing, + // and telling those apart cost a full round of testing once already. + WIN32_FIND_DATAA strayData; + HANDLE hStray = FindFirstFileA((root + "\\*.goplugin.dll").c_str(), &strayData); + if (hStray != INVALID_HANDLE_VALUE) + { + do + { + if (strayData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + continue; + + NetworkLog(ELogVerbosity::LOG_RELEASE, + "[Plugin] IGNORED %s\\%s - plugins live in their own folder. Move it to %s\\\\%s", + root.c_str(), strayData.cFileName, root.c_str(), strayData.cFileName); + } + while (FindNextFileA(hStray, &strayData) != 0); + + FindClose(hStray); + } + + if (loaded == 0) + { + NetworkLog(ELogVerbosity::LOG_RELEASE, "[Plugin] No plugins loaded (%s, %d folder(s) scanned)", + root.c_str(), folders); + } +} + +void GOPluginManager::UnloadAll() +{ + for (auto it = s_plugins.rbegin(); it != s_plugins.rend(); ++it) + { + if (it->shutdown != nullptr) + it->shutdown(); + if (it->module != nullptr) + FreeLibrary(it->module); + } + + s_plugins.clear(); + s_gameplayEventHooks.clear(); + s_renderHooks.clear(); +} + +// One gate for the whole framework: callbacks carry other players' queues, powers and buildings, so +// none of it may reach a plugin while the local client is a participant. Same condition the engine's +// own observer-only UI uses; replay playback passes, its local player being the observer. +bool GOPluginManager::IsLocalPlayerObserver() +{ + if (ThePlayerList == nullptr) + return false; + Player* localPlayer = ThePlayerList->getLocalPlayer(); + return (localPlayer != nullptr && (localPlayer->isPlayerObserver() || localPlayer->isPlayerDead())) ? true : false; +} + +void GOPluginManager::Tick() +{ + if (!IsLocalPlayerObserver()) + return; + + for (LoadedPlugin& plugin : s_plugins) + { + if (plugin.tick != nullptr) + plugin.tick(); + } +} + +// ---- IGameplayEventHooks dispatch. All no-ops unless IsLocalPlayerObserver() - a match +// participant never receives plugin callbacks. ---- +void GOPluginManager::DispatchUnitQueued(const GOUnitEvent& ev) +{ + if (!IsLocalPlayerObserver()) + return; + for (GOGameplayEventCallbacks& cb : s_gameplayEventHooks) + if (cb.onUnitQueued != nullptr) cb.onUnitQueued(&ev); +} + +void GOPluginManager::DispatchUnitCancelled(const GOUnitEvent& ev) +{ + if (!IsLocalPlayerObserver()) + return; + for (GOGameplayEventCallbacks& cb : s_gameplayEventHooks) + if (cb.onUnitCancelled != nullptr) cb.onUnitCancelled(&ev); +} + +void GOPluginManager::DispatchUnitCompleted(const GOUnitEvent& ev) +{ + if (!IsLocalPlayerObserver()) + return; + for (GOGameplayEventCallbacks& cb : s_gameplayEventHooks) + if (cb.onUnitCompleted != nullptr) cb.onUnitCompleted(&ev); +} + +void GOPluginManager::DispatchUpgradeQueued(const GOUpgradeEvent& ev) +{ + if (!IsLocalPlayerObserver()) + return; + for (GOGameplayEventCallbacks& cb : s_gameplayEventHooks) + if (cb.onUpgradeQueued != nullptr) cb.onUpgradeQueued(&ev); +} + +void GOPluginManager::DispatchUpgradeCancelled(const GOUpgradeEvent& ev) +{ + if (!IsLocalPlayerObserver()) + return; + for (GOGameplayEventCallbacks& cb : s_gameplayEventHooks) + if (cb.onUpgradeCancelled != nullptr) cb.onUpgradeCancelled(&ev); +} + +void GOPluginManager::DispatchUpgradeCompleted(const GOUpgradeEvent& ev) +{ + if (!IsLocalPlayerObserver()) + return; + for (GOGameplayEventCallbacks& cb : s_gameplayEventHooks) + if (cb.onUpgradeCompleted != nullptr) cb.onUpgradeCompleted(&ev); +} + +void GOPluginManager::DispatchBuildingDestroyed(const GOBuildingEvent& ev) +{ + if (!IsLocalPlayerObserver()) + return; + for (GOGameplayEventCallbacks& cb : s_gameplayEventHooks) + if (cb.onBuildingDestroyed != nullptr) cb.onBuildingDestroyed(&ev); +} + +void GOPluginManager::DispatchSpecialPowerTriggered(const GOSpecialPowerEvent& ev) +{ + if (!IsLocalPlayerObserver()) + return; + for (GOGameplayEventCallbacks& cb : s_gameplayEventHooks) + if (cb.onSpecialPowerTriggered != nullptr) cb.onSpecialPowerTriggered(&ev); +} + +void GOPluginManager::DispatchObjectDamaged(const GOCombatEvent& ev) +{ + if (!IsLocalPlayerObserver()) + return; + for (GOGameplayEventCallbacks& cb : s_gameplayEventHooks) + if (cb.onObjectDamaged != nullptr) cb.onObjectDamaged(&ev); +} + +void GOPluginManager::DispatchObjectHealed(const GOCombatEvent& ev) +{ + if (!IsLocalPlayerObserver()) + return; + for (GOGameplayEventCallbacks& cb : s_gameplayEventHooks) + if (cb.onObjectHealed != nullptr) cb.onObjectHealed(&ev); +} + +// ---- IRenderHooks dispatch. Same observer gate as the gameplay events. ---- + +void GOPluginManager::DispatchDrawOverlay() +{ + if (!IsLocalPlayerObserver()) + return; + for (GORenderCallbacks& cb : s_renderHooks) + if (cb.onDrawOverlay != nullptr) cb.onDrawOverlay(); +} + +void GOPluginManager::DispatchRawKeyUp(uint32_t scanCode, uint32_t modifierFlags) +{ + if (!IsLocalPlayerObserver()) + return; + for (GORenderCallbacks& cb : s_renderHooks) + if (cb.onRawKeyUp != nullptr) cb.onRawKeyUp(scanCode, modifierFlags); +} + +void GOPluginManager::DispatchMouseMove(int32_t x, int32_t y) +{ + if (!IsLocalPlayerObserver()) + return; + for (GORenderCallbacks& cb : s_renderHooks) + if (cb.onMouseMove != nullptr) cb.onMouseMove(x, y); +} + +void GOPluginManager::DispatchMouseButtonDown(uint8_t buttonIndex, int32_t x, int32_t y, uint32_t modifierFlags) +{ + if (!IsLocalPlayerObserver()) + return; + for (GORenderCallbacks& cb : s_renderHooks) + if (cb.onMouseButtonDown != nullptr) cb.onMouseButtonDown(buttonIndex, x, y, modifierFlags); +} + +void GOPluginManager::DispatchMouseButtonUp(uint8_t buttonIndex, int32_t x, int32_t y, uint32_t modifierFlags) +{ + if (!IsLocalPlayerObserver()) + return; + for (GORenderCallbacks& cb : s_renderHooks) + if (cb.onMouseButtonUp != nullptr) cb.onMouseButtonUp(buttonIndex, x, y, modifierFlags); +} diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 3fd458ff3b0..29e4604f802 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -111,9 +111,23 @@ static void drawFramerateBar(); #include "WinMain.h" +#include "WW3D2/dx8wrapper.h" +#include "GameNetwork/GeneralsOnline/Plugins/PluginManager.h" + // DEFINE AND ENUMS /////////////////////////////////////////////////////////// +// GameEngine must not include WW3D2 or WinMain.h, so the plugin framework is handed these instead. +static void* GetD3DDevice8ForPlugins() +{ + return (void*)DX8Wrapper::_Get_D3D_Device8(); +} + +static void* GetGameWindowForPlugins() +{ + return (void*)ApplicationHWnd; +} + #define no_SAMPLE_DYNAMIC_LIGHT 1 #ifdef SAMPLE_DYNAMIC_LIGHT static W3DDynamicLight * theDynamicLight = nullptr; @@ -636,6 +650,19 @@ void W3DDisplay::setup2DRenderState(TextureClass *tex, DrawImageMode mode, Bool { if (m_isBatching) { + // Render2DClass fills two dynamic buffers whose counts are unsigned short, so one batch may + // hold at most 65535 of either. Flush before the next primitive can cross that, which costs + // one extra draw call and loses nothing. Must precede the same-state early-out below, since + // an overflow is reached by exactly the run of same-state draws that early-out serves. + const int MAX_BATCH_ELEMENTS = 65535; + const int WORST_CASE_ELEMENTS_PER_DRAW = 64; // drawOpenRect is 4 lines: 16 verts, 24 indices + if (!m_batchNeedsInit && m_2DRender && + (m_2DRender->Get_Index_Count() > MAX_BATCH_ELEMENTS - WORST_CASE_ELEMENTS_PER_DRAW || + m_2DRender->Get_Vertex_Count() > MAX_BATCH_ELEMENTS - WORST_CASE_ELEMENTS_PER_DRAW)) + { + onFlush(); + } + if (!m_batchNeedsInit && m_batchTexture == tex && m_batchMode == mode && m_batchGrayscale == grayscale) { return; @@ -972,6 +999,8 @@ void W3DDisplay::init() DX8WebBrowser::Initialize(); } + GOPluginManager::SetNativeHandleProviders(GetD3DDevice8ForPlugins, GetGameWindowForPlugins); + // we're now online m_initialized = true; if( TheGlobalData->m_displayDebug ) diff --git a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/render2d.h b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/render2d.h index fd1b400b79d..e22f06c7d5d 100644 --- a/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/render2d.h +++ b/GeneralsMD/Code/Libraries/Source/WWVegas/WW3D2/render2d.h @@ -153,6 +153,11 @@ class Render2DClass // Color access DynamicVectorClass & Get_Color_Array () { return Colors; } + // Current batch size. Both dynamic buffers Render() fills are indexed by unsigned short, so a + // caller that batches across many primitives must flush before either count exceeds 65535. + int Get_Vertex_Count() const { return Vertices.Count(); } + int Get_Index_Count() const { return Indices.Count(); } + // statics to access the Screen Resolution in Pixels static void Set_Screen_Resolution( const RectClass & screen ); static const RectClass & Get_Screen_Resolution() { return ScreenResolution; }