diff --git a/src/d_think.h b/src/d_think.h
index ad16e4ed8a250edfa3bfe004953896246d7ef20a..330747e4b8c9ca1a47fa1a4fbc4ed8d6765da7f8 100644
--- a/src/d_think.h
+++ b/src/d_think.h
@@ -35,10 +35,7 @@ enum
 typedef struct
 {
 	unsigned length;
-	union {
-		char *chars;
-		const char *const_chars;
-	};
+	char *chars;
 } action_string_t;
 
 typedef struct
diff --git a/src/deh_lua.c b/src/deh_lua.c
index 913727df207a8fd4c00088f5c02e2fab4e53585c..257d8ae0ad5a131a01ac747c0389712211ba1d79 100644
--- a/src/deh_lua.c
+++ b/src/deh_lua.c
@@ -188,6 +188,8 @@ static int action_call(lua_State *L)
 {
 	actionf_t *action = *((actionf_t **)luaL_checkudata(L, 1, META_ACTION));
 	mobj_t *actor = *((mobj_t **)luaL_checkudata(L, 2, META_MOBJ));
+	if (!actor)
+		return LUA_ErrInvalid(L, "mobj_t");
 
 	action_val_t *call_args = NULL;
 
@@ -195,19 +197,27 @@ static int action_call(lua_State *L)
 	int num_action_args = n - 2;
 	if (num_action_args > 0)
 	{
-		call_args = Z_Malloc(num_action_args * sizeof(action_val_t), PU_STATIC, NULL);
+		call_args = Z_Calloc(num_action_args * sizeof(action_val_t), PU_STATIC, NULL);
+
 		for (int i = 3, j = 0; i <= n; i++)
-			LUA_ValueToActionVal(L, i, &call_args[j++]);
-	}
+		{
+			if (!LUA_ValueIsValidActionVal(L, i))
+			{
+				for (int k = 0; k < j; k++)
+					Action_FreeValue(call_args[k]);
+				Z_Free(call_args);
+				return luaL_error(L, va("value of type %s cannot be passed to an action", luaL_typename(L, i)));
+			}
 
-	if (!actor)
-	{
-		Z_Free(call_args);
-		return LUA_ErrInvalid(L, "mobj_t");
+			LUA_ValueToActionVal(L, i, &call_args[j++]);
+		}
 	}
 
 	action->acpscr(actor, call_args, num_action_args);
 
+	for (int i = 0; i < num_action_args; i++)
+		Action_FreeValue(call_args[i]);
+
 	Z_Free(call_args);
 
 	return 0;
diff --git a/src/deh_soc.c b/src/deh_soc.c
index eb68e956c78aac8eb10f6c536c117bf290a9749b..ee339763e4f91eb90c5d7b94f4b744085191d6c3 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -2758,14 +2758,6 @@ void readframe(MYFILE *f, INT32 num)
 			{
 				states[num].nextstate = get_state(word2);
 			}
-			else if (fastcmp(word1, "VAR1"))
-			{
-				states[num].vars[0] = ACTION_INTEGER_VAL((INT32)get_number(word2));
-			}
-			else if (fastcmp(word1, "VAR2"))
-			{
-				states[num].vars[1] = ACTION_INTEGER_VAL((INT32)get_number(word2));
-			}
 			else if (fastcmp(word1, "ACTION"))
 			{
 				size_t z;
@@ -2812,6 +2804,14 @@ void readframe(MYFILE *f, INT32 num)
 
 				free(actiontocompare);
 			}
+			else if (!strnicmp(word1, "VAR", 3)
+			&& strlen(word1) == 4
+			&& word1[3] >= '1' && word1[3] <= '8')
+			{
+				unsigned varSlot = (word1[3] - 0x30) - 1;
+				Action_FreeValue(states[num].vars[varSlot]);
+				states[num].vars[varSlot] = ACTION_INTEGER_VAL((INT32)get_number(word2));
+			}
 			else
 				deh_warning("Frame %d: unknown word '%s'", num, word1);
 		}
