For a quick guide on using NoxScript, see quickstart.
Some of your questions may be answered already in Q&A.
There are examples available as well.
If your question is not covered, please send a question here
or in our Discord.
Subsections of NoxScript
Quickstart
This guide will show how to create your first map script using OpenNox map script API in Go language.
First steps
Note
Make sure you have OpenNox installed. Vanilla Nox, Reloaded or other version won’t work!
To create your first map script, copy one of the existing map folders, or create your own in the editor.
If you copied the map, make sure to rename files inside the folder accordingly.
The way these runtime works is that usually one of them is the main runtime (NS4 in this case),
which provides all functionality available in OpenNox. Other runtimes are only a compatibility layer on top of it.
But an important one! They provide an already familiar interface for map developers (e.g. NS3 or EUD).
Because of this, it is safe to mix and match the runtimes. If you are experienced in NS3 - start from it.
If you are new to Nox scripting - pick the latest NS runtime (NS4).
Links above will link to Go language documentation for each runtime - this is the main source of documentation
for the functions and classes available in each runtime. This guide will only provide basic knowledge about the setup.
Using runtimes
Now, how to enable these runtimes?
In case you skipped the full setup, it’s only a matter of adding an import in a Go file and using it:
Notice how we are calling IsTalking without the ns3 prefix.
This is, however, not typical to Go, since it’s not clear if IsTalking is defined in your code or in the other package.
Now, going back to the full IDE setup, you may have noticed that it doesn’t recognize the imports.
This is because we need to update project dependencies in go.mod. We can do it with either of two ways:
go mod tidy from the terminal (requires Git).
Or hover over the unrecognized import name, select Quick fix..., and go get ... there.
In both cases it should download the dependency (which is the NS runtime you selected) and will add it to go.mod.
Now you should see autocomplete working properly on these runtime packages.
Updating runtimes
Script runtimes are updated periodically in OpenNox to expose new functionality or fix bugs.
This is done automatically, new OpenNox releases will enable new runtimes without any actions needed from the map developer.
However, the IDE will not see new functions in the new runtime version, unless it is updated in the map project.
The easiest way to update is to open go.mod file in the map folder and find the line with the runtime you want to update:
require (
github.com/opennox/noxscript/ns/v4 v4.3.0
// ... other lines
)
Just update the version there, and run go mod tidy.
The full list of version is available by clicking on the version in the script package documentation
and on the GitHub.
Packages
Now we know how to import runtimes and run scripts. How should we structure the code, though?
In Go, a unit of distribution is a “package”: a directory with one or more Go files.
Names of Go files do not matter, the only requirement is that all files in a directory must have the same
package directive at the top. OpenNox enforces additional limitation: package should match the map name.
Apart from that, the code organization is up to you. All functions and variables defined in one file will be
available for all other files in the same directory.
Ho do I …
This guide barely scratches the surface, and only show a few first steps.
If you are already familiar with original NoxScript 3, check the NS3 migration guide.
You may probably want to get a bit more familiar with Go. The language is very simple, so it should be straightforward.
An interactive Go tour is a great place to start.
Some of your questions may be answered already in Q&A.
There are examples available as well.
If your question is not covered, please send a question here
or in our Discord.
Examples
This page lists some examples of the scripts used in OpenNox.
Here you can find a full list of functions provided by NS3 runtime.
History
Vanilla Nox has its own script runtime based on compiled source files. Since original Nox editor was never released,
community recreated the script compiler from scratch.
Since there’s no official version, the scripts had different dialects, one of the latest is known as NoxScript 3.
There’s also a Panic’s EUD compiler that uses memory manipulation to extend script functionality.
OpenNox supports vanilla scripts with no changes required. Additionally, it implements a more advanced script runtime.
New runtime
To make the transition to the new script runtime smoother, OpenNox provides a compatibility layer for NoxScript 3.
The idea is that all the functions from NoxScript 3 should work exactly the same,
accept same arguments, etc. This way all existing functions can be copied with almost no changes (only syntax will vary).
To avoid confusion, we usually refer to vanilla scripts as “NoxScript 3”, while the new compatibility layer is called NS3.
If you have existing scripts you want to migrate, see our migration guide.
Otherwise, it’s better to start directly from the NS4, which is our current script version.
Subsections of NoxScript 3
Migrate from original
This guide will help migrate existing NoxScript 3
maps to NS3 in OpenNox.
In this guide we will refer to “NoxScript 3” as the original Nox script, while “NS3” as the new Go-based scripts for OpenNox.
Why?
First logical question: why bother? OpenNox runs original NoxScript 3 just fine.
A few reasons to migrate to OpenNox scripts:
NoxScript 3 is limited to what original Nox engine exposes. Which isn’t a lot. There won’t be any updates.
NS3 in OpenNox is a drop-in replacement. Same functions are supported.
We provide tooling to migrate 90% of your code automatically. Only minor tweaks are needed.
NS4 (and beyond) will have more and more features going forward, including modding support.
You won’t need a separate script compiler. Scripts are run directly from source.
More comprehensive type system: proper arrays, dictionaries (map), structures, classes.
Libraries included: string manipulation, full UTF8 support, math, sorting, etc.
Better performance: all libraries are compiled into OpenNox and run natively.
Better debugging: if script crashes, there’s a stack trace. Scripts can recover from it as well!
Go language used in scripts is used in OpenNox itself. Maybe one day you’ll decide to contribute?
It wouldn’t be fair to not list downsides:
It only works with OpenNox.
You’ll need to learn a new scripting language.
Script may need to do more work when saving/loading.
If you heard about EUD script compiler by Panic and maybe considered it, see this guide.
In general, we believe that OpenNox is the future of Nox modding, thus porting your map may give it an entirely new life!
How?
There are two path currently: converting the compiled script from the map or converting the source.
Converting the map script
You’ll need a recent noxtools installed (assuming you have Go 1.20+ installed):
go install github.com/opennox/libs/cmd/noxtools@latest
From the map directory:
noxscript ns decomp <mapname>.map
It will print a decompiled source code converted to Go and NS3 runtime.
Because of the limitation of Nox script assembly:
All variable names will be lost.
Some control flow may be replaced with goto.
But after fixing these issues, you should be ready to go!
Converting the source
TODO: Add a guide for using cxgo to automate it.
Currently, you’ll have to manually convert the source.
Until we automate it as well, please consider converting the extracted map script, as shown above.
NoxScript 3 is similar to C, which has a lot in common with Go.
However, Go syntax is slightly different in regard to types.
Conversion steps will include:
Swapping argument and variable names with types: int a -> a int.
Adding either var or const to variable definitions: int a -> var a int.
Moving function return type to the end: int foo() -> func foo() int.
Moving { to the previous line. E.g. func foo()\n{ -> func foo() {. Same for if, else, for.
Adding { ... } to if and else which do not have them: if (1) foo() -> if (1) { foo() }.
Removing void from returns.
Fixing variable types (Go doesn’t allow implicit type conversion).
After this, add the following header to your file:
Dot import should automatically resolve all references to NS3 functions.
After conversion
Limitations
There are some temporary limitations you should be aware of:
Timers will stop each time the map is reloaded. You’ll need to restart them from the script.
All callbacks will reset when map is reloaded. You’ll need to set them again from the script.
These issues will be resolved eventually.
New: Syntax
We highly recommend checking our Go tour to get familiar with the syntax, but we’ll give a short recap here.
File structure
All files must start with package <mapname>:
packageexample
It can be followed by one or more package imports:
import"fmt"import"strconv"// it is typical to group them:
import("fmt""strconv")
Then global variables and/or functions follow. Order of declarations doesn’t matter.
Variables and types
Most notable syntax distinction: in Go, the type name is on the right side, instead of on the left as in NoxScript 3:
varxint
intx;
Note that ; is no longer needed, and variable declaration must start with var (or const).
There’s a very good reason why types are on the right: it makes reading complex types more natural.
Just read them left to right!
For example, array: var x [3][2]int simply reads left to right as “array of 3 elements, each containing 2 int values”.
Much better than a random order of int x[2][3];.
Same for pointers: *[2]int reads “pointer to an array of 2 ints”. Compare it to int* x[2];.
Functions
Function declarations are also different:
They must start with func.
Types for arguments are on the right.
Return type is after the arguments.
Void return type must be omitted.
The opening {must be on the same line as the function header.
Arguments with the same types can be grouped.
Multiple returns are supported.
funcfoo(aint){}funcbar(x,yint,sstring)int{}
voidfoo(inta){}intbar(intx,inty,strings){}
Control flow
All control flow structures require the opening { to be on the same line, for example:
if(x){foo()}if(y){bar()}
if(x)foo();if(y){bar();}
The () in the condition is also optional:
ifx{foo()}ify{bar()}
if(x)foo();if(y){bar();}
Same rules for { apply for else:
ifx{foo()}else{bar()}
if(x)foo()elsebar()
Loops
Loops must not include ( and have same rules in regard to {:
variintfori=0;i<10;i++{}
inti;for(i=0;i<10;i++){}
Loop that use whilemust use for keyword:
forx{}
while(x){}
New: Core types
In original NoxScript 3, there were only a few types available: int, float, string, object.
In NS3 the list is much longer: bool, int, uint, float32 (analog of float), string, any, etc.
The object type is replaced with more specific types from NS3 package:
ObjectID,
ObjectGroupID,
WaypointID, etc.
An important distinction is that Go doesn’t allow implicit type conversion.
For example, in NoxScript it was okay to have an int variable and put an object there.
In NS3, this is requires an explicit type conversion: int(x). But, of course, it’s better to have correct types for your variables.
Another distinction of NS3 is the support of direct conversions between int and float.
It is done the same way: int(x) or float32(x).
Converting between int and string is supported via IntToString,
but it’s better to use Go’s standard library instead: strconv.Itoa.
It also supports conversion from string to int via Go’s standard library: strconv.Atoi.
Note, that this function may return an error, which you are free to ignore (with _):
x,_:=strconv.Atoi("1")
New: Strings
NoxScript 3 had a limitation that a frequent + operation on strings overflows a string table.
There’s no such limitation in NS3: any number of strings can be created.
Printing to strings is also supported with Go’s fmt.Sprintf:
Even though strings can be created with + and individual bytes can be accessed with s[i],
strings are immutable in Go! This means, once created, they cannot be edited, only new ones can be created.
If you need to change a few characters, consider converting to byte array, making changes, and converting back:
In NoxScript 3, only fixed arrays are supported. NS3 has support for Go slices, which are arrays with dynamic length:
vararr[]intfori:=0;i<3;i++{arr=append(arr,i+1)// adds elements to the end
}// len(arr) == 3
inti;intarr[3];for(i=0;i<3;i++){arr[i]=i+1;}
New: Structures
NS3 completely supports custom struct types. They are very similar to classes in other programming languages.
Let’s say we want to build an RPG map, and we want to record a new character class for all player units:
typeMyUnitstruct{IDns.ObjectID// types on the right: field "ID" with type "ns.ObjectID"
Classstring}funcinit(){unit:=ns.Object("Bob")// creates a new struct instance, takes pointer to it
myUnit:=&MyUnit{ID:unit,Class:"archer"}changeClass(myUnit,"shaman")}// changeClass accepts a pointer to struct be able to change fields.
// Removing the pointer will make a copy of the struct for this function!
funcchangeClass(unit*MyUnit,clstring){unit.Class=cl}
The changeClass function can also be rewritten as a method on MyUnit struct:
typeMyUnitstruct{IDns.ObjectID// types on the right: field "ID" with type "ns.ObjectID"
Classstring}// changeClass is a method on MyUnit struct pointer.
// Removing the pointer will make a copy of the struct for this function!
func(u*MyUnit)changeClass(clstring){u.Class=cl}funcinit(){unit:=ns.Object("Bob")myUnit:=&MyUnit{ID:unit,Class:"archer"}// now changeClass is available as a method on the struct instance
myUnit.changeClass("shaman")}
For developer coming from C, new structs always have their fields initialized to zero vales.
New: Dictionaries (maps)
NS3 support dictionaries/sets, which are unordered collections of values indexed by a key of any type.
For example, we made a MyUnit struct in the previous example. But how to quickly find MyUnit for ns.ObjectID?
typeMyUnitstruct{IDns.ObjectID// types on the right: field "ID" with type "ns.ObjectID"
Classstring}// mapUnits maps ns.ObjectID to *MyUnit.
// All maps must be created with make before use!
varmapUnits=make(map[ns.ObjectID]*MyUnit)funcinit(){createMyUnits()findAndChangeClass()}funccreateMyUnits(){unit:=ns.Object("Bob")myUnit:=&MyUnit{ID:unit,Class:"archer"}// add new record to the map, index by ID
mapUnits[unit]=myUnit}funcfindAndChangeClass(){unit:=ns.Object("Bob")// find by ID
myUnit:=mapUnits[unit]ifunit==nil{return// not found
}myUnit.Class="shaman"}
Keys can also be deleted from the map:
delete(mapUnits,unit)// delete by unit ID
Panic's EUD
History
Vanilla Nox has its own script runtime based on compiled source files. Since original Nox editor was never released,
community recreated the script compiler from scratch.
Since there’s no official version, the scripts had different dialects, one of the latest is known as NoxScript 3.
However, the script runtime of Nox was very limited. Eventually the community found a way to inject custom code into the
game, which lead to a new kind of map scripts, usually referred as “memhacks”.
Panic’s EUD is the most advanced project of this kind.
It uses memory manipulation to implement a lot of new functionality for the scripts.
Unfortunately, memory manipulation usually targets a single binary version (vanilla Nox), and thus cannot run on
other versions such as OpenNox. Because of this OpenNox does not support EUD scripts.
New runtime
Even though OpenNox cannot support scripts that use memory manipulation, we still can provide similar functions
in out own script runtime. Thus, OpenNox provides a compatibility layer for EUD scripts.
The idea is that all the functions from Panic’s EUD which do not
expose the memory directly should work exactly the same, in OpenNox
This way all existing functions can be copied with almost no changes (only syntax will vary).
If you have existing scripts you want to migrate, see our migration guide.
Otherwise, it’s better to start directly from the NS4, which is our current script version.
Subsections of Panic's EUD
Migrate from EUD
This guide will help migrate existing Panic’s EUD
maps to NS3 and
EUD layer in OpenNox.
First, it’s important to understand that “memhacks” and direct memory access is technically not possible in OpenNox.
Here are a few reasons why:
Memhacks target a specific binary. This means it only works on one Nox version.
OpenNox constantly evolves, thus we cannot guarantee any specific memory layout.
Memhacks usually inject assembly code that targets a specific CPU architecture (Intel/AMD x86).
OpenNox can be potentially compiled for ARM (e.g. MacBooks, Android, RPi), thus script will stop working there.
We attempted to resolve these issues with EUD developer Panic, but we don’t see any willingness for collaboration
from his side. If you really want EUD to be supported directly in OpenNox, please reach out to Panic.
Resolving these technical challenges is possible, but requires tight collaboration, and will to do so from him, first and foremost.
Why?
First logical question: why bother with converting to OpenNox? EUD works great in original Nox.
A few reasons to migrate to OpenNox scripts:
OpenNox is evolving and supports new features that are impossible in vanilla (e.g. HD, better multiplayer, modding, etc).
EUD library in OpenNox is a drop-in replacement. Same functions will be supported.
NS4 (and beyond) will have even more features going forward.
You won’t need a separate script compiler. Scripts are run directly from source.
More comprehensive type system: proper arrays, dictionaries (map), structures, classes.
A lot more libraries included: string manipulation, full UTF8 support, math, sorting, etc.
Better performance: all libraries are compiled into OpenNox and run natively.
Better debugging: if script crashes, there’s a stack trace. Scripts can recover from it as well!
Go language used in scripts is used in OpenNox itself. Maybe one day you’ll decide to contribute?
In general, we believe that OpenNox is the future of Nox modding, thus porting your map may give it an entirely new life!
How?
At this point, we are still working on improving the EUD
compatibility layer, so quite a few functions might still be unavailable. Please talk to us for more details.
In general the conversion process is very similar to the one for converting NS3 source manually.
Same changes must be done to the source to align the syntax. We are working on a more automated process.
You need to be aware that all functions using GetMemory and SetMemory must be rewritten in any case.
The first thing to do is to check if functions you are using are already available in Panic’s EUD library.
If so, updating your EUD script to using these function will help you convert to our EUD
library later as well.
We aim to provide a good migration path for well-known EUD libraries, so if you copied code from another EUD project,
there’s a high chance we support it in one of the libraries.
Still, if you cannot find anything similar, talk to us. We will help convert your code and include necessary functionality
into next OpenNox release.
After conversion
The NS3 guide applies here as well, so make sure to check it out!