• Do not use Works in Progress as a way of avoiding the releases system! Works in Progress can be used for sharing early betas and for getting suggestions for improvement. Releases of finished content are not allowed in this forum! If you would like to submit a finished addon, click here for instructions on how to do so.

Lua Scripting in SRB2 2.1

Status
Not open for further replies.

JTE

Member
The basics:
Lua scripts are loaded similar to SOC lumps, from any lump that starts with "LUA_" (i.e. LUA_INIT, LUA_GAME, LUA_SKIN, LUA_GLOB) or any file that ends with the .lua extention (similar to .soc) ... The lumps are loaded in the order they appear in the wad.

Initialization:
Before a Lua script is loaded, the Lua scripting system remains uninitialized and dormant, costing no further overhead than SOCs do in terms of memory and processing. When the first Lua script is found, a new Lua state is initialized and all of the hardcoded Lua libraries are loaded into it, which may take some significant processing, but only occurs once.

The global environment:
All Lua scripts run in this same Lua state, so for now I've disabled setting variables in the global environment, in an effort to prevent scripts from accidentally messing eachother up. Attempting to set a variable that doesn't exist (local variables must be declared beforehand) results in a Lua processing error.

Lua script errors:
Lua processing errors are handled by writing out a detailed error message (as generated by Lua) to the console as a warning alert and the currently running Lua script is halted.
If a Lua error occurs outside of a running script (for instance, while finding the appropriate Lua function to run or preparing arguments for it), however...! Then Lua will panic and crash the game with an I_Error, because this is a bug with the exe and must be fixed!

Program stability:
Nothing Lua scripts do should be able to destabilize the game (aside from potentially causing consistency failures, see below), anything 'dangerous' like manipulating pointers or calling internal functions with incorrect parameters should be handled and raise a Lua processing error; If it crashes, even with an I_Error message box, it's a bug.

Network consistency:
Local variables are currently out of my control, and some global variables from SRB2 which may be too useful to leave out completely, aren't necessarily network-synchronized and can in theory cause consistency failures.

For instance, if a script makes a local variable outside of all its functions, that variable is global to that specific script lump, and won't be reset between function calls, reset after the game returns to the title screen, or sent to other players should they join after it is set.

Loading and running a script:
When a Lua script is "loaded", it is immediately processed as if the script itself were a function. This means that everything you put inside of a script happens immediately when the wad is added with addfile, such as writing messages to the console, initializing variables and data tables with useful information, and declaring functions and attaching hooks. There is no "initialize this script" function that runs immediately after the wad is added, just the script itself.

After the script is loaded (and processed, generally adding things it needs to the global state as allowed and setting up other necessary local things), it will not be run again. There IS an overhead for loading/running a script, as Lua does its script compilation to bytecode at load time, so "running" a script by re-loading it repeatedly is FAR FROM IDEAL. Instead, Lua scripts are intended to declare functions, which can then be called by SRB2 at any time with minimal overhead.

SOCs, Actions, etc:
One of the things a Lua script can do is manipulate SRB2's info tables, declaring free slots and setting mobj info fields using enumerations and math, the same way a SOC does.

Unlike a SOC, however, Lua scripts can go a step further and actually declare entirely new "A_Action" functions for its objects to use. These are created by simply declaring a new global function with a name which starts in "A_", which SRB2 will recognize and specifically allow. Then, the newly declared action may be assigned to one or several existing states as its action, exactly the same as any hardcoded action function.

This function will take one to three arguments, named "actor", "var1", and "var2" by convention, which are the mobj_t of the object which called it and the state's var1 and var2 values. The function will be run whenever an object changes states to one which has been set to use this function as its action, and may do anything it damn well pleases.

Hooks:
Scripting hooks are the only other way which Lua functions are currently run. They are declared as local functions, and then passed to SRB2 as an argument of the hardcoded "addHook" function, along with other relevant parameters which vary depending on the hook type.

Throughout SRB2's code, in any convenient place where hardcoded behaviors are present, a call to one of Lua's functions through a scripting hook may be added, allowing script authors and users to create additional behaviors or replace existing ones. For instance, a function may be added for when a specific mobj type's fuse runs out, where the default behavior would cause it to die, instead the written Lua function may cause something else to happen, such as triggering a linedef executor or having a boss stop to contemplate the meaning of life.

Lua and BLUA:
SRB2's internal copy of Lua has been heavily modified to suit our needs. This changes it from the standard vanilla implementation of Lua in more ways than simply adding new libraries, so here's a section to explain the changes. Most of these were made in order for Lua to more readily integrate with SRB2.

Numbers:
Vanilla Lua's numbers are stored as "double" and do a lot of floating point math. This has been changed in our version to use ptrdiff_t, a type used for handling large signed integers, which means it is suitable for containing whole numbers, fixed point math (multiples of FRACUNIT), angles (ANGLE_180, etc), or any other unit of measurement SRB2 can possibly handle. However, this also means there's no decimals for script writers to use. All fractional math must be fixed point math.

