HOWTO: Event Scripting With Lua
PreambleThe aim of this post is to document how to write game events in Lua. I'm going to give a short introduction to lua, describe how to write a Lua event for WM, list the "wm" package functions, and talk about possible "gotchas"
Introducing LuaLua is a scripting language designed to be embedded in larger programs. It's small and fast, and the syntax is simple and straightforward. Because of this, Lua is popular as a games scripting language. World of Warcraft, for instance, uses Lua.
There are a lot of Lua resources available online. Some good starting points are:
http://www.lua.org/ http://lua-users.org/wiki/ http://www.lua.org/manual/5.1/Writing Lua events for WhoreMasterTo create a new event in WM you need two things. A script and a trigger to fire it. Let's look at the script, first.
A simple Lua script might look like his
-- double minus sign starts a comment in lua - it lasts to the end of the line
local event_state -- local variable
function init()
-- stuff to initialise the event goes here
wm.log("event init")
event_state = "starting"
return true
end
function run()
wm.log("run function called")
if event_state = "starting" then
wm.log("starting stage")
event_state = "stage 2"
return true
end
if event_state = "stage 2" then
wm.log("stage 2")
event_state = "ending"
return true
end
if event_state = "ending" then
wm.log("event ending")
return false
end
wm.log("how did I get here")
return false
end
The two functions, run() and init() are the key. The init() function gets called when your event is fired. Events may be triggered more than once, so this function is used to set all the local variables back to their start state.
The init function should normally return true - if it returns false (or nothing at all) then the game will take that as a sign that the event couldn't be properly initialised and should not be run.
The run() function is the one that gets called when the event actually executes. This too is probably going to get called more than once. For some things you need to leave the script before they can happen. Menus for instance
can be set up from inside a script, but they won't display until the script exits. This means that your run() method may need to be called several times in order to do what you want.
The game uses the value returned by run to determine if the function needs to be called again. if it returns false, (or nil, or no value at all), that means "this event is finished, remove it from the event queue". Returning true means "this event has more to do, please run it again".
Managing StateThe other thing the run method needs is a way to make sure that different things happens when run() is called at different times. The way to do that is with a state variable - that's what event_state is for in the example above. The run function tests the variable to find out where it was up to in the sequence of occurences that make up the event.
The wm.log() function writes to the log file, so we can see what the script is doing. The output from this script should be
LuaScript: event init
LuaScript: run function called
LuaScript: starting stage
LuaScript: run function called
LuaScript: stage 2
LuaScript: run function called
LuaScript: event ending
You can see that the run function gets called three times; once for
each of the events three stages. The first two stages return true, so
the game knows to keep running the script. The third one returns false
to end exectuion.
Looking at the event_state variable again, you'll see it's declared
local. That's because all the events in the game run in a shared
namespace. This means that any global variables may be overwritten by
other scripts. Local ones are private, and much safer.
The "wm" package
To keep from polluting the namespace, all the game interface functions
are packaged into the "wm" namespace. These functions and data structures
are how the game engine communicates with the scripts.
Currently, the following functions are defined:
wm.girl: This is the target girl of the event. If the script is attached to a girl, her details will be in here. If there is no girl for this event, then wm.girl will be nil. Note that the wm namespace is shared among scripts, which means that although wm.girl will be correct when init() runs, it could potentially change if other scripts are running. So it's good practice to copy the object to a local variable.
Currently wm.girl doesn't hold very much information, although more will be added soon.
- wm.girl.name The girl's name
- wm.girl.real_name The girl's "real" name.
- wm.girl.desc This is the girl's short description
- wm.girl.pointer This is a reference to the girl's internal address in c++.
- wm.add_girl_to_brothel Adds a girl to the current brothel. Default is the current target girl.
- wm.menu displays a game menu. Call it like this:
wm.menu {
-- each caption line is a menu choice
captions = {
"Strike the begger for his impertinence",
"Take a closer look"
},
-- the callback function gets called when the user makes a menu choice
-- best practice here is to set a state varaible and return
callback = function(choice)
if choice == 1 then
event_stage = "violence"
elseif choice == 2 then
event_stage = "brains"
else
wm.log("unexpected choice in menu callback")
end
end
}
- wm.get_sdl_ticks Get the SDL engine's count in milliseconds since the program started.
- wm.log(message) write message to the log file
- wm.message(text,colour) display text in a popup box. Colour is in the range 0-7
The "real name" is only significant for random girls, where the real name is the random name generated for the particular girl in question. the pointer field is for internal use. Please don't change it or things will stop working and the game may crash.
Caveats
There is one potential pitfall to be aware of. As mentioned earlier, local variables are a good idea. By the same token, if you break your script down into functions, then local functions are likewise good. However:
- local functions need to be declared before the functions that use them
- I don't think you can declare run and init as locals. Or if you do, I don't think the game will find the correct functions
For those interested - this is why it happens. If you're not interested then feel free to skip the next bit. tl;dnr and all that.
Anyway, the local functions problem is all to do with the way lua handles function calls. if you say foo() in a lua script, it assumes that there's goign to be a name "foo" in the global namespace table. If you then declare "local function foo()" then all subsequent invocations will use the local foo, but the ones prior to that point will still be pointing at the global namespace, which will be undefined.
You can't get around it by forward declarations either. If you try
local foo
function run()
foo()
end
foo = function()
wm.log("boo")
end
then run gets treated as a closure, and is called with the value of local foo as an upvalue - in other words it embeds nil as the value of foo in the fun function, even though the value is subsequently updated.
OK, that's everythign I can think of right now. I'll add some examples in a bit...