diff --git a/src/lua_baselib.c b/src/lua_baselib.c
index 16f238abeb19575cb5ce0e626be4660e723727ca..255b35dd9870bb290039c5e37eb7949573ac2af2 100644
--- a/src/lua_baselib.c
+++ b/src/lua_baselib.c
@@ -151,6 +151,7 @@ static const struct {
 	const char *utype;
 } meta2utype[] = {
 	{META_STATE,        "state_t"},
+	{META_STATEVARS,    "state_t.vars"},
 	{META_MOBJINFO,     "mobjinfo_t"},
 	{META_SFXINFO,      "sfxinfo_t"},
 	{META_SKINCOLOR,    "skincolor_t"},
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 9f5c33563e00c3f8791a26abddd8b5794058623e..ec77c62eb655bd6adc17fe3dec2646e397343980 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -35,6 +35,95 @@ extern UINT8 skincolor_modified[];
 
 state_t *astate;
 
+boolean LUA_ValueIsValidActionVal(lua_State *L, int i)
+{
+	switch (lua_type(L, i))
+	{
+	case LUA_TNUMBER:
+	case LUA_TBOOLEAN:
+	case LUA_TSTRING:
+	case LUA_TNIL:
+		return true;
+	}
+
+	return false;
+}
+
+#define CHECK_ACTION_VAL_TYPE(n) \
+	if (!LUA_ValueIsValidActionVal(L, n)) \
+		return luaL_error(L, va("values of type %s cannot be passed to actions as an argument", luaL_typename(L, n)))
+
+void LUA_ValueToActionVal(lua_State *L, int i, action_val_t *val)
+{
+	action_val_t value;
+
+	switch (lua_type(L, i))
+	{
+	case LUA_TNUMBER:
+		value = ACTION_INTEGER_VAL((INT32)luaL_optinteger(L, i, 0));
+		break;
+	case LUA_TBOOLEAN:
+		value = ACTION_BOOLEAN_VAL(lua_toboolean(L, i));
+		break;
+	case LUA_TSTRING:
+	{
+		action_string_t stringval;
+		Action_MakeString(&stringval, Z_StrDup(lua_tostring(L, i)));
+		value = ACTION_STRING_VAL(stringval);
+		break;
+	}
+	default:
+		value = ACTION_NULL_VAL;
+		break;
+	}
+
+	memcpy(val, &value, sizeof(action_val_t));
+}
+
+#define FIELDERROR(f, e) luaL_error(L, "bad value for " LUA_QL(f) " in table passed to states[] (%s)", e)
+#define TYPEERROR(f, t1, t2) FIELDERROR(f, va("%s expected, got %s", lua_typename(L, t1), lua_typename(L, t2)))
+
+static boolean GetActionValuesFromTable(lua_State *L, action_val_t *vars, int n)
+{
+	lua_Integer i = 0;
+	if (lua_istable(L, n))
+	{
+		lua_pushnil(L);
+		while (lua_next(L, n))
+		{
+			if (lua_isnumber(L, n + 1))
+			{
+				i = lua_tointeger(L, n + 1);
+				if (i < 1 || i > MAX_ACTION_VARS)
+				{
+					FIELDERROR("vars", va("var %d is invalid", i));
+					return false;
+				}
+				i--;
+			}
+			else
+			{
+				FIELDERROR("vars", "vars table requires keys to be numbers");
+			}
+
+			CHECK_ACTION_VAL_TYPE(n + 2);
+			Action_FreeValue(vars[i]);
+			LUA_ValueToActionVal(L, n + 2, &vars[i]);
+			lua_pop(L, 1);
+		}
+	}
+	else
+	{
+		FIELDERROR("vars", va("%s expected, got %s", lua_typename(L, LUA_TTABLE), luaL_typename(L, -1)));
+		return false;
+	}
+
+	return true;
+}
+
+#undef FIELDERROR
+#undef TYPEERROR
+
 enum sfxinfo_read {
 	sfxinfor_name = 0,
 	sfxinfor_singular,
@@ -776,14 +865,21 @@ static int lib_setState(lua_State *L)
 				return luaL_typerror(L, 3, "function");
 			}
 		} else if (i == 5 || (str && fastcmp(str, "var1"))) {
+			CHECK_ACTION_VAL_TYPE(3);
+			Action_FreeValue(state->vars[0]);
 			LUA_ValueToActionVal(L, 3, &state->vars[0]);
 		} else if (i == 6 || (str && fastcmp(str, "var2"))) {
+			CHECK_ACTION_VAL_TYPE(3);
+			Action_FreeValue(state->vars[1]);
 			LUA_ValueToActionVal(L, 3, &state->vars[1]);
 		} else if (i == 7 || (str && fastcmp(str, "nextstate"))) {
 			value = luaL_checkinteger(L, 3);
 			if (value < S_NULL || value >= NUMSTATES)
 				return luaL_error(L, "nextstate number %d is invalid.", value);
 			state->nextstate = (statenum_t)value;
+		} else if (str && fastcmp(str, "vars")) {
+			if (!GetActionValuesFromTable(L, state->vars, 3))
+				return 0;
 		}
 		lua_pop(L, 1);
 	}
@@ -834,33 +930,6 @@ boolean LUA_SetLuaAction(void *stv, const char *action)
 
 static UINT8 superstack[NUMACTIONS];
 