Standard libraries:
The only standard libraries SRB2 loads are as follows: basic library, string manipulation, table manipulation. That is it. Everything else has been completely removed or replaced by SRB2-specific functions. The reasoning for this is that the other libraries are unnecessary, "dangerous", or OS-dependent.

The package library allows the loading of additional Lua scripts and completely arbitrary DLL files (dangerous!), which is unnecessary since addfile loads Lua scripts and wad files which are dependent on eachother already handle that with bat files or whatever. Lua scripts CAN load other wads too, if it comes to that, just like any map-based console script can.

The math library was designed for floating point math, and the only functions it provides which are useful to SRB2 are math.abs (which has been replaced by a global abs() as in C) and math.random (which has been replaced by P_Random). Everything else has been replaced by fixed point and fine angle math. Additional functions (such as min(), max(), or FixedFloor()) may be written and provided later if necessary.

The io library has not been replaced yet, but it was very system-dependent and only readily capable of reading/writing/deleting completely arbitrary text files anywhere on your harddrive anyway. Ideally it will be replaced with a library which can only read lumps from wad files (both binary and text) and read/write txt, cfg, and non-gamedata dat files in SRB2's own folder and subfolders. So it might be able to replace your autoexec.cfg, but it shouldn't damage your system.

The os library would be used to get the system clock, force-exit the program, and rename/delete files from anywhere on your computer. It is gone. I don't care if you wanted to write a script that only activates on april fool's day, you're not doing it from the os clock.

The debug library is gone. It could access data meant to only be used internally by SRB2, break assertions, and use arbitrary pointers. That's all I have to say on the matter.

Additions:
As well as removing all the shit that didn't mesh well with SRB2, several things have been added to Lua syntax as well.
Vanilla Lua uses ~= for not equals comparisons, BLUA allows != as well, as in C.
Vanilla Lua uses -- and --[[ ]] for comments, I wrote in support for // and /* */ as well, as in C.
Bitwise operations have been added: & | ^^ << >> and ~, for binary and or xor shl shr and not. Note that ^ in Lua is used for exponents, so ^^ was used for binary xor instead.
break N support, where N is the number of loops (scopes) to break out of.
"continue" operation to continue to the next loop was added, vanilla Lua only has break and return.
do ... end patch, makes do blocks into function() when passed as an argument or assigned as a variable.
Hexadecimal \x0000 and unicode \u0000 string literals as in C.
Vanilla Lua requires "do" after for/while (as in "while EXP do ... end") -- This requirement has been rendered optional.
Vanilla Lua requires "then" after if (as in "if EXP then ... end") -- This requirement has been rendered optional.
Pseudo numbers: When assigning multiple variables, $1 will be the value of the first variable, $2 will be the value of the second, etc.
Vanilla Lua concatinates strings with "string 1 "..variable.." string 2", SRB2 allows "string 1 "+variable+" string 2" or "string 1 \$variable\ string 2"
__usedindex metatable hook, for when an existing index is assigned a new value.

Differences between C and Lua:
Lua scripting has been thus far designed to very closely mimic what coding for SRB2 in C is like. It primarily consists of calling existing hardcoded functions (such as P_InstaThrust and A_Look) and manipulating data values yourself (such as setting/unsetting any of our plethora of bit-wise flags).

However, as Lua is a scripting language and not the real-deal, there will always be some very strong differences in syntax and implementation, especially when you note that I am working hard to ensure SRB2's stability regardless of what a Lua script may try. In order to write a successful script, one must be aware of the differences from C programming to Lua scripting, in order to be able to transition.

Syntax:
Lua uses "if ... then ... end" structure instead of "if (...) { ... }" as in C. SRB2's custom Lua bastardization library specifically makes "then" and "do" optional, but the { } brackets are used for declaring table structures in Lua, so I'm not going that far to change it. It's important to note that Lua has an "elseif" statement, where "else if" is used in C and will not give you the appropriate behavior in Lua.

C demands every command end with a semicolon so that it can support multi-line operations, in Lua semicolons are optional and generally omitted.

SRB2's Lua library has been extended with bitwise operations, in order to manage all the bitwise flags SRB2 uses and such. These are the & | ^^ and ~ operations. In C, the ^^ operation is just ^, but Lua already has powers (exponents) as ^ so the bit-wise operation became ^^ instead.

Data structures:
Lua scripts use only one type of data structure to hold multiple variables, and that is the 'table'. For all purposes of the C array and the struct, the table is used in Lua. Every structure SRB2 shares to Lua mimics the form of a table, allowing you to set and retrieve values as you wish, with the primary difference being that Lua always uses '.' to access variables in a table, where SRB2 may use '->' for pointers.

Lua does not access/manipulate pointers directly, but it will never automatically make a copy of a table when that table is assigned to any given variable. Thus, a table representing a mobj or other structure may be passed to another function, and that function may manipulate it, as if it were a pointer anyway.

