Skip to content
Snippets Groups Projects
Select Git revision
  • better-distance-math
  • next default protected
  • movie
  • softcode-info
  • master protected
  • acs
  • spriteinfo-refactor
  • clipmidtex
  • custom-map-names
  • nogravity-trampolines
  • 2214-pre4
  • 2214-pre3
  • just-in-case
  • fix-opengl-parameter-crash
  • 2214-pre2
  • 2214-pre1
  • delfile2
  • cleanupmusic
  • gametype-refactor-1
  • extra-textures
  • SRB2_release_2.2.15
  • SRB2_release_2.2.13
  • SRB2_release_2.2.12
  • SRB2_release_2.2.11
  • SRB2_release_2.2.10
  • SRB2_release_2.2.9
  • SRB2_release_2.2.8
  • SRB2_release_2.2.7
  • SRB2_release_2.2.6
  • SRB2_release_2.2.5
  • SRB2_release_2.2.4
  • SRB2_release_2.2.3
  • SRB2_release_2.2.2
  • SRB2_release_2.2.1
  • SRB2_release_2.2.0
  • SRB2_release_2.1.25
  • SRB2_release_2.1.24
  • SRB2_release_2.1.23
  • SRB2_release_2.1.22
  • SRB2_release_2.1.21
40 results

lua_consolelib.c

