Skip to content

Lua cruft

linkanon edited this page Feb 8, 2020 · 12 revisions

Naming conventions

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.

Script template

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

Script event APIs

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.

Script config

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.

High level APIs

Utility functions

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

Global functions

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.

External classes access

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() )

The low level stuff

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

Raw memory access

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]...

Calling Win32

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)

Calling arbitrary functions

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).

Hooking functions

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 API mapping

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.

C++ binding macro madness

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.