NoxScript

This documentation covers NoxScript runtime implemented in OpenNox.

Runtimes

Currently, the list of supported script runtimes includes:

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.

Then, make the following files in you map folder:

script.go (any name like *.go will work):

package <mapname>
	
func init() {
	println("hello!")
}

go.mod:

module <mapname>

go 1.20

For example, original map:

maps/
  estate/
    estate.map
    estate.nxz

Copied, and with new files:

maps/
  example/
    example.map
    example.nxz
    script.go
    go.mod

In script.go:

package example

func init() {
	println("hello!")
}

In go.mod:

module example

go 1.20

Done! Now start the map in OpenNox, open game console, and you should see hello! message there.

Info

You don’t need Go or any other compiler to write map scripts for OpenNox.

Having said that, proper Go language setup will enable IDE support, type checking, autocompletion, etc. Thus, it’s highly advised.

Full setup

For proper setup you will need:

  1. Go 1.20+ (for type checking, dependencies)
  2. Git (for Windows)
  3. GoPls (for integration with VSCode)
  4. VSCode (IDE itself)
  5. Go extension for VSCode (includes docs about gopls)

Once done, we can make a new project:

  1. Copy existing map or create a new one in the map editor.
  2. Open map folder in VSCode.
  3. Open terminal (Terminal -> New Terminal in top menu in VSCode).
  4. go mod init <mapname> (e.g. go mod init example). This will create go.mod.
  5. Create a new Go file, e.g. script.go with package <mapname> (e.g. package example).
  6. Add some code to it, e.g. func init() { println("hello!") }.
  7. Save everything, start OpenNox and test the map.

Script runtimes

There are multiple versions of the script runtimes available (full list here), for example:

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:

package example

import (
    "fmt"
    
    "github.com/noxworld-dev/noxscript/ns/v4"
)

func OnFrame() {
	fmt.Println("players:", len(ns.Players()))
}

Couple a things to note here:

  • We imported standard Go package fmt used for printing to console. You can freely use other Go standard packages as well.
  • We imported NS as github.com/.../ns/v4, but we reference it as ns.* (e.g. in ns.Players). The v4 suffix is a package version.
  • OnFrame function will be called by OpenNox each game frame (aka “server tick”). So we should see a lot of messages in console!

That’s all what’s needed to use one of the runtimes. But what if we want to use both NS3 and NS4, for example?

Importing both packages directly won’t work - they have the same name. So we assign an alias to each version:

package example

import (
    "fmt"
    
    ns3 "github.com/noxworld-dev/noxscript/ns/v3"
    ns4 "github.com/noxworld-dev/noxscript/ns/v4"
)

func OnFrame() {
	fmt.Println("players:", len(ns4.Players()))
	fmt.Println("talking:", ns3.IsTalking())
}

If you got used to NoxScript 3, and you don’t want to write ns. or ns3. everywhere, you can also specify dot as an alias:

package example

import (
    "fmt"
    
    . "github.com/noxworld-dev/noxscript/ns/v3"
    ns4 "github.com/noxworld-dev/noxscript/ns/v4"
)

func OnFrame() {
	fmt.Println("players:", len(ns4.Players()))
	fmt.Println("talking:", IsTalking())
}

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/noxworld-dev/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.

NoxScript 4

Reference

Here you can find a full list of functions provided by NS4 runtime.

NS4 runtime

This is the current script runtime in OpenNox based on Go programming language.

See quickstart for a tour, or see NS3 for conversion from vanilla scripts and some history.

Subsections of NoxScript 4

Migrate from NS3

NS4 provides the same functionality as NS3, but uses object-oriented approach. It also supports a few unique features which are not available in NS3.

Changes

Objects

The ns3.ObjectID is replaced with ns4.Obj type. The main difference is that ns4.Obj is opaque and can no longer be used or stored as an integer.

It is possible to convert ns4.Obj to/from ns3.ObjectID using the following code:

ns4.AsObj(obj)
ns3.ObjectID(obj.ObjScriptID())

Similar functions are available for ns4.ObjGroup and ns3.ObjectGroupID:

ns4.AsObjGroup(group)
ns3.ObjectGroupID(group.ObjGroupScriptID())

Most object-specific functions are now available on the object itself, instead of a top-level library function:

obj.Enable(false)
obj.Delete()
ns3.ObjectOff(obj)
ns3.Delete(obj)

Object position is now available as a point type, instead of separate X and Y coordinates:

pos := obj.Pos()
x, y := pos.X, pos.Y
x, y := ns3.GetObjectX(obj), ns3.GetObjectY(obj)

Most functions can now accept this new position type instead of a coordinate pair:

pos := obj.Pos()
obj2.HitMelee(pos)
x, y := ns3.GetObjectX(obj), ns3.GetObjectY(obj)
ns3.HitLocation(obj2, x, y)

Position can also be constructed from X and Y values:

