A simple mission script language parser and runtime for Go.
- Labels and control flow
- Pointers and Addresses
- Expressions
- Preprocessor directives
go get -u github.com/nitwhiz/fxscriptTo use FXScript, you need to:
- Define your runtime environment by implementing the
vm.Environmentinterface. - Configure the parser with your custom commands and variables.
- Load and run your script.
The Environment interface allows the Runtime to interact with your application.
type MyEnvironment struct {
values map[fx.Identifier]int
}
func (e *MyEnvironment) Get(variable fx.Identifier) (value int) {
return e.values[variable]
}
func (e *MyEnvironment) Set(variable fx.Identifier, value int) {
e.values[variable] = value
}
func (e *MyEnvironment) HandleError(err error) {
fmt.Printf("Runtime error: %v\n", err)
}vmConfig := &vm.RuntimeConfig{
Identifiers: fx.IdentifierTable{
"health": 1,
"score": 2,
},
}
script, err := fx.LoadScript([]byte("set health, 100\n"), vmConfig.ParserConfig())r := vm.NewRuntime(script, vmConfig)
myEnv := &MyEnvironment{values: make(map[fx.Identifier]int)}
r.Start(0, myEnv)You can also start execution from a specific label in your script:
r.Call("myLabel", myEnv)Identifiers are mapped to integer addresses. In the script, they are used by name if defined in the ParserConfig.
set health, 100
set health, (health + 10)
FXScript supports arithmetic, logical, and bitwise expressions.
| Category | Operators |
|---|---|
| Arithmetic | +, -, *, /, % |
| Bitwise | & (AND), | (OR), ^ (XOR), << (LSH), >> (RSH) |
| Comparison | ==, !=, <, >, <=, >= |
| Unary | - (negation), * (deref), & (addr), ^ (NOT), ! (logic NOT) |
&<ident>: Returns the address of the identifier.*<expr>: Treats the result of<expr>as an address and returns the value of the variable at that address.
set a, 100
set b, *a // b = value of variable at address 100
set c, *(a+1) // c = value of variable at address (a + 1)
set d, &a // d = address of identifier 'a'
set flags, (flags | 1) // Set bit 0
set isSet, (flags & 1) // Check bit 0
set score, (10 + 20 * 2)
jumpIf (health < 10), danger_label
set health, 100
loop:
set health, (health - 1)
jumpIf (health == 0), end
goto loop
end:
nop
const name value: Defines a script-level constant.macro name ... endmacro: Defines a macro. See Macros for details.@include "file": Includes another file during preprocessing.
Macros allow you to define reusable blocks of code. They can also take parameters, which are prefixed with a $ sign.
macro my_macro $param1, $param2
set A, ($param1 + $param2)
endmacro
my_macro 10, 20
In this example, my_macro 10, 20 will be expanded to set A, (10 + 20).
Macro arguments are literally replaced in the macro body.
nop: No operation.set <ident>, <value>: Sets identifier to value.goto <label/addr>: Jumps to label or address.call <label/addr>: Calls subroutine at label or address.ret: Returns from subroutine.jumpIf <condition>, <label/addr>: Jumps to target if<condition>evaluates to a non-zero value.
You can extend FXScript with your own commands:
const CmdMyCustom = fx.UserCommandOffset + 1
vmConfig := &vm.RuntimeConfig{
UserCommands: []*vm.Command{
{
Name: "myCommand",
Typ: CmdMyCustom,
Handler: func(f *vm.RuntimeFrame, args []fx.ExpressionNode) (jumpTarget int, jump bool) {
fmt.Println("Custom command executed!")
return
},
},
},
}
script, err := fx.LoadScript([]byte("myCommand\n"), vmConfig.ParserConfig())
if err != nil {
panic(err)
}
r := vm.NewRuntime(script, vmConfig)
myEnv := &MyEnvironment{values: make(map[fx.Identifier]int)}
r.Start(0, myEnv)The Handler function for a custom command has the following signature:
func(f *vm.RuntimeFrame, args []fx.ExpressionNode) (jumpTarget int, jump bool)jumpTarget: The new Program Counter (PC) value if a jump should occur.jump: Iftrue, the runtime will set the PC tojumpTarget. Iffalse, the runtime continues with the next command.
For commands that take arguments, you can use the vm.WithArgs helper to automatically unmarshal and evaluate arguments into a struct using reflection.
type MyArgs struct {
Target fx.Identifier `arg:""` // Unmarshals the identifier address
Value int `arg:""` // Evaluates expression to int
Scale float64 `arg:"2,optional"` // Optional 3rd argument (index 2)
}
r.RegisterCommands([]*vm.Command{
{
Typ: CmdMyCustom,
Handler: func(f *vm.RuntimeFrame, args []fx.ExpressionNode) (jumpTarget int, jump bool) {
return vm.WithArgs(f, args, func(f *vm.RuntimeFrame, a *MyArgs) (jumpTarget int, jump bool) {
// a.Target is the fx.Identifier (the address)
// a.Value is the evaluated integer result
f.Set(a.Target, a.Value * 2)
return
})
},
},
})The vm.WithArgs helper supports unmarshalling into a struct. The behavior depends on the field type:
fx.Identifier:- If a plain identifier (like
health) or&healthis passed, it unmarshals to its address. - If an expression is passed (like
*health), it is evaluated and cast tofx.Identifier.
- If a plain identifier (like
int,float64,string: Evaluates the expression and casts the result to the field type.
The set command uses Variable fx.Identifier and Value int.
set A, 10: Sets the variable at addressAto10.set A, B: Sets variableAto the value ofB.set A, *B: Sets variableAto the value of the variable pointed to byB.set A, &B: Sets variableAto the address ofB.set *A, 10: Sets the variable whose address is the value ofAto10.