Blame
  • sphere's avatar
    sphere authored
    Fix up Lua command/cvar safety
    
    See merge request STJr/SRB2!1943
    2df3fb53
    History
    lua_consolelib.c 17.10 KiB
    // SONIC ROBO BLAST 2
    //-----------------------------------------------------------------------------
    // Copyright (C) 2012-2016 by John "JTE" Muniz.
    // Copyright (C) 2012-2023 by Sonic Team Junior.
    //
    // This program is free software distributed under the
    // terms of the GNU General Public License, version 2.
    // See the 'LICENSE' file for more details.
    //-----------------------------------------------------------------------------
    /// \file  lua_consolelib.c
    /// \brief console modifying/etc library for Lua scripting
    
    #include "doomdef.h"
    #include "fastcmp.h"
    #include "p_local.h"
    #include "g_game.h"
    #include "byteptr.h"
    #include "z_zone.h"
    
    #include "lua_script.h"
    #include "lua_libs.h"
    #include "lua_hud.h" // hud_running errors
    
    // for functions not allowed in hud.add hooks
    #define NOHUD if (hud_running)\
    return luaL_error(L, "HUD rendering code should not call this function!");
    // for functions not allowed in hooks or coroutines (supercedes above)
    #define NOHOOK if (!lua_lumploading)\
    		return luaL_error(L, "This function cannot be called from within a hook or coroutine!");
    
    static consvar_t *this_cvar;
    
    void Got_Luacmd(UINT8 **cp, INT32 playernum)
    {
    	UINT8 i, argc, flags;
    	char buf[256];
    
    	// don't use I_Assert here, goto the deny code below
    	// to clean up and kick people who try nefarious exploits
    	// like sending random junk lua commands to crash the server
    
    	if (!gL) goto deny;
    
    	lua_settop(gL, 0); // Just in case...
    	lua_pushcfunction(gL, LUA_GetErrorMessage);
    
    	lua_getfield(gL, LUA_REGISTRYINDEX, "COM_Command"); // push COM_Command
    	if (!lua_istable(gL, -1)) goto deny;
    
    	argc = READUINT8(*cp);
    	READSTRINGN(*cp, buf, 255);
    	strlwr(buf); // must lowercase buffer
    	lua_getfield(gL, -1, buf); // push command info table
    	if (!lua_istable(gL, -1)) goto deny;
    
    	lua_remove(gL, -2); // pop COM_Command
    
    	lua_rawgeti(gL, -1, 2); // push flags from command info table
    	if (lua_isboolean(gL, -1))
    		flags = (lua_toboolean(gL, -1) ? 1 : 0);
    	else
    		flags = (UINT8)lua_tointeger(gL, -1);
    	lua_pop(gL, 1); // pop flags
    
    	// requires server/admin and the player is not one of them
    	if ((flags & 1) && playernum != serverplayer && !IsPlayerAdmin(playernum))
    		goto deny;
    
    	lua_rawgeti(gL, -1, 1); // push function from command info table
    
    	// although honestly this should be true anyway
    	// BUT GODDAMNIT I SAID NO I_ASSERTS SO NO I_ASSERTS IT IS
    	if (!lua_isfunction(gL, -1)) goto deny;
    
    	lua_remove(gL, -2); // pop command info table
    
    	LUA_PushUserdata(gL, &players[playernum], META_PLAYER);
    	for (i = 1; i < argc; i++)
    	{
    		READSTRINGN(*cp, buf, 255);
    		lua_pushstring(gL, buf);
    	}
    	LUA_Call(gL, (int)argc, 0, 1); // argc is 1-based, so this will cover the player we passed too.
    	return;
    
    deny:
    	//must be hacked/buggy client
    	if (gL) // check if Lua is actually turned on first, you dummmy -- Monster Iestyn 04/07/18
    		lua_settop(gL, 0); // clear stack
    
    	CONS_Alert(CONS_WARNING, M_GetText("Illegal lua command received from %s\n"), player_names[playernum]);
    	if (server)
    		SendKick(playernum, KICK_MSG_CON_FAIL | KICK_MSG_KEEP_BODY);
    }
    
    // Wrapper for COM_AddCommand commands
    void COM_Lua_f(void)
    {
    	char *buf, *p;
    	UINT8 i, flags;
    	UINT16 len;
    	INT32 playernum = consoleplayer;
    
    	I_Assert(gL != NULL);
    
    	lua_settop(gL, 0); // Just in case...
    	lua_pushcfunction(gL, LUA_GetErrorMessage);
    
    	lua_getfield(gL, LUA_REGISTRYINDEX, "COM_Command"); // push COM_Command
    	I_Assert(lua_istable(gL, -1));
    
    	// use buf temporarily -- must use lowercased string
    	buf = Z_StrDup(COM_Argv(0));
    	strlwr(buf);
    	lua_getfield(gL, -1, buf); // push command info table
    	I_Assert(lua_istable(gL, -1));
    	lua_remove(gL, -2); // pop COM_Command
    	Z_Free(buf);
    
    	lua_rawgeti(gL, -1, 2); // push flags from command info table
    	if (lua_isboolean(gL, -1))
    		flags = (lua_toboolean(gL, -1) ? COM_ADMIN : 0);
    	else
    		flags = (UINT8)lua_tointeger(gL, -1);
    	lua_pop(gL, 1); // pop flags
    
    	if (flags & COM_SPLITSCREEN) // flag 2: splitscreen player command.
    	{
    		if (!splitscreen)
    		{
    			lua_pop(gL, 1); // pop command info table
    			return; // can't execute splitscreen command without player 2!
    		}
    		playernum = secondarydisplayplayer;
    	}
    
    	if (netgame && !( flags & COM_LOCAL ))/* don't send local commands */
    	{ // Send the command through the network
    		UINT8 argc;
    		lua_pop(gL, 1); // pop command info table
    
    		if (flags & COM_ADMIN && !server && !IsPlayerAdmin(playernum)) // flag 1: only server/admin can use this command.
    		{
    			CONS_Printf(M_GetText("Only the server or a remote admin can use this.\n"));
    			return;
    		}
    
    		if (COM_Argc() > UINT8_MAX)
    			argc = UINT8_MAX;
    		else
    			argc = (UINT8)COM_Argc();
    		if (argc == UINT8_MAX)
    			len = UINT16_MAX;
    		else
    			len = (argc+1)*256;
    
    		buf = malloc(len);
    		p = buf;
    		WRITEUINT8(p, argc);
    		for (i = 0; i < argc; i++)
    			WRITESTRINGN(p, COM_Argv(i), 255);
    		if (flags & COM_SPLITSCREEN)
    			SendNetXCmd2(XD_LUACMD, buf, p-buf);
    		else
    			SendNetXCmd(XD_LUACMD, buf, p-buf);
    		free(buf);
    		return;
    	}
    
    	// Do the command locally, NetXCmds don't go through outside of GS_LEVEL || GS_INTERMISSION
    	lua_rawgeti(gL, -1, 1); // push function from command info table
    	I_Assert(lua_isfunction(gL, -1));
    	lua_remove(gL, -2); // pop command info table
    
    	LUA_PushUserdata(gL, &players[playernum], META_PLAYER);
    	for (i = 1; i < COM_Argc(); i++)
    		lua_pushstring(gL, COM_Argv(i));
    	LUA_Call(gL, (int)COM_Argc(), 0, 1); // COM_Argc is 1-based, so this will cover the player we passed too.
    }
    
    // Wrapper for COM_AddCommand
    static int lib_comAddCommand(lua_State *L)
    {
    	int com_return = -1;
    	const char *luaname = luaL_checkstring(L, 1);
    
    	// must store in all lowercase
    	char *name = Z_StrDup(luaname);
    	strlwr(name);
    
    	luaL_checktype(L, 2, LUA_TFUNCTION);
    	NOHOOK
    	if (lua_gettop(L) >= 3)
    	{ // For the third argument, only take a boolean or a number.
    		lua_settop(L, 3);
    		if (lua_type(L, 3) == LUA_TBOOLEAN)
    		{
    			CONS_Alert(CONS_WARNING,
    					"Using a boolean for admin commands is "
    					"deprecated and will be removed.\n"
    					"Use \"COM_ADMIN\" instead.\n"
    			);
    		}
    		else
    			luaL_checktype(L, 3, LUA_TNUMBER);
    	}
    	else
    	{ // No third argument? Default to 0.
    		lua_settop(L, 2);
    		lua_pushinteger(L, 0);
    	}
    
    	lua_getfield(L, LUA_REGISTRYINDEX, "COM_Command");
    	I_Assert(lua_istable(L, -1));
    
    	lua_createtable(L, 2, 0);
    		lua_pushvalue(L, 2);
    		lua_rawseti(L, -2, 1);
    
    		lua_pushvalue(L, 3);
    		lua_rawseti(L, -2, 2);
    	lua_setfield(L, -2, name);
    
    	// Try to add the Lua command
    	com_return = COM_AddLuaCommand(name);
    
    	if (com_return < 0)
    	{ // failed to add -- free the lowercased name and return error
    		Z_Free(name);
    		return luaL_error(L, "Couldn't add a new console command \"%s\"", luaname);
    	}
    	else if (com_return == 1)
    	{ // command existed already -- free our name as the old string will continue to be used
    		CONS_Printf("Replaced command \"%s\"\n", name);
    		Z_Free(name);
    	}
    	else
    	{ // new command was added -- do NOT free the string as it will forever be used by the console
    		CONS_Printf("Added command \"%s\"\n", name);
    	}
    	return 0;
    }
    
    static int lib_comBufAddText(lua_State *L)
    {
    	int n = lua_gettop(L);  /* number of arguments */
    	player_t *plr = NULL;
    	if (n < 2)
    		return luaL_error(L, "COM_BufAddText requires two arguments: player and text.");
    	NOHUD
    	lua_settop(L, 2);
    	if (!lua_isnoneornil(L, 1))
    		plr = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
    	if (plr && plr != &players[consoleplayer])
    		return 0;
    	COM_BufAddTextEx(va("%s\n", luaL_checkstring(L, 2)), COM_LUA);
    	return 0;
    }
    
    static int lib_comBufInsertText(lua_State *L)
    {
    	int n = lua_gettop(L);  /* number of arguments */
    	player_t *plr = NULL;
    	if (n < 2)
    		return luaL_error(L, "COM_BufInsertText requires two arguments: player and text.");
    	NOHUD
    	lua_settop(L, 2);
    	if (!lua_isnoneornil(L, 1))
    		plr = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
    	if (plr && plr != &players[consoleplayer])
    		return 0;
    	COM_BufInsertTextEx(va("%s\n", luaL_checkstring(L, 2)), COM_LUA);
    	return 0;
    }
    
    void LUA_CVarChanged(void *cvar)
    {
    	this_cvar = cvar;
    }
    
    static void Lua_OnChange(void)
    {
    	/// \todo Network this! XD_LUAVAR
    
    	lua_pushcfunction(gL, LUA_GetErrorMessage);
    	lua_insert(gL, 1); // Because LUA_Call wants it at index 1.
    
    	// From CV_OnChange registry field, get the function for this cvar by name.
    	lua_getfield(gL, LUA_REGISTRYINDEX, "CV_OnChange");
    	I_Assert(lua_istable(gL, -1));
    	lua_pushlightuserdata(gL, this_cvar);
    	lua_rawget(gL, -2); // get function
    
    	LUA_RawPushUserdata(gL, this_cvar);
    
    	LUA_Call(gL, 1, 0, 1); // call function(cvar)
    	lua_pop(gL, 1); // pop CV_OnChange table
    	lua_remove(gL, 1); // remove LUA_GetErrorMessage
    }
    
    static int lib_cvRegisterVar(lua_State *L)
    {
    	const char *k;
    	lua_Integer i;
    	consvar_t *cvar;
    	luaL_checktype(L, 1, LUA_TTABLE);
    	lua_settop(L, 1); // Clear out all other possible arguments, leaving only the first one.
    	NOHOOK
    	cvar = ZZ_Calloc(sizeof(consvar_t));
    	LUA_PushUserdata(L, cvar, META_CVAR);
    
    #define FIELDERROR(f, e) luaL_error(L, "bad value for " LUA_QL(f) " in table passed to " LUA_QL("CV_RegisterVar") " (%s)", e);
    #define TYPEERROR(f, t) FIELDERROR(f, va("%s expected, got %s", lua_typename(L, t), luaL_typename(L, -1)))
    
    	lua_pushnil(L);
    	while (lua_next(L, 1))
    	{
    		// stack: cvar table, cvar userdata, key/index, value
    		//            1             2            3        4
    		i = 0;
    		k = NULL;
    		if (lua_isnumber(L, 3))
    		{
    			i = lua_tointeger(L, 3);
    		}
    		else if (lua_isstring(L, 3))
    		{
    			k = lua_tostring(L, 3);
    		}
    
    		if (i == 1 || (k && fasticmp(k, "name")))
    		{
    			if (!lua_isstring(L, 4))
    			{
    				TYPEERROR("name", LUA_TSTRING)
    			}
    			cvar->name = Z_StrDup(lua_tostring(L, 4));
    		}
    		else if (i == 2 || (k && fasticmp(k, "defaultvalue")))
    		{
    			if (!lua_isstring(L, 4))
    			{
    				TYPEERROR("defaultvalue", LUA_TSTRING)
    			}
    			cvar->defaultvalue = Z_StrDup(lua_tostring(L, 4));
    		}
    		else if (i == 3 || (k && fasticmp(k, "flags")))
    		{
    			if (!lua_isnumber(L, 4))
    			{
    				TYPEERROR("flags", LUA_TNUMBER)
    			}
    			cvar->flags = (INT32)lua_tointeger(L, 4);
    		}
    		else if (i == 4 || (k && fasticmp(k, "PossibleValue")))
    		{
    			if (lua_islightuserdata(L, 4))
    			{
    				CV_PossibleValue_t *pv = lua_touserdata(L, 4);
    				if (pv == CV_OnOff || pv == CV_YesNo || pv == CV_Unsigned || pv == CV_Natural)
    					cvar->PossibleValue = pv;
    				else
    					FIELDERROR("PossibleValue", "CV_PossibleValue_t expected, got unrecognised pointer")
    			}
    			else if (lua_istable(L, 4))
    			{
    				// Accepts tables in the form of {MIN=0, MAX=9999} or {Red=0, Green=1, Blue=2}
    				// and converts them to CV_PossibleValue_t {{0,"MIN"},{9999,"MAX"}} or {{0,"Red"},{1,"Green"},{2,"Blue"}}
    				//
    				// I don't really like the way this does it because a single PossibleValue table
    				// being used for multiple cvars will be converted and stored multiple times.
    				// So maybe instead it should be a seperate function which must be run beforehand or something.
    				size_t count = 0;
    				CV_PossibleValue_t *cvpv;
    
    				lua_pushnil(L);
    				while (lua_next(L, 4))
    				{
    					count++;
    					lua_pop(L, 1);
    				}
    
    				lua_getfield(L, LUA_REGISTRYINDEX, "CV_PossibleValue");
    				I_Assert(lua_istable(L, 5));
    				lua_pushlightuserdata(L, cvar);
    				cvpv = lua_newuserdata(L, sizeof(CV_PossibleValue_t) * (count+1));
    				lua_rawset(L, 5);
    				lua_pop(L, 1); // pop CV_PossibleValue registry table
    
    				i = 0;
    				lua_pushnil(L);
    				while (lua_next(L, 4))
    				{
    					// stack: [...] PossibleValue table, index, value
    					//                       4             5      6
    					if (lua_type(L, 5) != LUA_TSTRING
    					|| lua_type(L, 6) != LUA_TNUMBER)
    						FIELDERROR("PossibleValue", "custom PossibleValue table requires a format of string=integer, i.e. {MIN=0, MAX=9999}");
    					cvpv[i].strvalue = Z_StrDup(lua_tostring(L, 5));
    					cvpv[i].value = (INT32)lua_tonumber(L, 6);
    					i++;
    					lua_pop(L, 1);
    				}
    				cvpv[i].value = 0;
    				cvpv[i].strvalue = NULL;
    				cvar->PossibleValue = cvpv;
    			}
    			else
    			{
    				FIELDERROR("PossibleValue", va("%s or CV_PossibleValue_t expected, got %s", lua_typename(L, LUA_TTABLE), luaL_typename(L, -1)))
    			}
    		}
    		else if (cvar->flags & CV_CALL && (i == 5 || (k && fasticmp(k, "func"))))
    		{
    			if (!lua_isfunction(L, 4))
    			{
    				TYPEERROR("func", LUA_TFUNCTION)
    			}
    			lua_getfield(L, LUA_REGISTRYINDEX, "CV_OnChange");
    			I_Assert(lua_istable(L, 5));
    			lua_pushlightuserdata(L, cvar);
    			lua_pushvalue(L, 4);
    			lua_rawset(L, 5);
    			lua_pop(L, 1);
    			cvar->func = Lua_OnChange;
    		}
    		lua_pop(L, 1);
    	}
    
    #undef FIELDERROR
    #undef TYPEERROR
    
    	if (!cvar->name || cvar->name[0] == '\0')
    	{
    		return luaL_error(L, M_GetText("Variable has no name!"));
    	}
    
    	if (!cvar->defaultvalue)
    	{
    		return luaL_error(L, M_GetText("Variable has no defaultvalue!"));
    	}
    
    	if ((cvar->flags & CV_NOINIT) && !(cvar->flags & CV_CALL))
    	{
    		return luaL_error(L, M_GetText("Variable %s has CV_NOINIT without CV_CALL"), cvar->name);
    	}
    
    	if ((cvar->flags & CV_CALL) && !cvar->func)
    	{
    		return luaL_error(L, M_GetText("Variable %s has CV_CALL without a function"), cvar->name);
    	}
    
    	cvar->flags |= CV_ALLOWLUA;
    	// actually time to register it to the console now! Finally!
    	cvar->flags |= CV_MODIFIED;
    	CV_RegisterVar(cvar);
    
    	if (cvar->flags & CV_MODIFIED)
    	{
    		return luaL_error(L, "failed to register cvar (probable conflict with internal variable/command names)");
    	}
    
    	// return cvar userdata
    	return 1;
    }
    
    static int lib_cvFindVar(lua_State *L)
    {
    	const char *name = luaL_checkstring(L, 1);
    	LUA_PushUserdata(L, CV_FindVar(name), META_CVAR);
    	return 1;
    }
    
    static int CVarSetFunction
    (
    		lua_State *L,
    		void (*Set)(consvar_t *, const char *),
    		void (*SetValue)(consvar_t *, INT32)
    ){
    	consvar_t *cvar = *(consvar_t **)luaL_checkudata(L, 1, META_CVAR);
    
    	if (!(cvar->flags & CV_ALLOWLUA))
    		return luaL_error(L, "Variable '%s' cannot be set from Lua.", cvar->name);
    
    	switch (lua_type(L, 2))
    	{
    		case LUA_TSTRING:
    			(*Set)(cvar, lua_tostring(L, 2));
    			break;
    		case LUA_TNUMBER:
    			(*SetValue)(cvar, (INT32)lua_tonumber(L, 2));
    			break;
    		default:
    			return luaL_typerror(L, 1, "string or number");
    	}
    
    	return 0;
    }
    
    static int lib_cvSet(lua_State *L)
    {
    	return CVarSetFunction(L, CV_Set, CV_SetValue);
    }
    
    static int lib_cvStealthSet(lua_State *L)
    {
    	return CVarSetFunction(L, CV_StealthSet, CV_StealthSetValue);
    }
    
    static int lib_cvAddValue(lua_State *L)
    {
    	consvar_t *cvar = *(consvar_t **)luaL_checkudata(L, 1, META_CVAR);
    
    	if (!(cvar->flags & CV_ALLOWLUA))
    		return luaL_error(L, "Variable %s cannot be set from Lua.", cvar->name);
    
    	CV_AddValue(cvar, (INT32)luaL_checknumber(L, 2));
    
    	return 0;
    }
    
    // CONS_Printf for a single player
    // Use 'print' in baselib for a global message.
    static int lib_consPrintf(lua_State *L)
    {
    	int n = lua_gettop(L);  /* number of arguments */
    	int i;
    	player_t *plr;
    	if (n < 2)
    		return luaL_error(L, "CONS_Printf requires at least two arguments: player and text.");
    	//HUDSAFE
    
    	plr = *((player_t **)luaL_checkudata(L, 1, META_PLAYER));
    	if (!plr)
    		return LUA_ErrInvalid(L, "player_t");
    	if (plr != &players[consoleplayer])
    		return 0;
    	lua_getglobal(L, "tostring");
    	for (i=2; i<=n; i++) {
    		const char *s;
    		lua_pushvalue(L, -1);  /* function to be called */
    		lua_pushvalue(L, i);   /* value to print */
    		lua_call(L, 1, 1);
    		s = lua_tostring(L, -1);  /* get result */
    		if (s == NULL)
    			return luaL_error(L, LUA_QL("tostring") " must return a string to "
    													 LUA_QL("CONS_Printf"));
    		if (i>2) CONS_Printf("\n");
    		CONS_Printf("%s", s);
    		lua_pop(L, 1);  /* pop result */
    	}
    	CONS_Printf("\n");
    	return 0;
    }
    
    static luaL_Reg lib[] = {
    	{"COM_AddCommand", lib_comAddCommand},
    	{"COM_BufAddText", lib_comBufAddText},
    	{"COM_BufInsertText", lib_comBufInsertText},
    	{"CV_RegisterVar", lib_cvRegisterVar},
    	{"CV_FindVar", lib_cvFindVar},
    	{"CV_Set", lib_cvSet},
    	{"CV_StealthSet", lib_cvStealthSet},
    	{"CV_AddValue", lib_cvAddValue},
    	{"CONS_Printf", lib_consPrintf},
    	{NULL, NULL}
    };
    
    static int cvar_get(lua_State *L)
    {
    	consvar_t *cvar = *(consvar_t **)luaL_checkudata(L, 1, META_CVAR);
    	const char *field = luaL_checkstring(L, 2);
    
    	if(fastcmp(field,"name"))
    		lua_pushstring(L, cvar->name);
    	else if(fastcmp(field,"defaultvalue"))
    		lua_pushstring(L, cvar->defaultvalue);
    	else if(fastcmp(field,"flags"))
    		lua_pushinteger(L, cvar->flags);
    	else if(fastcmp(field,"value"))
    		lua_pushinteger(L, cvar->value);
    	else if(fastcmp(field,"string"))
    		lua_pushstring(L, cvar->string);
    	else if(fastcmp(field,"changed"))
    		lua_pushboolean(L, cvar->changed);
    	else if (devparm)
    		return luaL_error(L, LUA_QL("consvar_t") " has no field named " LUA_QS, field);
    	else
    		return 0;
    	return 1;
    }
    
    int LUA_ConsoleLib(lua_State *L)
    {
    	// Metatable for consvar_t
    	luaL_newmetatable(L, META_CVAR);
    		lua_pushcfunction(L, cvar_get);
    		lua_setfield(L, -2, "__index");
    	lua_pop(L,1);
    
    	// Set empty registry tables
    	lua_newtable(L);
    	lua_setfield(L, LUA_REGISTRYINDEX, "COM_Command");
    	lua_newtable(L);
    	lua_setfield(L, LUA_REGISTRYINDEX, "CV_Vars");
    	lua_newtable(L);
    	lua_setfield(L, LUA_REGISTRYINDEX, "CV_PossibleValue");
    	lua_newtable(L);
    	lua_setfield(L, LUA_REGISTRYINDEX, "CV_OnChange");
    
    	// Push opaque CV_PossibleValue pointers
    	// Because I don't care enough to bother.
    	lua_pushlightuserdata(L, CV_OnOff);
    	lua_setglobal(L, "CV_OnOff");
    	lua_pushlightuserdata(L, CV_YesNo);
    	lua_setglobal(L, "CV_YesNo");
    	lua_pushlightuserdata(L, CV_Unsigned);
    	lua_setglobal(L, "CV_Unsigned");
    	lua_pushlightuserdata(L, CV_Natural);
    	lua_setglobal(L, "CV_Natural");
    
    	// Set global functions
    	lua_pushvalue(L, LUA_GLOBALSINDEX);
    	luaL_register(L, NULL, lib);
    	return 0;
    }