obj.HitMelee(ns4.Ptf(10, 20))
ns3.HitLocation(obj, 10, 20)

Waypoints

Similar to objects, waypoints are also represented with opaque ns4.WaypointObj type.

Conversion to/from ns3.WaypointID is still available:

ns4.AsWaypoint(wp)
ns3.WaypointID(wp.WaypointScriptID())

Waypoints now also have methods instead of top-level library functions:

wp.Enable(false)
ns3.WaypointOff(wp)

Position is also returned as a point data type:

pos := wp.Pos()
x, y := pos.X, pos.Y
x, y := ns3.GetWaypointX(obj), ns3.GetWaypointY(obj)

This, in turn, allows using any type that has Pos() method (also known as ns4.Positioner) in places where position is expected:

ns4.CreateObject("Beholder", ns4.Ptf(10, 20))
ns4.CreateObject("Beholder", ns4.GetHost())
ns4.CreateObject("Beholder", ns4.Waypoint("Spot"))
ns3.CreateObject("Beholder", ns3.Waypoint("Spot"))

NoxScript 3

Reference

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/noxworld-dev/opennox-lib/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:

package example

import (    
    . "github.com/noxworld-dev/noxscript/ns/v3"
)

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>:

package example

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:

var x int
int x;

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.
func foo(a int) {
}

func bar(x, y int, s string) int {
}
void foo(int a)
{
}

int bar(int x, int y, string s)
{
}

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:

if x { foo() }

if y {
    bar()
}
if (x) foo();

if (y)
{
    bar();
}

Same rules for { apply for else:

if x {
    foo()
} else {
    bar()
}
if (x) foo()
else bar()

Loops

Loops must not include ( and have same rules in regard to {:

var i int
for i = 0; i < 10; i++ {
}
int i;
for (i = 0; i < 10; i++)
{
}

Loop that use while must use for keyword:

for x {
}
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:

s := fmt.Sprintf("Hello player %d!", n)

There are quite a few more string functions available in strings, https://pkg.go.dev/strconv and unicode packages.

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:

s := "Gello"
b := []byte(s) // type: []byte
b[0] = 'H'
s = string(b) // "Hello"

New: Variable type inference

NoxScript 3 requires a type for new variables to be set explicitly. NS3 allows automatic inference of variable type:

a := 10 // int
b := false // bool
x := ns.Object("Bob") // ObjectID
int a = 10; // int
int b = 0; // bool 
int x = Object("Bob"); // object

The := operator does two things: defines a new variable (similar to var) and infers a type for it.

But note that using float values produces a float64 type, not float32:

x := 0.0 // float64!
y := float32(0.0) // float32
x = y // error!

However:

const x = 0.0 // untyped float
var y float32 = x // no error

Constants are “untyped” and infer the type automatically from a variable they assigned to.

New: Range loop

In NS3 a new type of loop is available: for range, which allows to loop over all values in an array.

var arr [3]int
// writing: loop over indexes only
for i := range arr {
    arr[i] = i+1;
}
cnt := 0
// reading: loop over values
for _, v := range arr {
    cnt += v
}
int i;
int arr[3];
int cnt = 0;
for (i = 0; i < 3; i++) // writing
{
    arr[i] = i+1;
}
for (i = 0; i < 3; i++) // reading
{
    cnt += arr[i];
}

New: Dynamic arrays (slices)

In NoxScript 3, only fixed arrays are supported. NS3 has support for Go slices, which are arrays with dynamic length:

var arr []int
for i := 0; i < 3; i++ {
    arr = append(arr, i+1) // adds elements to the end
}
// len(arr) == 3
int i;
int arr[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:

type MyUnit struct {
	ID ns.ObjectID // types on the right: field "ID" with type "ns.ObjectID"
	Class string
}

func init() {
    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!
func changeClass(unit *MyUnit, cl string) {
    unit.Class = cl
}

The changeClass function can also be rewritten as a method on MyUnit struct:

type MyUnit struct {
	ID ns.ObjectID // types on the right: field "ID" with type "ns.ObjectID"
	Class string
}

// 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(cl string) {
    u.Class = cl
}

func init() {
    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?

type MyUnit struct {
	ID ns.ObjectID // types on the right: field "ID" with type "ns.ObjectID"
	Class string
}

// mapUnits maps ns.ObjectID to *MyUnit.
// All maps must be created with make before use!
var mapUnits = make(map[ns.ObjectID]*MyUnit)

func init() {
	createMyUnits()
	findAndChangeClass()
}

func createMyUnits() {
    unit := ns.Object("Bob")
    myUnit := &MyUnit{ID: unit, Class:"archer"}
    // add new record to the map, index by ID
    mapUnits[unit] = myUnit
}

func findAndChangeClass() {
    unit := ns.Object("Bob")
	// find by ID
	myUnit := mapUnits[unit]
	if unit == 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!