Variable Name | Type | Description | Default Value |
---|---|---|---|
ASSEMBLY | string | Full path to your assembly/dll (without extension) | none |
ENTRYPOINT | string | Full type name containing static entrypoint method. | NWN.Internal |
METHOD | string | Name of entrypoint method to call. Method must be static void without any parameters. | Bootstrap |
NETHOST_PATH | string | Path where to load libnethost.so from | none |
You will need dotnet-sdk
installed on the server to use this plugin. The same package is used to build the managed code. Installation instructions are here.
There are no compiletime dependencies for the unmanaged code.
Copy everything within [DotNET/NWN/...](NWN) into a folder that will be your module's root. Then, in that folder run dotnet build
. This will produce several files, most notably:
NWN.dll
- your compiled codeNWN.runtimeconfig.json
- config file that tells nwnx how to run your codeIn your startup script, you need to minimally set up the assembly path (note: without .dll extension):
export NWNX_DOTNET_ASSEMBLY=/full/path/to/your/compiled/NWN
And then start NWNX+NWServer.
If you get the following error:
Unable to load libnethost.so. .NET plugin will be unavailable.
make sure you have dotnet-sdk-3.1
(or later?) installed on your system. If it is present, run locate libnethost.so
to find the location of the library. Then add it to:
export NWNX_DOTNET_NETHOST_PATH=/full/path/to/libnethost.so
Please also open an issue showing the output of your locate
command so that we can add the path to the auto-search code.
If you get the following error:
Unable to load runtime config [...]
verify your NWNX_DOTNET_ASSEMBLY
variable - it needs to contain the path to your DLL, with the base name, but without the extension. So, if you have:
/some/path/my/project/NWN.dll /some/path/my/project/NWN.runtimeconfig.json /some/path/my/project/NWN.deps.json /some/path/my/project/NWN.pdb
your NWNX_DOTNET_ASSEMBLY
should be /some/path/my/project/NWN
If you get the following error:
Unable to get [something].[something]() function [...]
and you have changed the default entrypoint, verify that your NWNX_DOTNET_ENTRYPOINT
contains the full Namespace.Class.Subclass
, and that NWNX_DOTNET_METHOD
is declared as a static void
method with no arguments.
E.g. NWNX_DOTNET_METHOD=Bootstrap
-> public static void Bootstrap()
In Entrypoints.cs you have three events that you need to implement:
This event is called only once, when the dotnet runtime has been initialized. Use this event to initialize any static data you might have.
This event runs whenever a named script - i.e. event scripts set in the toolset, or with SetEventScript
- is to be executed. This runs before any nwscript scripts, and runs for all scripts, including stock bioware ones.
If you do not want to handle this script, and let the regular nwscript do it (e.g. for nw_
,x0_
and x2_
scripts), you should just return -1
or SCRIPT_NOT_HANDLED
. Otherwise, it's up to you to dispatch however you want, and return >=0
. The return value is used in case of StartingConditional
scripts.
oidSelf
is the handle of the object running the script. You can also access this object with NWScript.OBJECT_SELF
.
Dictionary<string, RunScriptHandlerDelegate>
to register functions that handle scriptsThis is called every main loop frame (approximately/ideally every 10ms). It runs in script context (so NWScript functions are available), but OBJECT_SELF == OBJECT_INVALID
.
You should keep the processing in this function to a minimum, as it may have adverse effect on server performance.
All classic NWScript functions are available in the NWN.NWScript
class.
For example:
All the managed code provided in this repo is meant just as a primer, You are encouraged to copy it and adapt to your own needs.
When talking to unmanaged code, we only deal with basic types. Objects are passed as uint
s. Other engine structures, like Effect
,Location
, etc, all have IntPtr Handle
which is passed instead.
Engine structures like Effect
/Location
have a backing unmanaged structure, and need to be cleaned up after usage. It is recommended to have a wrapper like this to allow the native C# garbage collector to handle it for you:
The basic interop between unmanaged NWNX and managed module code is entirely contained within Bootstrap.cs. Your managed DLL needs to have this function:
exposed. By default, NWNX will look for the Bootstrap()
function in NWN.Internal
, but you can override that through the NWNX_DOTNET_ENTRYPOINT
and NWNX_DOTNET_METHOD
environment variables.
In your bootstrap code, you will need to register a set of method handlers that will be called by NWNX/NWN to execute your C# code.
For this, we use the RegisterHandler
methods exposed by NWNX_DotNET. This can be called through Platform Invoke (PInvoke).
At minimum, it is recommended to register the main loop, run script, closure and signal handlers:
This plugin exposes various functions and utilities that can be used through platform invoke.
Functions in DotNETExports.cpp declared with NWNX_EXPORT
can be accessed by declaring an extern function in C#, with an appropriate DllImport
attribute.
Example:
The OBJECT_SELF
global is handled in the managed code. In the OnRunScript
handler, the argument oidSelf
is assigned to the global. Before the handler finishes, the old value is restored.
You can find implementation details in Internal.cs, where a Stack<>
is used to keep track of nested script calls.
A closure is a script context that is saved to be executed at a later time - DelayCommand
, AssignCommand
, etc. The closure support in dotnet is almost entirely handled on the managed side. The three native functions you'd use to schedule a closure are:
The oid
is the ID of the object that will run the closure (usually OBJECT_SELF
for DelayCommand
). The eventId
is any ulong
tag given to this closure that will then be handed back. The native code will then just schedule {oid, eventId}
pair to execute at the given time. When it executes, it will call back into the closure handler that was registered with RegisterClosureHandler
The sample call implements closures in Internal.cs, as:
Exceptions work within the managed code, however they do not cross the managed/unmanaged boundary. This means that uncaught exceptions will bring down the entire nwserver process. It is advisable to wrap most of your logic in try...catch
blocks in all three entry points (See Internal.cs for examples).
Errors which happen while executing unmanaged code invoked through one of the bootstrap delegates will also bring down the server (e.g. by passing invalid IntPtr Handle
for engine structures)