From 5be91bc7811643692cc50a21b96268fc010a09c7 Mon Sep 17 00:00:00 2001 From: uniboi <64006268+uniboi@users.noreply.github.com> Date: Sun, 6 Oct 2024 17:28:35 +0200 Subject: [PATCH] Improve plugin docs (#104) Co-authored-by: GeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com> --- .../plugins/exposed-interfaces/index.md | 11 ++ .../plugins/exposed-interfaces/northstar.md | 31 ++++ docs/Modding/plugins/index.md | 75 +++++++++ docs/Modding/plugins/interfaces.md | 158 ++++++++++++++++++ docs/Modding/plugins/interfacesapi.md | 98 ----------- docs/Modding/plugins/required-interfaces.md | 136 +++++++++++++++ docs/Modding/plugins/resources.md | 9 - 7 files changed, 411 insertions(+), 107 deletions(-) create mode 100644 docs/Modding/plugins/exposed-interfaces/index.md create mode 100644 docs/Modding/plugins/exposed-interfaces/northstar.md create mode 100644 docs/Modding/plugins/index.md create mode 100644 docs/Modding/plugins/interfaces.md delete mode 100644 docs/Modding/plugins/interfacesapi.md create mode 100644 docs/Modding/plugins/required-interfaces.md delete mode 100644 docs/Modding/plugins/resources.md diff --git a/docs/Modding/plugins/exposed-interfaces/index.md b/docs/Modding/plugins/exposed-interfaces/index.md new file mode 100644 index 00000000..29a7b4cf --- /dev/null +++ b/docs/Modding/plugins/exposed-interfaces/index.md @@ -0,0 +1,11 @@ +# Available interfaces + +!!! note + + Not every interface is documented here. + + Feel free to add more or tell us which ones are missing. + +Multiple binaries of the game expose interfaces you can use. + +This section aims to document all of them incrementally. \ No newline at end of file diff --git a/docs/Modding/plugins/exposed-interfaces/northstar.md b/docs/Modding/plugins/exposed-interfaces/northstar.md new file mode 100644 index 00000000..65be10fd --- /dev/null +++ b/docs/Modding/plugins/exposed-interfaces/northstar.md @@ -0,0 +1,31 @@ +# Northstar + +All interfaces exposed by Northstar. + +## ISys + +!!! cpp-class "ISys" + + Provides basic functions to interact with northstar. + + !!! cpp-function "void Log(HMODULE pluginHandle, LogLevel level, char* msg)" + + Log a message using the console managed by northstar. + + Messages logged using this method are prefixed with the `LOG_NAME` retrieved via your `PluginId` implementation and printed to the game's console. + + !!! cpp-function "void Unload(HMODULE pluginHandle)" + + Gracefully unload a plugin (usually your own plugin) + + !!! cpp-function "void Reload(HMODULE pluginHandle)" + + Reload a plugin. The plugin can know if it has been reloaded via the `reloaded` parameter passed in the `Init` callback exposed in the `IPluginCallbacks` Interface. + +!!! cpp-enum "LogLevel : i32" + + !!! cpp-member "INFO = 0" + + !!! cpp-member "WARN = 0" + + !!! cpp-member "ERR = 0" \ No newline at end of file diff --git a/docs/Modding/plugins/index.md b/docs/Modding/plugins/index.md new file mode 100644 index 00000000..ec59a760 --- /dev/null +++ b/docs/Modding/plugins/index.md @@ -0,0 +1,75 @@ +# Plugins + +!!! note + + Writing plugins require some very basic familiarity with C++ + + +Plugins are native modules loaded by Northstar. Because they're not limited to any API provided by the game, plugins are a powerful tool to enable you to do almost anything you could with a custom Northstar Client. + +For example, you could write a plugin that [compiles maps](https://github.com/catornot/furnace) at runtime, allows squirrel scripts to save and load recorded animations [from disk](https://github.com/uniboi/recanim) or provide [discord rpc](https://github.com/R2Northstar/NorthstarDiscordRPC) integration. + +However because plugins have full access to your system plugins should be treated with caution and regular scripts are preferred if possible. + +## Installation + +Any `.dll` file located in the directories `/plugins/` and `/packages//plugins/` will be attempted to load as a plugin. + +To manually install a plugin, copy the plugin `.dll` into the `/plugins/` directory. + +!!! note + + The default northstar profile name is `R2Northstar`. Profiles are located in the same location as `NorthstarLauncher.exe`. + + +Any Plugin is only valid if it exposes every required [interface](interfaces.md). + +If a plugin does not expose every Interface the Launcher requires, it is unloaded immediately. + +## Development + +To debug a plugin it's recommended to compile Northstar itself or download the debug data from the [Release Page](https://github.com/R2Northstar/NorthstarLauncher/releases). + +When developing a plugin it's usually easier to output the binary directly in the `/plugins/` directory, the `packages/` directory is usually only used for complete packaged mods downloaded from Thunderstore. + +### Valid plugins + +Every plugin must fulfill these criteria to be considered valid by Northstar. + +Invalid plugins get unloaded as soon as possible. + +- The plugin module must export a function named [`CreateInterface`](interfaces.md/#createinterface) that returns an interface instance when called. + +- Every [required interface](required-interfaces.md) must be exposed via `CreateInterface` + +### Debugging + +If you know how to use a debugger you can safely skip this. + +For simplicity it's assumed your development environment is Visual Studio and you're compiling Northstar from source. + +Otherwise are the steps basically the same everywhere else. + +1. Clone the [Launcher](https://github.com/R2Northstar/NorthstarLauncher) + +2. Set the binary output directory to the location wherever you have installed your game. The compiled `NorthstarLauncher.exe` should be outputted next to `Titanfall.exe`. + +3. Launch `NorthstarLauncher.exe` with a debugger or attach a debugger to a running process + +4. Load debug symbols of your plugin(s) (for example `myplugin.pdb`) in the debugger + +That's it. + +### Hooks + +Any Plugin can install hooks anywhere in memory it desires. However, plugins should avoid hooks if possible because of conflicts with Northstar itself or hooks installed by other plugins. + +It is not generically possible to determine if a given address has already been hooked by a different plugin and there is currently **no mechanism for plugins to share information which addresses have hooks installed**. + +Northstar does not expose an interface to (un)install hooks. Every plugin is expected to use their own provider. + +## Examples and Libraries + +- A small plugin [""framework""](https://github.com/uniboi/NSPluginTemplate/) to show the basics of writing plugins and Squirrel bindings in C + +- A complete [Rust plugin library](https://crates.io/crates/rrplug) that allows you to easily integrate safely with large parts of the engine itself diff --git a/docs/Modding/plugins/interfaces.md b/docs/Modding/plugins/interfaces.md new file mode 100644 index 00000000..7c030605 --- /dev/null +++ b/docs/Modding/plugins/interfaces.md @@ -0,0 +1,158 @@ +# Interfaces + +!!! note + + The term "Interface" sometimes describes both an abstract C++ class or an instance of a class that implements the abstract class (Interface). + +The interface API of Northstar Plugins is modelled after [Source Interfaces](https://developer.valvesoftware.com/wiki/Category:Interfaces). For examples how interfaces are created in-engine, you can check search leaked code of source engine games [using these macros to expose an interface](https://sourcegraph.com/github.com/perilouswithadollarsign/cstrike15_src@f82112a2388b841d72cb62ca48ab1846dfcc11c8/-/blob/public/tier1/interface.h?L86-148). + +For example, [search](https://sourcegraph.com/search?q=repo:%5Egithub%5C.com/perilouswithadollarsign/cstrike15_src%24+EXPOSE_SINGLE_INTERFACE_GLOBALVAR+&patternType=keyword&sm=0) for `EXPOSE_SINGLE_INTERFACE_GLOBALVAR` for exposed single static pointers to interface implementations. + +This design choice of interface ABI layout was chosen to mimic interfaces binaries of Titanfall 2 exposes and to provide one uniform Interface API between the game, Northstar and any Plugin. + +Therefore any plugin is not restricted to interfaces exposed by Northstar but can use the entire suite the game and other plugins offer. + +## Memory Layout + +!!! warning + + In some cases, different compilers use different layouts for virtual function tables. + + If you're using a compiler that is not msvc, either change your interface or use structs with an explicit vtable member. + + If your plugin is not compiled with C or C++ **make sure the structure follows the C ABI** + +Because interface instances are C++ class instances that extend a virtual class, every instance has a [vtable](https://en.wikipedia.org/wiki/Virtual_method_table) pointer at offset `0`. + +### VTables + +!!! note + + Methods interfaces are always documented in the order they appear in the vtable. + +A vtable is a list of pointers to methods of that interface. + +The first parameter of these methods is always a pointer to the instance. In C++ this parameter is passed implicitly. + +For example, the method [`ISys::Reload`](https://github.com/R2Northstar/NorthstarLauncher/blob/71349f05b69923dbf091d27f8e256bcc3022e859/primedev/plugins/interfaces/sys/ISys.h#L18) has two parameters: `ISys*` and `HMODULE`, even though only one is defined explicitly. Keep this in mind when you're writing plugins not in C++. + +The complete [`ISys`](exposed-interfaces/northstar.md#isys) interface instance layout looks like this in C: + +```c +typedef struct CSys { + struct { + // the vtable contains all methods of this interface + void (*log)(struct CSys *self, HMODULE handle, LogLevel level, const char *msg); + void (*unload)(struct CSys *self, HMODULE handle); + void (*reload)(struct CSys *self, HMODULE handle); + } *vftable; // the instance only contains a pointer to the vtable +} CSys; + +// CSys* sys; +// sys->vtable->log(sys, handle, 0, "hello world"); +``` + +!!! error + + The first parameter (implicit instance pointer) **is omitted in the documentation of interfaces**, remember to include the parameter if you're implementing a plugin in a language that is not C++. + +An interface may include more than one vtable, depending on the definition. Usually one for every class it extends. + +## CreateInterface + +Every plugin, northstar and some binaries of the game expose a set of interfaces via `CreateInterface`. + +!!! cpp-function "void* CreateInterface(char* name, int* status)" + + **Parameters:** + + - `[in] char* name` - null terminated name of the interface requested + + - `[out, optional] int* status` - a pointer to an int where the status code is written to. `0` means an interface was created, `1` means no interface could be instantiated. + + **Returns:** + + - A pointer to the instantiated interface or NULL if failed. + +`CreateInterface` is an exported function. The address of it can be obtained via [`GetProcAddress`](https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress) + +## Exposing an interface + +Exposing an interface means in practice that it has to be possible to obtain a pointer to an instance if `CreateInterface` of your plugin is called for the respective interface name. + +In the source engine this is implemented as a linked list, however you're free to implement this however you want. + +In essence, this is what the functionality of `CreateInterface` boils down to: + +```c +void* CreateInterface(char* name, int* status) { + if(strcmp(name, "IPluginId001") == 0) { + if(status) *status = 0; + return g_IPluginId; + } + + if(strcmp(name, "IPluginCallbacks001") == 0) { + if(status) *status = 0; + return g_IPluginCallbacks; + } + + // repeat for every interface you expose ... + + if(status) *status = 1; + return 0; +} +``` + +## Interface Names + +An interface name is a unique string associated with an interface. Names consist of a) the interface name and b) the version of the interface (usually 3 digits in decimal). + +Every interface is expected to be fully backwards compatible. This means in practice, that (public) members can only be added to an interface, not removed. + +For example, consider this fictional interface `ILog`: + +```c++ +class ILog { + public: + virtual void print(char* msg) = 0; +} +``` + +The vtable would like this: + +```c +struct ILog_vtable { + void* print; +}; +``` + +This interface would be called `ILog001`, because it's the first version of the interface. + +Now we want to extend the interface to also expose a function called `fmt` to format a string: + +```c++ +class ILog { + public: + virtual void print(char* msg) = 0; + virutal void fmt(char* fmt, ...) = 0; +} +``` + +This would turn the vtable into this: + +```c +struct ILog_vtable { + void* print; + void* fmt; +}; +``` + +Now we would call this interface `ILog002` because it's the second iteration of the interface. + +!!! warning + + Because you are only allowed to increase the size of an interface while keeping the offsets of all previous members the same, the interfaces **`ILog002` is backwards compatible with `ILog001`**. + + In practice this means that you only need to expose the newest version of an interface. + + It is expected of [CreateInterface](#createinterface) to return an instance, even if only a newer version of that interface is exposed. diff --git a/docs/Modding/plugins/interfacesapi.md b/docs/Modding/plugins/interfacesapi.md deleted file mode 100644 index d7b912bd..00000000 --- a/docs/Modding/plugins/interfacesapi.md +++ /dev/null @@ -1,98 +0,0 @@ -# Interface API - -the plugins system now use source interfaces. - -The launcher exposes almost everything required by plugins in interfaces that allow for backwards compatibility. -The only thing that's passed to a plugin directly is the northstar dll HWND and a struct of data that's different for each plugin. - -Plugins are required to expose a `void* CreateInterface(const char* name, int* status)` function to share their own interfaces. -The launcher loads the `PluginId` interface from the plugin to query info such as it's name. - -Plugins can use the `CreateInterface` function exposed by the northstarDll to use northstar interfaces such as for logging. -An interface is just an abstract class to force all functions into a vftable. - -## Northstar Interfaces - -### NSSys001 - -Exposes some system functionality to plugins - -```cpp -// 32 bit -enum LogLevel { - INFO, - WARN, - ERR, -}; - -// handle: handle of the plugin. Passed to the plugin on init. -void Log(HMODULE handle, LogLevel level, char* msg); // logs a message with the plugin's log name -void Unload(HMODULE handle); // unloads the plugin -void Reload(HMODULE handle); -``` - -## Required Plugin Interfaces - -Interfaces that have to be exposed for the plugin to be loaded. - -### PluginId001 - -```cpp -// strings of data about the plugin itself. may be extended in the future -// 32 bit -enum PluginString { - NAME, // the name of the plugin - LOG_NAME, // the name used for logging - DEPENDENCY_NAME, // the name used for squirrel dependency constants created. The value returned for this has to be a valid squirrel identifier or the plugin will fail to load -} - -// bitfields about the plugin -// 32 bit -enum PluginField { - CONTEXT // 0x1 if the plugin is allowed to run on dedicated servers and 0x2 if the plugin is allowed to run on clients (is this even needed seems useless to me) -} - -char* GetString(PluginString prop); -i64 GetField(PluginField prop); -``` - -### PluginCallbacks001 - -```cpp -struct PluginNorthstarData { HMODULE handle; }; - -// COPY THE initData IT MAY BE MOVED AT RUNTIME -void Init(HMODULE nsModule, const PluginNorthstarData* initData, bool reloaded); // called after the plugin has been validated. The nsmodule allows northstar plugins to work for the ronin client as well (assuming they update their fork lmao) -void Finalize(); // called after all plugins have been loaded. Useful for dependencies -void Unload(); // called just before the plugin is getting unloaded -void OnSqvmCreated(CSquirrelVM* sqvm); // the context of the sqvm is contained in the instance -void OnSqvmDestroying(CSquirrelVM* sqvm); // callback with the sqvm instance that's about to be destroyed (for UI, CLIENT is destroyed for some reason??) -void OnLibraryLoaded(HMODULE module, const char* libraryName); // called for any library loaded by the game (for example engine.dll) -void RunFrame(); // just runs on every frame of the game I think -``` - -## What's an interface anyways? - -Interfaces are just abstract classes. So make sure the first parameter is always a pointer to the instance of the interface you're using. - -an example what NSSys001 looks like in C: - -```cpp -typedef enum { - LOG_INFO, - LOG_WARN, - LOG_ERR, -}; - -typedef struct CSys { - struct { - void (*log)(struct CSys* self, HMODULE handle, LogLevel level, char* msg); - void (*unload)(struct CSys* self, HMODULE handle); - }* vftable; -} CSys; - -// use like this -g_c_sys->vftable->log(g_c_sys, g_handle, LOG_INFO, "my balls are itching"); -``` - -Interfaces are created with CreateInterface that's exposed in another dll. diff --git a/docs/Modding/plugins/required-interfaces.md b/docs/Modding/plugins/required-interfaces.md new file mode 100644 index 00000000..f42149b1 --- /dev/null +++ b/docs/Modding/plugins/required-interfaces.md @@ -0,0 +1,136 @@ +# Required Interfaces + +!!! error + + Remember that the first parameter of any function for every interface is always a pointer to an instance of that interface + + **The first implicit parameter is omitted in this documentation for simplicity and C++ code parity** + +!!! warning + + Every enum in this file should be treated as non exhaustive and members can be added in future Northstar versions. + +Some interfaces are required to be exposed by every plugin. + +Plugins without all of these interfaces will be unloaded immediately. + +## IPluginId + +!!! note + + Current latest version: `IPluginId001` + +The IPluginId provides information about the plugin itself, like the name and when to run. + +!!! cpp-class "IPluginId" + + !!! cpp-function "char* GetString(PluginString id)" + + Returns a string associated with the id, or `0` if the id is unknown. + + !!! cpp-function "int64_t GetField(PluginField id)" + + Returns a number associated with the id, or `0` if the id is unknown. + +!!! cpp-enum "enum PluginString : i32" + + To distinguish what string is requested with `IPluginId::GetString` + + !!! cpp-member "NAME = 0" + + The name of the plugin + + !!! cpp-member "LOG_NAME = 1" + + The prefix to use for logs in northstar. Use 8 characters to align with northstar prefixes. + + !!! cpp-member "DEPENDENCY_NAME = 1" + + For every loaded plugin a squirrel global constant is registered with this value. Has to be a valid squirrel identifier. + + The constant can be used to control when a script is compiled and can be used in the "RunOn" field of the mod.json or in a script directly + + ```squirrel + #if MYPLUGIN + print("my plugin is loaded"); + #else + print("my plugin is not loaded :("); + #endif + ``` + +!!! cpp-enum "enum PluginField : i32" + + To distinguish what integer is requested with `IPluginId::GetField` + + !!! cpp-member "CONTEXT = 0" + + A bitfield used to determine if the plugin should loaded in the current context. + + - DEDICATED: 0x1 + + Keep the plugin loaded on dedicated northstar servers + + - CLIENT: 0x3 + + Keep the plugin loaded on dedicated northstar clients + + !!! cpp-member "COLOR = 1" + + Determines the color used for the log prefix of the plugin. + + If all channels are 0, Northstar will use a default color. + + * \[bits 0-8\]: RED + + * \[bits 8-16\]: BLUE + + * \[bits 16-24\]: GREEN + +## IPluginCallbacks + +!!! note + + Current latest version: `IPluginCallbacks001` + + +For some commonly used events northstar provides callbacks. + +!!! cpp-class "IPluginCallbacks" + + !!! cpp-function "void Init(HMODULE northstarModule, const PluginNorthstarData* initData, bool reloaded)" + + Called after the plugin has been loaded and validated. + + Use [DllMain](https://learn.microsoft.com/en-us/windows/win32/dlls/dllmain) to initialize systems required for the validation of your plugin, like registering all interfaces for your `CreateInterface` export. + + !!! cpp-function "void Finalize()" + + Called after all plugins are initialized + + !!! cpp-function "void Unload()" + + Called right before this plugin is unloaded + + !!! cpp-function "void OnSqvmCreated(CSquirrelVM* sqvm)" + + Called after an sqvm is created + + !!! cpp-function "void OnSqvmDestroying(CSquirrelVM* sqvm)" + + Called just before the sqvm is destroyed + + !!! cpp-function "void OnLibraryLoaded(HMODULE module, char* name)" + + Called whenever any module is loaded, either by northstar, the game or any plugin + + !!! cpp-function "void RunFrame()" + + Called every game frame + +!!! cpp-class "PluginNorthstarData" + + Data that's useful for the initialization of your plugin + + !!! cpp-member "HMODULE pluginHandle" + + The handle of the initializing plugin diff --git a/docs/Modding/plugins/resources.md b/docs/Modding/plugins/resources.md deleted file mode 100644 index 466b6f73..00000000 --- a/docs/Modding/plugins/resources.md +++ /dev/null @@ -1,9 +0,0 @@ -# Resources - -## Templates - -* **NSPluginTemplate:** https://github.com/uniboi/NSPluginTemplate/ - -## Libs - -* **rrplug:** https://crates.io/crates/rrplug