noxworld-dev / opennox

OpenNox main repository.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Allow calling from legacy NoxScript into new NS scripts

dennwc opened this issue · comments

Similar to #633, the intention here is to improve intorop between legacy NoxScript and the new NS3/4 runtime.

The goal is to at least allow legacy NoxScript to call functions from new runtime. With that in place, old maps could be gradually ported to use more NS scripts, function by function. The author may also choose to write the legacy code in a way that can detect OpenNox presence, possibly disabling some features, or just printing an error message.

Obviously, we cannot change how NoxScript works, since that will break compatibility.

However, we could do a few tricks with the "magic byte code". Imagine something like this in legacy NoxScript (pseudocode):

var var0, var1, var2, var3 int

// magic keyword starts the sequence
var0 = "opennox.Call"
var0 = "Foo"          // func name in NS
var0 = 2              // arguments count
// pass two function arguments
var0 = var1
var0 = var2
var0 = 1 // returns count
// mark destination variable for the return
var3 = 0

Which would be an equivalent of:

var3 = Foo(var1, var2)

This looks weird, since usually NoxScript uses the stack for passing variables. Unfortunately, we cannot really use the native VM stack without changing NoxScript compilers. That would require changes to some old projects that are currently unmaintained.

Instead, a much longer byte code version is used here. All the manipulations show above produce no side effects except changing local variables back and forth. This can be written in a legacy map editor or script compiler - no changes required!

What will happen if the vanilla Nox is used with this script? Literally nothing! Variables change their values a few times and that's it. The script can even detect if it's running in vanilla or OpenNox by checking the return:

if var3 == 0 {
   // not opennox
   return
}
// success! do something else

This magic requires OpenNox to check the byte code it executes for this magic sequence:

PushString  <index of "opennox.Call" string>
StoreString <tmp var index>

From there on, all "magic" instructions come in pairs, always mentioning the tmp/call var:

LoadVar     <arg var index>  // single data instruction - load argument var in this case
StoreString <tmp var index>  // <- same index as before!

Tmp/call variable will act as a token, which OpenNox will use to validate this "magic byte code". If one of the instructions will skip the token, OpenNox will execute the code as usual.

This continues until all all call parameters are set. Then, the return section follows:

PushInt 0 // default value for return (if not opennox)
StoreInt <return var index> // <- here's where the return value will be written

To recap, the whole example of var3 = Foo(var1, var2) using var0 as the "call var":

// magic keyword, set token to var0
PushString <index of "opennox.Call" string>
StoreString var0

// function name
PushString <index of "Foo" string>
StoreString var0

// number of arguments = 2
PushInt 2
StoreString var0

// argument 1 = var1
LoadVar var1
StoreString var0

// argument 2 = var2
LoadVar var2
StoreString var0

// number of returns
PushInt 1
StoreString var0

// default value for return = 0 and bind return to var3
PushInt 0 
StoreInt var3 

One interesting possibility is that this code could be encapsulated into a single NoxScript function. In that case, OpenNox will completely rewrite it to serve as a bridge function instead of analyzing each assignment instruction for the "magic keyword". So that can be a part of the requirement: single function that only contains the "magic byte code" and nothing else.

Having said that, the encapsulated byte code should end after the number of returns was set. In practice, it looks like map editors and compilers do not support returns properly. So we will have to leave the return var binding in the caller. It's ugly, but it should work.

With that approach take into account, the final legacy code will look like this:

func Foo(a1 int, a2 int) {
   var var0 int
   var0 = "opennox.Call" // magic keyword
   var0 = "Foo"          // func name in NS
   var0 = 2              // arguments count
   // pass two function arguments
   var0 = a1
   var0 = a2
   // returns count
   var0 = 1
   // caller should bind return var!
}

func DoSomething() {
   var var3 int
   Foo(obj, 10)
   var3 = 0 // bind return for Foo
}