Lua table arrays have a natural starting index of 1, rather than 0 as in C, but all of SRB2's arrays (such as mobjinfo) will be faithful to C. Be aware that table.insert(), ipairs, etc. will start at 1 and ignore any value at 0. (I may look into changing this at some point. Lua used to consider a number value of '0' to be equivalent of true, for instance. We're flexible here.)

Internally, SRB2 has not actually created a table for each mobj_t, player_t, etc., but actually passed Lua a pointer and asked it to be kept a secret. Thus, unlike a regular Lua table, variables may not be assigned haphazardly to any SRB2 structures, and can be controlled or modified as appropriate whenever a value is requested or assigned.

Some values, such as a mobj's x, y, and z position, outright refuse to be edited directly, as it would break the game (blockmaps, linked lists and all that). In this case, there should be provided an alternative method to change these values, such as P_TeleportMove for mobj positions or a BotTiccmd hook for player button presses to be generated.

Functions:
In Lua, functions do not have to explicitly declare their return value, and are instead declared simply as 'function' or 'local function'. Similarly, variable types aren't explicitly defined, and any variable can contain any type of data, thus the arguments of a function are simply local variable names to use, if the argument is to be accepted at all.

Any function may take any type or number of arguments and return any type or number of values, including 'nil' (in place of NULL, or nothing). It's interesting to note that C functions can only return one value and often use pointers as arguments to mimic the ability to return multiple informative values, but this isn't necessarily practical in Lua as Lua only has tables in place of pointers.

Instead, Lua may easily return several values at once, which can be recorded into multiple variables as such:
local x,y,z = GetCoordinates(mobj)
Now the local variables x, y, and z contain the three values returned by the function 'GetCoordinates', or nil if the function failed to return a value for any of them.

Bot AI in Lua:
New to SRB2 2.1 is a feature of the singleplayer character select screen, in which writing in the skin name as "sonic&tails" (or any other "skin&skin") will start a new game in which the player is using the first character and the second character is added to the game as a 'bot' with limited AI.

These bots differ from previous bots in SRB2JTE and so on in that they are specifically designed for singleplayer use ONLY and take up the slot of the splitscreen player, even disabling its own AI should player 2's controls be touched.

The default AI is intentionally quite stupid (or at least barebones), and specifically refuses to activate its own special abilities regardless of circumstance. It will never attempt to double jump, and will only spindash if it's sitting right next to you while you are charging one yourself.

However, it was brought to my attention that people may wish to attempt to script their own bot AI in Lua, for one reason or another. Since anything hardcoded should be modifiable through Lua, I've added a hook named "BotAI" which can be used to completely override the AI for a specific character skin by name.

The "BotAI" hook is used as such:
Code:
local function LocalBotAI(sonic, tails)
    local forward = false
    local backward = false
    local left = false
    local right = false
    local strafeleft = false
    local straferight = false
    local jump = false
    local spin = false

    return forward, backward, left, right, strafeleft, straferight, jump, spin
end

addHook("BotAI", LocalBotAI, "tails")

The addHook parameter "tails" is the character skin this bot AI gets activated on. The two arguments 'sonic' and 'tails' are the mobj_t of the consoleplayer (player 1, the user at the keyboard) and the bot (player 2, the stupid fox that follows you around). I chose these names just because tails follows sonic, but they can be something more generic like "human" and "bot", "master" and "slave", "alchemist" and "homunculus", etc. etc. if you like, it doesn't matter. The return values are all of the keyboard presses of the bot for this game tic.

Each of those boolean variables are for different keypresses. Bots, for the purpose of simplifying AI, are non-analog keyboard-users, so setting left to true will cause it to turn left at the same rate as turning left by the arrow keys. If more advanced access is required, a low-level hook "BotTiccmd" may be used instead (which activates on any bot type and manipulates the ticcmd directly) or the values of the Tails mobj may be manipulated directly (But that's cheating!).

Alternatively, "BotAI" can also be used like this:
Code:
local function LocalBotAI(sonic, tails)
    local keys = {}
    keys.forward = true
    keys.left = false
    keys.right = false
    keys.jump = false
    keys.spin = false
    return keys
end

addHook("BotAI", LocalBotAI, "tails")

This method is similar to the above example, except not all keys are required to even exist (You may have a bot which never spindashes, by never writing a spin key in at all), where having multiple return values would force you to at least put a nil in order to have the appropriate number of values.

Additional hooks which are necessary for good bot AI but aren't as of yet provided include:
A hook which is called when a bot or mobj's movement is stopped or altered by collision (specifically their momx, momy, momz, as P_Move and such immediately return a value if the move was not allowed)
A hook which may be called every tic to ask whether a bot should be respawned, in case the AI wishes to continue its existence even far offscreen.
A hook which is called when a bot is initially spawned, where the default AI simply puts the bot on the player 2 spawn thing as normal.
A hook which is called when a bot is in the process of being respawned, which can be used to manipulate the arrival position/movement/state and other such things.
 
Last edited:
Status
Not open for further replies.

Who is viewing this thread (Total: 1, Members: 0, Guests: 1)

Back
Top