diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c3b8f39bd5..50d9e642d7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -271,6 +271,69 @@ When the type or function you need is missing or incorrect in YRpp, add or fix i **Important:** Always push your YRpp branch *before* pushing the Phobos commit that references it, otherwise CI cannot resolve the submodule. +### Interop API design + +#### Key macros + +Interop exports are declared in `src/Interop/*.h` using macros from `src/Utilities/Macro.h`: + +| Macro | Purpose | Notes | +|---|---|---| +| `DEFINE_CALLBACK(returnType, FuncName, params...)` | Declares a callback type. Callbacks are chain-able (each receives previous result) and stored in a static `std::vector`. Use null-checks when invoking. | Declare in header, initialize in `.cpp` | +| `DEFINE_EXPORT(returnType, FuncName, params...)` | Exports a C function for P/Invoke. Signature: `extern "C" __declspec(dllexport)`. Use `_Phobos` suffix to avoid naming collisions with Ares. | See `src/Interop/TechnoExt.h/cpp` for examples | + +#### Interop API version management + +Interop API versions are defined in `src/Interop/Version.h` and follow **Semantic Versioning 2.0.0** (see https://semver.org/): + +| Version Component | Increment When | Example | +|---|---|---| +| **Major** | Breaking change (backward incompatible). Existing external code **will not work** without modification. | Changing callback signature: `double f(int a)` → `double f(int a, int b)` | +| **Minor** | New backward-compatible feature added. External code continues to work without change. | Adding a new `DEFINE_EXPORT` function or new callback type. | +| **Patch** | Backward-compatible bug fix only. No API changes. | Fixing a logic error in an exported function implementation. | + +**Version constants in `Version.h`:** +```cpp +#define INTEROP_API_VERSION_MAJOR 1 +#define INTEROP_API_VERSION_MINOR 2 +#define INTEROP_API_VERSION_PATCH 0 +``` + +**When to increment:** + +- **MAJOR bump** (1.0.0 → 2.0.0): + - Change callback/function signature (parameter type, return type, count). + - Remove or rename a callback/export. + - Change callback return value semantics (e.g., return value now used differently). + +- **MINOR bump** (1.0.0 → 1.1.0): + - Add a new callback type and its registration function. + - Add a new `DEFINE_EXPORT` function. + - Extend callback behavior with new optional parameters (if backward compatible). + +- **PATCH bump** (1.0.0 → 1.0.1): + - Fix a bug in an exported function's implementation. + - Clarify documentation/comments. + - No API signature changes. + +**Example workflow:** + +1. You add `DEFINE_EXPORT(void, NewFeature_Phobos, ...)` - this is a new feature → **MINOR** bump (1.0.0 → 1.1.0). +2. You change the signature of an existing callback from `int f(ptr)` to `int f(ptr, int extra)` → **MAJOR** bump (1.1.0 → 2.0.0). +3. You fix a logic bug in `ConvertToType_Phobos` without changing its signature → **PATCH** bump (2.0.0 → 2.0.1). + +**Deprecated APIs:** + +When an exported function or callback becomes obsolete but must remain for compatibility, keep its stub but document it with a version range and deprecation note: + +```cpp +/// +/// DEPRECATED: Use RegisterCalculateSightCallback_Phobos (available since 1.1.0). +/// This function will be removed in version 3.0.0. +/// +DEFINE_EXPORT(void, RegisterSightModifier_Phobos, OldCallbackType callback); // Removed in 3.0.0 +``` + ## Code Style (enforced by .editorconfig) - **Tabs** for indentation (size 4). diff --git a/.github/workflows/pr-doc-checker.yml b/.github/workflows/pr-doc-checker.yml index 90a22d82ef..273aeedb6b 100644 --- a/.github/workflows/pr-doc-checker.yml +++ b/.github/workflows/pr-doc-checker.yml @@ -77,6 +77,7 @@ jobs: -e ^docs/Fixed-or-Improved-Logics.md$ \ -e ^docs/AI-Scripting-and-Mapping.md$ \ -e ^docs/User-Interface.md$ \ + -e ^docs/Interoperability.md$ \ -e ^docs/Miscellanous.md$ \ ) then @@ -87,3 +88,35 @@ jobs: echo "Please, document your changes or use [No Documentation Needed] label for your Pull Request." exit 1 fi + + Interop-Version-Check: + name: Interop API Version Updated + # If the No Documentation Needed label is set, then workflow will not be executed + if: ${{ !contains(github.event.pull_request.labels.*.name, 'No Documentation Needed') }} + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Check that Interop API version is updated + run: | + # Check if any files in src/Interop/ were modified (excluding Version.h itself for initial checks) + CHANGED_FILES=$(git diff --name-only $(git merge-base origin/$BASE_BRANCH HEAD)) + INTEROP_MODIFIED=$(echo "$CHANGED_FILES" | grep -E "^src/Interop/.*\.(h|cpp)$" | grep -v "^src/Interop/Version\." || true) + + if [ -z "$INTEROP_MODIFIED" ]; then + echo "No Interop files modified. Skipping version check." + exit 0 + fi + + # If Interop files were modified, check if Version.h was also modified + if echo "$CHANGED_FILES" | grep -q "^src/Interop/Version\.h$"; then + echo "Thank you for updating the Interop API version! 😋" + exit 0 + else + echo "You modified Interop API but forgot to update the version! 🧐" + echo "Please update the version constants in 'src/Interop/Version.h' according to Semantic Versioning." + echo "See .github/copilot-instructions.md for version bumping guidelines." + exit 1 + fi diff --git a/.gitignore b/.gitignore index c7875aa55b..874d93ae3c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ out # Python virtual environment for docs .venv/ + +debug.log diff --git a/CREDITS.md b/CREDITS.md index 181d17374f..de792734b4 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -749,6 +749,7 @@ This page lists all the individual contributions to the project by their author. - Allow replacing vanilla repairing with togglable auto repairing - Fix an issue that the time for units in the area guard mission to reacquire targets after eliminating the target is significantly longer than that in other missions - Framework for dynamic sight + - Export interface for external call - **solar-III (凤九歌)** - Target scanning delay customization (documentation) - Skip target scanning function calling for unarmed technos (documentation) diff --git a/Phobos.vcxproj b/Phobos.vcxproj index e1aa9e9c96..6f838840bd 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -32,6 +32,11 @@ + + + + + @@ -225,6 +230,11 @@ + + + + + diff --git a/README.md b/README.md index 52c77fe94d..e56af57c5f 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,10 @@ Credits This project was founded by [@Belonit](https://github.com/Belonit) (Gluk-v48) and [@Metadorius](https://github.com/Metadorius) (Kerbiter) in 2020, with the first public stable release in 2021. Since then it has grown into a large community project with many contributors and maintainers. +### Interoperability + +Phobos has opened the external interfaces of some key components. If you are also developing your own engine extension and wish to use Phobos at the same time, please check out [Interoperability](Interoperability.md). + ### Maintenance crew Maintenance crew consists of experienced Phobos contributors who are recognized and given the permission to maintain and shape the project to the extent of their permissions. diff --git a/docs/General-Info.md b/docs/General-Info.md index 5b1dc16cde..17e5324bd2 100644 --- a/docs/General-Info.md +++ b/docs/General-Info.md @@ -28,3 +28,7 @@ Phobos fully supports saving and loading thanks to prototype code from publicly While Phobos is standalone, it is designed to be used alongside [Ares](https://ares.strategy-x.com) and [CnCNet5 spawner](https://github.com/CnCNet/cncnet-for-ares). Adding new features or improving existing ones is done with compatibility with those in mind. While we would also like to support HAres we can't guarantee compatibility with it due to the separation of it's userbase and developers from international community. We welcome any help on the matter though! + +### API versioning + +See [Interoperability](Interoperability.md#api-version-tracking). diff --git a/docs/Interoperability.md b/docs/Interoperability.md new file mode 100644 index 0000000000..4793da8043 --- /dev/null +++ b/docs/Interoperability.md @@ -0,0 +1,278 @@ + +# Interoperability + +This page documents the exported interfaces in [Interop](https://github.com/Phobos-developers/Phobos/tree/develop/src/Interop). + +## API Version Tracking + +### Semantic Versioning Rules + +- **Major (X)**: Increment for breaking changes (backward incompatible API modifications). + Example: Deleting an API, changing function parameters, modifying data structures that break existing code. + When bumped, reset Minor and Patch to 0 (e.g., 1.2.3 → 2.0.0). + +- **Minor (Y)**: Increment for backward-compatible new features. + Example: Adding a new API function, new parameters that don't break existing code. + When bumped, reset Patch to 0 (e.g., 1.2.3 → 1.3.0). + +- **Patch (Z)**: Increment for backward-compatible miscs (no API changes). + Example: Fixing crashes, correcting calculations, optimizing internals without changing interface (e.g., 1.2.3 → 1.2.4). + +### GetInteropAPIVersion + +```cpp +InteropAPIVersion GetInteropAPIVersion() +``` + +Returns the current Interop API version as a structure `{ major, minor, patch }`. + +**Example (C# P/Invoke):** +```csharp +[DllImport("Phobos.dll", CallingConvention = CallingConvention.StdCall)] +public static extern InteropAPIVersion GetInteropAPIVersion(); + +var version = GetInteropAPIVersion(); +if (version.major >= 1 && version.minor >= 1) +{ + // Safe to use features from v1.1.0 onwards +} +``` + +### Deprecated API Handling + +When an API is deprecated, its function stub is retained but with a fatal error handler. The calling application will receive a descriptive error message and must stop execution. This ensures: +1. Broken links are immediately detected at runtime (not a silent crash). +2. Clear messaging guides developers to the replacement API and version range. +3. Migration timeline is documented in the error message. + +**Example** (not currently in use): + +```cpp +// DEPRECATED: Removed in Interop API v2.0.0. Use NewAPI instead. +// Availability: [1.0.0, 2.0.0) +// Calling this will trigger a fatal error with the message: +// "SomeOldAPI_Deprecated has been removed in Interop API v2.0.0 (was available in v1.0.0-v1.x.x). Please use NewAPI." +``` + +## Available APIs + +| Module | API | Availability | Status | +|----------|-----|---------------|--------| +| AttachEffect | AE_Attach | [1.0.0, ∞) | Active | +| AttachEffect | AE_Detach | [1.0.0, ∞) | Active | +| AttachEffect | AE_DetachByGroups | [1.0.0, ∞) | Active | +| AttachEffect | AE_TransferEffects | [1.0.0, ∞) | Active | +| BulletExt | Bullet_SetFirerOwner | [1.0.0, ∞) | Active | +| EventExt | EventExt_AddEvent | [1.0.0, ∞) | Active | +| TechnoExt | ConvertToType_Phobos | [1.0.0, ∞) | Active | +| TechnoExt | RegisterCalculateExtraThreatCallback | [1.0.0, ∞) | Active | + +### AttachEffect + +#### AE_Attach + +**Availability:** [1.0.0, ∞) + +```cpp +int AE_Attach( + TechnoClass* pTarget, + HouseClass* pInvokerHouse, + TechnoClass* pInvoker, + AbstractClass* pSource, + const char** effectTypeNames, + int typeCount, + int durationOverride, + int delay, + int initialDelay, + int recreationDelay +) +``` + +Attaches one or more AttachEffect types to the target. + +- Parameters: + - pTarget: Target unit to receive effects. + - pInvokerHouse: Invoker house context. + - pInvoker: Invoker techno context. + - pSource: Optional source object context. + - effectTypeNames: Array of AttachEffect type names. + - typeCount: Number of entries in effectTypeNames. + - durationOverride: If non-zero, duration override is applied. + - delay: If >= 0, delay override is applied. + - initialDelay: If >= 0, initial delay override is applied. + - recreationDelay: If >= -1, recreation delay override is applied. +- Returns: + - > 0 on success (value returned by internal attach routine). + - 0 on failure. +- Fails when: + - pTarget is null. + - effectTypeNames is null. + - typeCount <= 0. + - No valid effect type name resolves to an existing AttachEffectType. + +#### AE_Detach + +**Availability:** [1.0.0, ∞) + +```cpp +int AE_Detach( + TechnoClass* pTarget, + const char** effectTypeNames, + int typeCount +) +``` + +Detaches effects by explicit effect type names. + +- Parameters: + - pTarget: Target unit to remove effects from. + - effectTypeNames: Array of AttachEffect type names to remove. + - typeCount: Number of entries in effectTypeNames. + +- Returns: + - > 0 on success (value returned by internal detach routine). + - 0 on failure. +- Fails when: + - pTarget is null. + - effectTypeNames is null. + - typeCount <= 0. + - No valid effect type name resolves to an existing AttachEffectType. + +#### AE_DetachByGroups + +**Availability:** [1.0.0, ∞) + +```cpp +int AE_DetachByGroups( + TechnoClass* pTarget, + const char** groupNames, + int groupCount +) +``` + +Detaches effects by AttachEffect group name. + +- Parameters: + - pTarget: Target unit to remove effects from. + - groupNames: Array of group names. + - groupCount: Number of entries in groupNames. + +- Returns: + - > 0 on success (value returned by internal detach-by-group routine). + - 0 on failure. +- Fails when: + - pTarget is null. + - groupNames is null. + - groupCount <= 0. + - groupNames contains no non-null entries. + +#### AE_TransferEffects + +**Availability:** [1.0.0, ∞) + +```cpp +void AE_TransferEffects( + TechnoClass* pSource, + TechnoClass* pTarget +) +``` + +Transfers all attached effects from source to target. + +- Parameters: + - pSource: Source unit. + - pTarget: Target unit. + +- Behavior: + - No operation if pSource or pTarget is null. + +## Vanilla class extension + +### TechnoExt + +#### ConvertToType_Phobos + +**Availability:** [1.0.0, ∞) + +```cpp +bool ConvertToType_Phobos(FootClass* pThis, TechnoTypeClass* toType) +``` + +Converts a FootClass instance to another TechnoType. + +- Parameters: + - pThis: Unit to convert. + - toType: Destination TechnoType. + +- Returns: + - true if conversion succeeds. + - false if conversion fails. +- Notes: + - This API forwards directly to TechnoExt::ConvertToType. + +#### RegisterCalculateExtraThreatCallback + +**Availability:** [1.0.0, ∞) + +```cpp +typedef double (*CalculateExtraThreatCallback)(TechnoClass* pThis, ObjectClass* pTarget, double originalThreat); + +void RegisterCalculateExtraThreatCallback(CalculateExtraThreatCallback callback) +``` + +Registers a callback function to calculate extra threat for a unit. + +- Parameters: + - callback: Callback function pointer that returns the calculated threat modifier. Signature: `double callback(TechnoClass* pThis, ObjectClass* pTarget, double originalThreat)`. + +- Behavior: + - If callback is non-null, it is added to the internal callback list. + - When threat calculations occur, all registered callbacks are invoked to compute additional threat contributions. + - Multiple callbacks can be registered and are executed in registration order. + +- Notes: + - The callback invocation method is `totalThreat = cb(pThis, pTarget, totalThreat)`. + +### BulletExt + +#### Bullet_SetFirerOwner + +**Availability:** [1.0.0, ∞) + +```cpp +bool Bullet_SetFirerOwner(BulletClass* pBullet, HouseClass* pHouse) +``` + +Updates the recorded firer house for a bullet extension. + +- Parameters: + - pBullet: Bullet instance. + - pHouse: New firer house (can be null if caller intentionally clears ownership). + +- Returns: + - true if the bullet extension is found and updated. + - false on failure. +- Fails when: + - pBullet is null. + - No BulletExt entry exists for pBullet. + +### EventExt + +#### EventExt_AddEvent + +**Availability:** [1.0.0, ∞) + +```cpp +bool EventExt_AddEvent(EventExt* pEventExt) +``` + +Invokes AddEvent on an EventExt object. + +- Parameters: + - pEventExt: Event extension instance. + +- Returns: + - false if pEventExt is null. + - Otherwise, returns the result of pEventExt->AddEvent(). + + diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 9b559e527b..e54d6928b5 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -711,6 +711,7 @@ Fixes / interactions with other extensions: - Fixed a bug where passengers created by the InitialPayload logic or TeamType with `Full=true` would fail to fire when the transport unit with `OpenTopped=yes` moved to an area that the passengers' `MovementZone` cannot move into (by NetsuNegi) - Fixed a bug where game will crash after loading if a techno with `AlphaImage` converts to a type without it, or an anim with `AlphaImage` changes to a type without it through `Next` (by NetsuNegi & FlyStar) - Fixed the issue that `BombSight` not being updated correctly in techno conversion (by TaranDahl) +- [Export interface for external call](index.md#interoperability) (by TaranDahl) ``` diff --git a/docs/index.md b/docs/index.md index 6fd2d8e666..45f870286e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,6 +16,7 @@ New / Enhanced Logics Fixed / Improved Logics AI Scripting and Mapping User Interface +Interoperability Miscellanous ``` diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index 3793e7781a..097434457f 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -2461,6 +2461,9 @@ msgstr "修复了非超时空矿车在矿区无矿后即便矿石已重新生成 msgid "Miners back to work when ore regenerated" msgstr "矿车在矿石再生后返回工作" +msgid "Export interface for external call" +msgstr "用于外部调用的导出接口" + msgid "Allow disable an over-optimization in targeting" msgstr "允许禁用索敌中的过度优化" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index 4fa6168aec..38b71f038f 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -2590,6 +2590,11 @@ msgstr "" "修复了有 `AlphaImage` 的单位转换为无 AlphaImage 的类型,或有 `AlphaImage` 的动画通过 `Next` " "更改为无 AlphaImage 的类型后读档会导致游戏崩溃的 Bug(by NetsuNegi & FlyStar)" +msgid "" +"[Export interface for external call](index.md#interoperability) (by " +"TaranDahl)" +msgstr "用于外部调用的导出接口(by TaranDahl)" + msgid "" "Fixed the issue that `BombSight` not being updated correctly in techno " "conversion (by TaranDahl)" diff --git a/docs/locale/zh_CN/LC_MESSAGES/index.po b/docs/locale/zh_CN/LC_MESSAGES/index.po index a158b9f6f9..9a52479762 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/index.po +++ b/docs/locale/zh_CN/LC_MESSAGES/index.po @@ -384,6 +384,12 @@ msgstr "" "[@Metadorius](https://github.com/Metadorius)(Kerbiter)于 2020 年创立,2021 " "年首次发布稳定版。此后它已发展成为一个由众多贡献者和维护者组成的大型社区项目。" +msgid "Interoperability" +msgstr "互操作性" + +msgid "Phobos has opened the external interfaces of some key components. If you are also developing your own engine extension and wish to use Phobos at the same time, please check out [Interoperability](Interoperability.md)." +msgstr "Phobos为一些关键组件打开了外部接口。如果你也在开发自己的引擎扩展并且希望同时使用Phobos,请查阅[Interoperability](Interoperability.md)。" + msgid "Maintenance crew" msgstr "维护团队" diff --git a/src/Ext/Techno/Body.cpp b/src/Ext/Techno/Body.cpp index 2646b31b12..17a4cfc524 100644 --- a/src/Ext/Techno/Body.cpp +++ b/src/Ext/Techno/Body.cpp @@ -9,6 +9,7 @@ #include #include +#include TechnoExt::ExtContainer TechnoExt::ExtMap; UnitClass* TechnoExt::Deployer = nullptr; @@ -1151,7 +1152,15 @@ bool __fastcall TechnoExt::ApplyKillDriver(TechnoClass** pData, void*, HouseClas int TechnoExt::ExtData::GetSight() { - return this->TypeExtData->OwnerObject()->Sight; + double sight = this->TypeExtData->OwnerObject()->Sight; + + for (auto& callback : TechnoExtInterop::CalculateSightCallbacks) + { + if (callback) + sight = callback(this->OwnerObject(), sight); + } + + return static_cast(sight); } // ============================= diff --git a/src/Ext/Techno/Body.h b/src/Ext/Techno/Body.h index 20ec1e7c6a..e8018d0a8b 100644 --- a/src/Ext/Techno/Body.h +++ b/src/Ext/Techno/Body.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include diff --git a/src/Ext/Techno/Hooks.TargetEvaluation.cpp b/src/Ext/Techno/Hooks.TargetEvaluation.cpp index 4ffddf15c2..85ad897768 100644 --- a/src/Ext/Techno/Hooks.TargetEvaluation.cpp +++ b/src/Ext/Techno/Hooks.TargetEvaluation.cpp @@ -1,4 +1,5 @@ #include "Body.h" +#include "Interop/TechnoExt.h" // Cursor & target acquisition stuff not directly tied to other features can go here. @@ -506,6 +507,11 @@ DEFINE_HOOK(0x70CF87, TechnoClass_ThreatCoefficient_CanAttackMeThreatBonus, 0x9) }; ApplyLastTargetDistanceBonus(); + for (auto const& cb : TechnoExtInterop::CalculateExtraThreatCallbacks) + { + totalThreat = cb(pThis, pTarget, totalThreat); + } + return 0; } diff --git a/src/Interop/AttachEffect.cpp b/src/Interop/AttachEffect.cpp new file mode 100644 index 0000000000..681a1a96d7 --- /dev/null +++ b/src/Interop/AttachEffect.cpp @@ -0,0 +1,109 @@ + +#include "AttachEffect.h" +#include "New/Entity/AttachEffectClass.h" +#include "New/Type/AttachEffectTypeClass.h" + +DEFINE_EXPORT(int, AE_Attach, + TechnoClass* pTarget, + HouseClass* pInvokerHouse, + TechnoClass* pInvoker, + AbstractClass* pSource, + const char** effectTypeNames, + int typeCount, + int durationOverride, + int delay, + int initialDelay, + int recreationDelay +) +{ + if (!pTarget || !effectTypeNames || typeCount <= 0) + return 0; + + AEAttachInfoTypeClass attachInfo; + + for (int i = 0; i < typeCount; i++) + { + if (effectTypeNames[i]) + { + if (auto pType = AttachEffectTypeClass::Find(effectTypeNames[i])) + attachInfo.AttachTypes.push_back(pType); + } + } + + if (attachInfo.AttachTypes.empty()) + return 0; + + if (durationOverride != 0) + attachInfo.DurationOverrides.push_back(durationOverride); + + if (delay >= 0) + attachInfo.Delays.push_back(delay); + + if (initialDelay >= 0) + attachInfo.InitialDelays.push_back(initialDelay); + + if (recreationDelay >= -1) + attachInfo.RecreationDelays.push_back(recreationDelay); + + return AttachEffectClass::Attach(pTarget, pInvokerHouse, pInvoker, pSource, attachInfo); +} + +DEFINE_EXPORT(int, AE_Detach, + TechnoClass* pTarget, + const char** effectTypeNames, + int typeCount +) +{ + if (!pTarget || !effectTypeNames || typeCount <= 0) + return 0; + + AEAttachInfoTypeClass detachInfo; + + for (int i = 0; i < typeCount; i++) + { + if (effectTypeNames[i]) + { + if (auto pType = AttachEffectTypeClass::Find(effectTypeNames[i])) + detachInfo.RemoveTypes.push_back(pType); + } + } + + if (detachInfo.RemoveTypes.empty()) + return 0; + + return AttachEffectClass::Detach(pTarget, detachInfo); +} + +DEFINE_EXPORT(int, AE_DetachByGroups, + TechnoClass* pTarget, + const char** groupNames, + int groupCount +) +{ + if (!pTarget || !groupNames || groupCount <= 0) + return 0; + + AEAttachInfoTypeClass detachInfo; + + for (int i = 0; i < groupCount; i++) + { + if (groupNames[i]) + detachInfo.RemoveGroups.push_back(groupNames[i]); + } + + if (detachInfo.RemoveGroups.empty()) + return 0; + + return AttachEffectClass::DetachByGroups(pTarget, detachInfo); +} + +DEFINE_EXPORT(void, AE_TransferEffects, + TechnoClass* pSource, + TechnoClass* pTarget +) +{ + if (!pSource || !pTarget) + return; + + AttachEffectClass::TransferAttachedEffects(pSource, pTarget); +} diff --git a/src/Interop/AttachEffect.h b/src/Interop/AttachEffect.h new file mode 100644 index 0000000000..40c40a84f2 --- /dev/null +++ b/src/Interop/AttachEffect.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +// Interop exports for AttachEffect operations. +// C# P/Invoke examples (simplified): +// ```csharp +// [DllImport("Phobos.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "AE_Attach")] +// public static extern int AE_Attach(IntPtr pTarget, IntPtr pInvokerHouse, IntPtr pInvoker, IntPtr pSource, IntPtr effectTypeNames, int typeCount, int durationOverride, int delay, int initialDelay, int recreationDelay); +// +// [DllImport("Phobos.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "AE_Detach")] +// public static extern int AE_Detach(IntPtr pTarget, IntPtr effectTypeNames, int typeCount); +// ``` + +#include "Utilities/Macro.h" + +/// +/// Attaches AttachEffect instances to a target unit. +/// +DEFINE_EXPORT(int, AE_Attach, + TechnoClass* pTarget, + HouseClass* pInvokerHouse, + TechnoClass* pInvoker, + AbstractClass* pSource, + const char** effectTypeNames, + int typeCount, + int durationOverride, + int delay, + int initialDelay, + int recreationDelay +); + +/// +/// Removes AttachEffect instances matching given types from a unit. +/// +DEFINE_EXPORT(int, AE_Detach, + TechnoClass* pTarget, + const char** effectTypeNames, + int typeCount +); + +/// +/// Removes AttachEffect instances matching given groups from a unit. +/// +DEFINE_EXPORT(int, AE_DetachByGroups, + TechnoClass* pTarget, + const char** groupNames, + int groupCount +); + +/// +/// Transfers AttachEffect instances from one unit to another. +/// +DEFINE_EXPORT(void, AE_TransferEffects, + TechnoClass* pSource, + TechnoClass* pTarget +); diff --git a/src/Interop/BulletExt.cpp b/src/Interop/BulletExt.cpp new file mode 100644 index 0000000000..c73d6dac9a --- /dev/null +++ b/src/Interop/BulletExt.cpp @@ -0,0 +1,16 @@ + +#include "BulletExt.h" + +DEFINE_EXPORT(bool, Bullet_SetFirerOwner, BulletClass* pBullet, HouseClass* pHouse) +{ + if (!pBullet) + return false; + + const auto pBulletExt = BulletExt::ExtMap.TryFind(pBullet); + + if (!pBulletExt) + return false; + + pBulletExt->FirerHouse = pHouse; + return true; +} diff --git a/src/Interop/BulletExt.h b/src/Interop/BulletExt.h new file mode 100644 index 0000000000..3fe4a0130e --- /dev/null +++ b/src/Interop/BulletExt.h @@ -0,0 +1,7 @@ +#pragma once + +#include "Utilities/Macro.h" +#include +#include + +DEFINE_EXPORT(bool, Bullet_SetFirerOwner, BulletClass* pBullet, HouseClass* pHouse); diff --git a/src/Interop/EventExt.cpp b/src/Interop/EventExt.cpp new file mode 100644 index 0000000000..61576116d9 --- /dev/null +++ b/src/Interop/EventExt.cpp @@ -0,0 +1,10 @@ +#include "EventExt.h" + +DEFINE_EXPORT(bool, EventExt_AddEvent, EventExt* pEventExt) +{ + if (pEventExt) + { + return pEventExt->AddEvent(); + } + return false; +} diff --git a/src/Interop/EventExt.h b/src/Interop/EventExt.h new file mode 100644 index 0000000000..d4c91c519c --- /dev/null +++ b/src/Interop/EventExt.h @@ -0,0 +1,6 @@ +#pragma once + +#include "Utilities/Macro.h" +#include + +DEFINE_EXPORT(bool, EventExt_AddEvent, EventExt* pEventExt); diff --git a/src/Interop/TechnoExt.cpp b/src/Interop/TechnoExt.cpp new file mode 100644 index 0000000000..2bf4793947 --- /dev/null +++ b/src/Interop/TechnoExt.cpp @@ -0,0 +1,23 @@ +#include "TechnoExt.h" +#include +#include + +std::vector TechnoExtInterop::CalculateExtraThreatCallbacks = {}; +std::vector TechnoExtInterop::CalculateSightCallbacks = {}; + +DEFINE_EXPORT(bool, ConvertToType_Phobos, FootClass* pThis, TechnoTypeClass* toType) +{ + return TechnoExt::ConvertToType(pThis, toType); +} + +DEFINE_EXPORT(void, RegisterCalculateExtraThreatCallback, CalculateExtraThreatCallback callback) +{ + if (callback) + TechnoExtInterop::CalculateExtraThreatCallbacks.push_back(callback); +} + +DEFINE_EXPORT(void, RegisterCalculateSightCallback, CalculateSightCallback callback) +{ + if (callback) + TechnoExtInterop::CalculateSightCallbacks.push_back(callback); +} diff --git a/src/Interop/TechnoExt.h b/src/Interop/TechnoExt.h new file mode 100644 index 0000000000..0abb18e11f --- /dev/null +++ b/src/Interop/TechnoExt.h @@ -0,0 +1,41 @@ +#pragma once + + +// Interop exports for external engine extensions. +// C# P/Invoke example: +// ```csharp +// [DllImport("Phobos.dll", CallingConvention = CallingConvention.StdCall, EntryPoint = "ConvertToType_Phobos")] +// [return: MarshalAs(UnmanagedType.I1)] +// public static extern bool ConvertToType_Phobos(IntPtr pThis, IntPtr toType); +// ``` +// Use `IntPtr` or `unsafe` pointers in managed code. The function below exposes +// the same signature as used internally (FootClass*, TechnoTypeClass*). + +#include "Utilities/Macro.h" +#include + +#include + +#include + +DEFINE_CALLBACK(double, CalculateExtraThreatCallback, TechnoClass* pThis, ObjectClass* pTarget, double originalThreat); +DEFINE_CALLBACK(double, CalculateSightCallback, TechnoClass* pThis, double originalSight); + +class TechnoExtInterop +{ +public: + static std::vector CalculateExtraThreatCallbacks; + static std::vector CalculateSightCallbacks; +}; + +/// +/// Converts a unit to a different type. +/// +/// Pointer to the FootClass instance to convert +/// Pointer to the target TechnoTypeClass +/// true if conversion was successful, false otherwise +DEFINE_EXPORT(bool, ConvertToType_Phobos, FootClass* pThis, TechnoTypeClass* toType); + +DEFINE_EXPORT(void, RegisterCalculateExtraThreatCallback, CalculateExtraThreatCallback callback); + +DEFINE_EXPORT(void, RegisterCalculateSightCallback, CalculateSightCallback callback); diff --git a/src/Interop/Version.cpp b/src/Interop/Version.cpp new file mode 100644 index 0000000000..1139bacf53 --- /dev/null +++ b/src/Interop/Version.cpp @@ -0,0 +1,24 @@ +#include "Version.h" +#include "Utilities/Debug.h" + +DEFINE_EXPORT(InteropAPIVersion, GetInteropAPIVersion) +{ + return InteropAPIVersion{ + INTEROP_API_VERSION_MAJOR, + INTEROP_API_VERSION_MINOR, + INTEROP_API_VERSION_PATCH + }; +} + +// ============================================================================ +// Deprecated API implementation example (commented out for future use) +// ============================================================================ +/* +DEFINE_EXPORT(int, SomeOldAPI_Deprecated, int param1) +{ + // Trigger fatal error with explanation. + Debug::Fatal("SomeOldAPI_Deprecated has been removed in Interop API v2.0.0 (was available in v1.0.0-v1.x.x). " + "Please update your code to use the replacement API."); + return 0; // unreachable +} +*/ diff --git a/src/Interop/Version.h b/src/Interop/Version.h new file mode 100644 index 0000000000..bfb914fb75 --- /dev/null +++ b/src/Interop/Version.h @@ -0,0 +1,48 @@ +#pragma once + +#include "Utilities/Macro.h" + +// ============================================================================ +// Interop API Semantic Versioning (independent from Phobos build number) +// See https://semver.org/ for version semantics. +// ============================================================================ + +#define INTEROP_API_VERSION_MAJOR 1 +#define INTEROP_API_VERSION_MINOR 0 +#define INTEROP_API_VERSION_PATCH 0 + +/// +/// Interop API version information structure. +/// Follows Semantic Versioning 2.0.0: +/// - Major: Increment for breaking changes (backward incompatible API modifications). +/// - Minor: Increment for new backward-compatible features. +/// - Patch: Increment for backward-compatible bug fixes. +/// +struct InteropAPIVersion +{ + unsigned int major; + unsigned int minor; + unsigned int patch; +}; + +/// +/// Returns the current Interop API version following Semantic Versioning. +/// Each exported API is annotated with availability range [startVersion, endVersion). +/// If endVersion == 0, the API is still active. +/// +DEFINE_EXPORT(InteropAPIVersion, GetInteropAPIVersion); + +// ============================================================================ +// Deprecated API example (commented out for future use) +// When an API reaches end-of-life, keep its function stub but call fatal error. +// ============================================================================ +/* +/// +/// DEPRECATED: Removed in Interop API v2.0.0. Use SomeNewAPI instead. +/// Calling this function will trigger a fatal error. +/// Availability: [1.0.0, 2.0.0) +/// +DEFINE_EXPORT(int, SomeOldAPI_Deprecated, + int param1 +); +*/ diff --git a/src/Utilities/Macro.h b/src/Utilities/Macro.h index a0bdefff07..26833e823c 100644 --- a/src/Utilities/Macro.h +++ b/src/Utilities/Macro.h @@ -3,6 +3,16 @@ #include #include "Patch.h" +// Export / calling-convention macros for public C ABI functions +// Usage: +// DEFINE_EXPORT(return_type, func_name, arglist...) +#define DEFINE_EXPORT(ret, name, ...) extern "C" __declspec(dllexport) ret __stdcall name(__VA_ARGS__) + +// Callback typedef helper +// Usage: +// DEFINE_CALLBACK(return_type, func_name, arglist...) +#define DEFINE_CALLBACK(ret, name, ...) typedef ret (__stdcall* name)(__VA_ARGS__) + #define GET_REGISTER_STATIC_TYPE(type, dst, reg) static type dst; _asm { mov dst, reg } template