-void LUA_ValueToActionVal(lua_State *L, int i, action_val_t *val)
-{
-	action_val_t value;
-
-	switch (lua_type(L, i))
-	{
-	case LUA_TNUMBER:
-		value = ACTION_INTEGER_VAL((INT32)luaL_optinteger(L, i, 0));
-		break;
-	case LUA_TBOOLEAN:
-		value = ACTION_BOOLEAN_VAL(lua_toboolean(L, i));
-		break;
-	case LUA_TSTRING:
-	{
-		action_string_t stringval;
-		Action_MakeString(&stringval, lua_tostring(L, i));
-		value = ACTION_STRING_VAL(stringval);
-		break;
-	}
-	default:
-		value = ACTION_NULL_VAL;
-		break;
-	}
-
-	memcpy(val, &value, sizeof(action_val_t));
-}
-
 boolean LUA_CallAction(enum actionnum actionnum, mobj_t *actor, action_val_t *args, unsigned argcount)
 {
 	I_Assert(actor != NULL);
@@ -987,6 +1056,10 @@ static int state_get(lua_State *L)
 		PushActionValue(L, st->vars[1]);
 		return 1;
 	}
+	else if (fastcmp(field,"vars")) {
+		LUA_PushUserdata(L, st->vars, META_STATEVARS);
+		return 1;
+	}
 	else if (fastcmp(field,"nextstate"))
 		number = st->nextstate;
 	else if (devparm)
@@ -1023,7 +1096,7 @@ static int state_set(lua_State *L)
 		switch(lua_type(L, 3))
 		{
 		case LUA_TNIL: // Null? Set the action to nothing, then.
-			st->action.acp1 = NULL;
+			st->action.acpscr = NULL;
 			break;
 		case LUA_TSTRING: // It's a string, expect the name of a built-in action
 			LUA_SetActionByName(st, lua_tostring(L, 3));
@@ -1036,8 +1109,6 @@ static int state_set(lua_State *L)
 				return luaL_error(L, "not a valid action?");
 
 			st->action = *action;
-			st->action.acv = action->acv;
-			st->action.acp1 = action->acp1;
 			break;
 		}
 		case LUA_TFUNCTION: // It's a function (a Lua function or a C function? either way!)
@@ -1049,13 +1120,23 @@ static int state_set(lua_State *L)
 			lua_pop(L, 1); // pop LREG_STATEACTION
 			st->action.acpscr = (actionf_script)A_Lua; // Set the action for the userdata.
 			break;
-		default: // ?!
+		default: // Something else
 			return luaL_typerror(L, 3, "function");
 		}
-	} else if (fastcmp(field,"var1"))
+	} else if (fastcmp(field,"var1")) {
+		CHECK_ACTION_VAL_TYPE(3);
+		Action_FreeValue(st->vars[0]);
 		LUA_ValueToActionVal(L, 3, &st->vars[0]);
-	else if (fastcmp(field,"var2"))
+	}
+	else if (fastcmp(field,"var2")) {
+		CHECK_ACTION_VAL_TYPE(3);
+		Action_FreeValue(st->vars[1]);
 		LUA_ValueToActionVal(L, 3, &st->vars[1]);
+	}
+	else if (fastcmp(field,"vars")) {
+		if (!GetActionValuesFromTable(L, st->vars, 3))
+			return 0;
+	}
 	else if (fastcmp(field,"nextstate")) {
 		value = luaL_checkinteger(L, 3);
 		if (value < S_NULL || value >= NUMSTATES)
@@ -1075,6 +1156,34 @@ static int state_num(lua_State *L)
 	return 1;
 }
 
+static int statevars_get(lua_State *L)
+{
+	action_val_t *vars = *((action_val_t **)luaL_checkudata(L, 1, META_STATEVARS));
+	int n = luaL_checkinteger(L, 2);
+	if (n <= 0 || n > MAX_ACTION_VARS)
+		return luaL_error(L, LUA_QL("state_t") " field 'vars' index %d out of range (1 - %d)", n, MAX_ACTION_VARS);
+	PushActionValue(L, vars[n-1]);
+	return 1;
+}
+
+static int statevars_set(lua_State *L)
+{
+	action_val_t *vars = *((action_val_t **)luaL_checkudata(L, 1, META_STATEVARS));
+	int n = luaL_checkinteger(L, 2);
+	if (n <= 0 || n > MAX_ACTION_VARS)
+		return luaL_error(L, LUA_QL("state_t") " field 'vars' index %d out of range (1 - %d)", n, MAX_ACTION_VARS);
+	n--;
+	Action_FreeValue(vars[n]);
+	LUA_ValueToActionVal(L, 3, &vars[n]);
+	return 0;
+}
+
+static int statevars_len(lua_State *L)
+{
+	lua_pushinteger(L, MAX_ACTION_VARS);
+	return 1;
+}
+
 ///////////////
 // MOBJ INFO //
 ///////////////
