-
-
Notifications
You must be signed in to change notification settings - Fork 36
Lua cruft
Lua world is lowercase, C++ world is g_hungariancamel
, and mixing of the
two is mainly to prevent pointless renaming - C++ structure directly bound to
to Lua will usually keep its C++ name as-is. Also makes it clearly visible
what comes from where.
Also, local variables holding constants should be all-caps.
General template for scripts (in the Script tab) is as follows:
--@INFO Text shown in script list
local _M = {}
-- optional
local options = {
-- name, default, settings dialog format
{"var1", 0, "Enable var1: %b"},
{"var2", 1, "Enable var2: %b"},
}
-- optional
function on.eventname(eventarg,arg2,argN...)
if options.var1 == 1 then
... do something if var1 is enabled...
end
return eventarg
end
-- mandatory
function _M:load()
mod_load_config(self, options)
... things to do on load...
end
-- optional
function _M:unload()
...things to do on unload...
end
-- optional
function _M:config()
mod_edit_config(self, options, "Script settings window title")
...things to do when settings button is hit...
end
return _M
In top level block of scripts, you put event handlers like so:
on.event = function(data)
return data
end
These event handlers are chained according to script load order. Ie next module
hooking the event, receives data
returned by previous script's event handler.
Currently supported events:
on.d3d9_preload(d3dmodule)
on.launch() -- right after one clicks launch the game
on.first_tick(hwnd) -- first tick of the game. from this point on, its safe to assume theres a 3d context
on.load_card(seat,saveortransfer) -- a character is loaded into seat (either by adding a student, or loading a save)
on.unload_card(seat)
on.start_h(hinfo)
on.end_h(hinfo)
on.change_h(hinfo,prevpos,prevactive,prevpassive,prevactiveface,prevpassiveface)
on.char_spawn(char) -- right before character hipoly being spawned
on.char_spawn_end(char) -- right after hipoly spawned
on.char_update(char) -- (already spawned) hipoly update
on.char_despawn(char) -- hipoly being despawned
on.answer(yesno, answerstruct) -- NPC or PC answering
on.clothes(clothes, seatnumber) -- NPC or PC changing clothes
on.move(actionparams) -- NPC moving somewhere
on.convo_npc_answer(convostruct)
on.convo_pc_answer(convostruct)
on.ui_event(state)
Typically, event arguments directly map C++ types, so grep AAU source_code for LUA_EVENT - events where return value matters to C++, and LUA_EVENT_NORET - where return value is ignored (but passed parameters can be still often mutated by lua).
Then you go see the classes passed as arguments and check their bindLua() to figure out which fields and methods are proxied to Lua.
mod_load_config(modobj,options)
and mod_edit_config(modobj,opotions,wintitle)
are provided for convenience. The options table initially lists subtables with
configurable options from which IupParam
is constructed by mod_edit_config()
when you hit 'Settings' in script tab.
In turn, these variable names are hooked to the table (via metatable) which
points it directly to script's config object. Meaning if you modify any value
in there (fe options.var1 = 0
), calling Config.save()
is enough.
p(table)
-- Pretty printer, recursively prints tables
exe_type
-- global variable, string value of either "play" or "edit".
log.spam(format, args...)
-- logs formatted string. other log levels are log.info, log.warn, log.error, log.crit.
print()
-- behaves same as normal Lua print, but logs as spam
info()
-- behaves like print(), but logs on info level
for name in readdir("path/mask") do...
-- returns directory reading iterator
aau_path("comp","on","nent"....)
appends path components to base (full) AAU path. No arguments simply returns the the AAU base path.
play_path()
-- works same as aau_path, but for AA2Play
edit_path()
-- works same as aau_path, but for AA2Edit
host_path()
-- either edit_path
or play_path
, depending on where the script is currently running.
Theres much more, but mostly undocumented. Just read init.lua
SetLoadOverrides(skirt,boob,eye)
configures overrides for model load. These are to be set inside on.char_spawn
event, and reset to original values in on.char_spawn_end
GetCharacter(seat)
retrieve character struct of given seat (or nil if vacant)
GetPlayerCharacter()
retrieves char struct of PC
SetPlayerCharacter()
sets current PC (very unsafe, useable only for short durations in restricted context)
SetFocusBone(bone,offx,offy,offz)
-- Makes camera track a particular bone (ExtClass::Frame instance). Mainly used for POV. nil bone stops tracking.
xx = LoadXX(displaylist,ppname,filename)
loads a particular xx file, and returns ExtClass::XXFile instance. display is typically a global structure, there are bunch for various parts of the game. set bLogPPAccess=2
to trace LoadXX events the game makes, and you can copy displaylist values from there, provided those are a fixed global.
PPReadFile(ppname,filename)
retrieves contents of filename in ppname, as raw string. all overrides and shadow sets apply, ie its same operation the game makes to load assets.
Both PPReadFile and LoadXX
must be provided with full path to ppfile, ie you need to construct it using play_path / edit_path
.
cast("classname", dword) -> userdata
Casts arbitrary pointer to given ExtClass. Example: cast("ExtClass::CharacterStruct", 0x12345678)
Once something becomes an extclass instance, its methods can be invoked (see earlier paragraphs about bindLua() )
Anything which takes 'addr' as first argument can be prefixed with g_
, in
which case addr is relative to game exe base. Ie g_poke_dword(0x12345,0x6789)
will patch game.exe+0x12345
All memory access is protected. That is, stepping on invalid pointer won't crash the game. Peek will return nil on invalid memory access, poke will return nil instead of number of bytes written.
poke(addr,"bytes")
memcpy(addr, bytes, #bytes)
poke_dword(addr,dword)
memory[addr] = dword
poke_walk(start,dword,off1,off2...)
memory[start][off1][off2]... = dword
peek(addr,count)
char[count] buffer; memcpy(buffer, addr, count); return buffer
peek_dword(addr)
dword = memory[addr]
peek_walk(start,off1,off2,...)
dword = memory[start][off1][off2]...
Full win32 access is provided and functions are resolved on the fly the first time they are mentioned. The only thing you need to be careful about is argument count - those imply the stdcall function signature we'll call the function with.
Some examples:
MessageBoxA(0,"Read only","Strings",0)
ExitProcess(0)
local eax, edx = proc_invoke(addr, this, args...)
Invokes a function at 'addr' (can be userdata or lua number) with this and args. Lua strings are converted to pointers to the string data, numbers to DWORDs, nil to 0. For pointers to buffers, the pattern is to allocate raw memory, and poke() into it, like so:
local rect = malloc(16)
GetWindowRect(GetDesktopWindow(), rect)
local screenw, screenh = string.unpack("<II", peek(rect+8, 8))
free(rect
Only stdcall/thiscall is supported. If the function is stdcall, 'this' argument is ignored, but must be still provided even if nil. You must always provide exact number of arguments the real function has, even if those are trailing nils (so be careful with unpack because trailing nils are not stored in tables).
local orig = hook_vptr(addr, nargs, function(orig, this, arg1, argn..) .... end)
Injects pointer to lua function at addr (ie typically address of vtable, or IAT).
The Lua function you provide may call the original function (which is received
as first argument) via proc_invoke()
- or not.
To undo the hook, do:
poke_dword(addr, orig)
local orig, savedbytes = hook_func(addr,savedbytes,nargs,function(orig,this,arg1,argn...)...end)
More advanced function hooking - this is when patching IAT/vtable is not suitable.
In that case, we put a redirect jump to Lua dispatcher to the function prologue -
you must figure out correct savedbytes
of the prologue, it needs to be at least
6, and expresses the instruction boundary. savedbytes
must not contain any
jumps. Again, the function you provide is called and you have to call orig
-
which points a page with savedbytes
followed by jump back to the rest of the
epilogue. To undo the hook, you can just:
poke(orig,savedbytes)
glua is lightweight clone of selene and works very similiar to it, ie you can
g_Lua["table"][1][2][3] = 1;
and works exactly as expected
Or:
auto obj = g_Lua["obj"];
obj["method"](obj, 1,2);
Is equivalent to
obj:method(1,2)
note that objects are cast by lvalue in the end and may lead to ambiguities,
so you have to be explicit at times, eg int(g_Lua["object"])
invokes
the proper cast operator.
To bind global functions which just return some object to lua, use global binding table, eg:
auto tab = g_Lua["_BINDING'];
SomeClass *obj;
// Only these 2 kinds of lambdas are accepted
tab["Fun1"] = [](GLua::State &s) { // can use LUA_LAMBDA() macro instead
cout << "arg1 is << int(s.get(1)) << "\n";
s.push(obj);
return 1;
}
tab["Fun1"] = [](lua_State *L) { // can use macro LUA_LAMBDA_L instead
cout << "arg1 is" << lua_tointeger(L, 1) << "\n";
lua_pushlightuserdata(L, obj); // note that this will not tag type properly
lua_pushinteger(L, 1);
return 2;
}
In this instance, s.push will recognize the class pointer, and will propagate the class type to lua so bound methods to it will work.
class Embedded {
int x, y;
}
class SomeClass {
Embedded emb;
int member;
SomeClass *ptr;
const char *str;
char strbuf[256];
int array[256];
int *ptrarr;
int ptrarrsz;
struct {
float m[16];
} *matrix;
SomeClass *objarray;
int objarraysz;
SomeClass *method2arg(int x, int y);
SomeClass *method3arg(int x, SomeClass *o, const char *s);
static inline bindLua() {
#define LUA_CLASS SomeClass
// optional, but encouraged - names the class when print()ing the
// object value, also adds dump() convenience method.
LUA_NAME;
LUA_BIND(member); // works for atomic types like int, float, byte, bool and pointers
LUA_BIND(ptr); // works for atomic types like int, float, byte, bool and pointers
LUA_BINDP(emb); // objects which are not pointer must use P suffix
LUA_BINDARR(array); // binds array, autodeteccts size using sizeof()
LUA_BINDSTR(strbuf); // binds string/byte buffer, sizeof() again
// bindarre is for snowflaking array accesses, the format is simply
// LUA_BINDARRE(field,.subindex,limit) -> field.subindex[i]
LUA_BINDARRE(ptrarr,,_self->ptrarrsz);
LUA_BINDARRE(matrix, .m, 16 && (_self->matrix != 0));
// the middle argument can be used to construct cherrypicks
// the limit argument can be abused to include arbitrary
conditions, like a nullcheck
// _self simply refers to 'this' of the object
// similiar to LUA_BINDP, the array reference would result
// in non-pointer to object, so we have to add the P suffix
LUA_BINDARREP(objarray,, objarraysz);
// directly maps the method. separate macros for argument
// counts used, if you need different arity, just add more
// to gluam.h
LUA_MGETTER2(method2arg);
LUA_MGETTER3(method3arg);
}
}
In this example, SomeClass is used, however arbitrary classes can be used everywhere - as long you tell Lua about their specific bindings.
Pointers to opaque types without bindLua can be used too, however those will obviously not have any field/method access proxies, you can merely pass the instance pointer around.