A simple mission script language parser and runtime for Go.
- Custom commands
- Expression evaluation
- Identifiers and constants
- Labels and control flow (
goto,call,ret) - Preprocessor directives (
@include) - Script-level directives (
const,macro) - Conditional jumps (
jumpIf,jumpIfFlag,jumpIfNotFlag) hostCallforvm.Environmentinteraction- Pointers
*
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) 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)
}
func (e *MyEnvironment) HostCall(f *vm.RuntimeFrame, args []any) (pc int, jump bool) {
// Handle host calls from script
return
}config := &fx.ParserConfig{
Variables: fx.IdentifierTable{
"health": 1,
"score": 2,
},
}
script, err := fx.LoadScript([]byte("set health 100\n"), config)r := vm.NewRuntime(script)
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 IDs. In the script, they are used by name if defined in the ParserConfig.
set health 100
add health 10
Use * to retrieve a value from an address pointed to by an identifier or expression:
set a 100
set b (*a + 1)
Use ^ for bitwise NOT:
set a ^1
FXScript supports basic arithmetic expressions:
set score (10 + 20 * 2)
set health 100
loop:
add health -1
jumpIf health 0 end
goto loop
end:
nop
@include "other.fx": Includes another script fileconst name value: Defines a constantmacro name ... endmacro: Defines a macro
nop: No operationset <ident> <value>: Set identifier to valuecopy <from_ident> <to_ident>: Copy value from one identifier to anotheradd <ident> <value>: Add value to identifier value in memorygoto <label/addr>: Jump to label or addresscall <label/addr>: Call subroutineret: Return from subroutinejumpIf <ident> <value> <label/addr>: Jump if identifier equals valuehostCall ...args: CallHostCallon the runtime environment
You can extend FXScript with your own commands:
const CmdMyCustom = fx.UserCommandOffset + 1
config := &fx.ParserConfig{
CommandTypes: fx.CommandTypeTable{
"myCommand": CmdMyCustom,
},
}
script, err := fx.LoadScript([]byte("myCommand\n"), config)
if err != nil {
panic(err)
}
r := vm.NewRuntime(script)
r.RegisterCommands([]*vm.Command{
{
Typ: CmdMyCustom,
Handler: func(f *vm.RuntimeFrame, args []fx.ExpressionNode) (jumpTarget int, jump bool) {
fmt.Println("Custom command executed!")
return
},
},
})
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:
type MyArgs struct {
Target fx.Identifier `arg:""`
Value int `arg:""`
}
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) {
f.Set(a.Target, a.Value * 2)
return
})
},
},
})