@@ -1969,6 +2078,7 @@ int LUA_InfoLib(lua_State *L)
 	lua_setfield(L, LUA_REGISTRYINDEX, LREG_ACTIONS);
 
 	LUA_RegisterUserdataMetatable(L, META_STATE, state_get, state_set, state_num);
+	LUA_RegisterUserdataMetatable(L, META_STATEVARS, statevars_get, statevars_set, statevars_len);
 	LUA_RegisterUserdataMetatable(L, META_MOBJINFO, mobjinfo_get, mobjinfo_set, mobjinfo_num);
 	LUA_RegisterUserdataMetatable(L, META_SKINCOLOR, skincolor_get, skincolor_set, skincolor_num);
 	LUA_RegisterUserdataMetatable(L, META_COLORRAMP, colorramp_get, colorramp_set, colorramp_len);
diff --git a/src/lua_libs.h b/src/lua_libs.h
index 2e3c706528f24dd127f77b0e1256463643c59654..e5305328a6605149e83d44b722c6678c6ddc4f8f 100644
--- a/src/lua_libs.h
+++ b/src/lua_libs.h
@@ -24,6 +24,7 @@ extern boolean ignoregameinputs;
 #define LREG_METATABLES "METATABLES"
 
 #define META_STATE "STATE_T*"
+#define META_STATEVARS "STATE_T*VARS"
 #define META_MOBJINFO "MOBJINFO_T*"
 #define META_SFXINFO "SFXINFO_T*"
 #define META_SKINCOLOR "SKINCOLOR_T*"
diff --git a/src/lua_script.h b/src/lua_script.h
index 1074fcbc09f55309bb60692ffa7450dee3f6fcff..23563e09687fb20c693d2c0131420c61f376f5c9 100644
--- a/src/lua_script.h
+++ b/src/lua_script.h
@@ -47,6 +47,7 @@ extern INT32 lua_lumploading; // is LUA_LoadLump being called?
 int LUA_GetErrorMessage(lua_State *L);
 int LUA_Call(lua_State *L, int nargs, int nresults, int errorhandlerindex);
 boolean LUA_CallAction(enum actionnum actionnum, mobj_t *actor, action_val_t *args, unsigned argcount);
+boolean LUA_ValueIsValidActionVal(lua_State *L, int i);
 void LUA_ValueToActionVal(lua_State *L, int i, action_val_t *val);
 void LUA_LoadLump(UINT16 wad, UINT16 lump, boolean noresults);
 #ifdef LUA_ALLOW_BYTECODE
diff --git a/src/p_action.h b/src/p_action.h
index 9a23cab0d2ebf2fd2b8b24823f391e4eca1759bd..fcf95f7fdac8ac4f62e5e649efee319965387de1 100644
--- a/src/p_action.h
+++ b/src/p_action.h
@@ -20,7 +20,10 @@ INT32 Action_ValueToInteger(action_val_t value);
 
 char *Action_ValueToString(action_val_t value);
 
-void Action_MakeString(action_string_t *out, const char *str);
+void Action_FreeValue(action_val_t value);
+
+void Action_MakeString(action_string_t *out, char *str);
+void Action_FreeStringChars(action_string_t *str);
 
 // IMPORTANT NOTE: If you add/remove from this list of action
 // functions, don't forget to update them in deh_tables.c!
diff --git a/src/p_enemy.c b/src/p_enemy.c
index 95d3baabed9b8d6cd9821fe796b106aa51560c7a..28cfaac59dd9361b94acb5468666041683d1aef9 100644
--- a/src/p_enemy.c
+++ b/src/p_enemy.c
@@ -700,12 +700,24 @@ for (i = cvar.value; i; --i) spawnchance[numchoices++] = type
 //
 // ACTION ROUTINES
 //
-void Action_MakeString(action_string_t *out, const char *str)
+void Action_FreeValue(action_val_t value)
 {
-	out->const_chars = str;
+	if (ACTION_VAL_IS_STRING(value))
+		Action_FreeStringChars(&value.v_string);
+}
+
+void Action_MakeString(action_string_t *out, char *str)
+{
+	out->chars = str;
 	out->length = strlen(str);
 }
 
+void Action_FreeStringChars(action_string_t *str)
+{
+	Z_Free(str->chars);
+	str->chars = NULL;
+}
+
 static const char *Action_GetTypeName(UINT8 type)
 {
 	switch (type)