diff --git a/src/d_think.h b/src/d_think.h
index 12d44a8a202af5aeca9f7f58a18c5efc9f3242e1..a7924105664224ad8b13b774f7934e950695ecff 100644
--- a/src/d_think.h
+++ b/src/d_think.h
@@ -35,6 +35,7 @@ typedef struct
 {
 	unsigned length;
 	char *chars;
+	UINT32 hash;
 } action_string_t;
 
 typedef struct
@@ -44,7 +45,7 @@ typedef struct
 	{
 		INT32 v_integer;
 		boolean v_bool;
-		action_string_t v_string;
+		UINT32 v_string_id;
 	};
 } action_val_t;
 
@@ -56,11 +57,11 @@ typedef struct
 #define ACTION_NULL_VAL (action_val_t){ .type = ACTION_VAL_NULL }
 #define ACTION_INTEGER_VAL(val) (action_val_t){ .type = ACTION_VAL_INTEGER, .v_integer = (val) }
 #define ACTION_BOOLEAN_VAL(val) (action_val_t){ .type = ACTION_VAL_BOOLEAN, .v_bool = (val) }
-#define ACTION_STRING_VAL(val) (action_val_t){ .type = ACTION_VAL_STRING, .v_string = (val) }
+#define ACTION_STRING_VAL(val) (action_val_t){ .type = ACTION_VAL_STRING, .v_string_id = (val) }
 
 #define ACTION_VAL_AS_INTEGER(val) ((val).v_integer)
 #define ACTION_VAL_AS_BOOLEAN(val) ((val).v_bool)
-#define ACTION_VAL_AS_STRING(val) ((val).v_string)
+#define ACTION_VAL_AS_STRING(val) ((val).v_string_id)
 
 //
 // Experimental stuff.
diff --git a/src/deh_lua.c b/src/deh_lua.c
index 257d8ae0ad5a131a01ac747c0389712211ba1d79..6e6cacf312cd2a9cc4a669e029a63a1570091f2f 100644
--- a/src/deh_lua.c
+++ b/src/deh_lua.c
@@ -203,8 +203,6 @@ static int action_call(lua_State *L)
 		{
 			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)));
 			}
@@ -215,9 +213,6 @@ static int action_call(lua_State *L)
 
 	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 877b1c43886889b7836e923a56c2d68cd7f85631..9b8d7cc5dd65f7ceadf2accd270c57fdef3fe64e 100644
--- a/src/deh_soc.c
+++ b/src/deh_soc.c
@@ -86,17 +86,17 @@ static boolean check_string_token(char *word)
 	return true;
 }
 
-static boolean parse_string_token(action_string_t *string, char *word)
+static boolean parse_string_token(char **string_chars, unsigned *string_length, char *word)
 {
 	int token_length = strlen(word);
 	int length = token_length - 2;
 	if (length <= 0)
 		return false;
 
-	string->chars = Z_Calloc(length + 1, PU_STATIC, NULL);
-	string->length = 0;
+	*string_chars = Z_Calloc(length + 1, PU_STATIC, NULL);
+	*string_length = 0;
 
-	char *chars = string->chars;
+	char *chars = *string_chars;
 
 	char *str = word + 1;
 	char *str_end = word + token_length - 1;
@@ -116,7 +116,7 @@ static boolean parse_string_token(action_string_t *string, char *word)
 				*chars = '\\';
 			else
 			{
-				Z_Free(string->chars);
+				Z_Free(*string_chars);
 				deh_warning("Invalid escape character in string token");
 				return false;
 			}
@@ -124,7 +124,7 @@ static boolean parse_string_token(action_string_t *string, char *word)
 		else
 			*chars = *str;
 
-		string->length++;
+		(*string_length)++;
 		chars++;
 		str++;
 	}
@@ -142,15 +142,18 @@ static boolean parse_word(action_val_t *value, char *word)
 			return false;
 		}
 
-		action_string_t string;
+		char *string_chars = NULL;
+		unsigned string_length = 0;
 
-		if (!parse_string_token(&string, word))
+		if (!parse_string_token(&string_chars, &string_length, word))
 		{
 			*value = ACTION_NULL_VAL;
 			return false;
 		}
 
-		*value = ACTION_STRING_VAL(string);
+		*value = ACTION_STRING_VAL(Action_AddString(string_chars, string_length));
+
+		Z_Free(string_chars);
 	}
 	else if (fastcmp(word, "true"))
 	{
@@ -2905,7 +2908,6 @@ void readframe(MYFILE *f, INT32 num)
 			&& word1[3] >= '1' && word1[3] <= '8')
 			{
 				unsigned varSlot = (word1[3] - 0x30) - 1;
-				Action_FreeValue(states[num].vars[varSlot]);
 				parse_word(&states[num].vars[varSlot], word2);
 			}
 			else
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 6eba2308c4e576dbef543cf1bac6fcc54b22dc78..2a273b823ce40eafa1cee6cb8fcee06b41890c00 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -67,9 +67,9 @@ void LUA_ValueToActionVal(lua_State *L, int i, action_val_t *val)
 		break;
 	case LUA_TSTRING:
 	{
-		action_string_t stringval;
-		Action_MakeString(&stringval, Z_StrDup(lua_tostring(L, i)));
-		value = ACTION_STRING_VAL(stringval);
+		size_t string_length = 0;
+		const char *string = lua_tolstring(L, i, &string_length);
+		value = ACTION_STRING_VAL(Action_AddString(string, string_length));
 		break;
 	}
 	default:
@@ -107,7 +107,6 @@ static boolean GetActionValuesFromTable(lua_State *L, action_val_t *vars, int n)
 			}
 
 			CHECK_ACTION_VAL_TYPE(n + 2);
-			Action_FreeValue(vars[i]);
 			LUA_ValueToActionVal(L, n + 2, &vars[i]);
 			lua_pop(L, 1);
 		}
@@ -716,7 +715,13 @@ static void PushActionValue(lua_State *L, action_val_t arg)
 	else if (ACTION_VAL_IS_BOOLEAN(arg))
 		lua_pushboolean(L, ACTION_VAL_AS_BOOLEAN(arg));
 	else if (ACTION_VAL_IS_STRING(arg))
-		lua_pushlstring(L, arg.v_string.chars, arg.v_string.length);
+	{
+		action_string_t *string = Action_GetString(ACTION_VAL_AS_STRING(arg));
+		if (string)
+			lua_pushlstring(L, string->chars, string->length);
+		else
+			lua_pushnil(L);
+	}
 	else if (ACTION_VAL_IS_NULL(arg))
 		lua_pushnil(L);
 }
@@ -864,11 +869,9 @@ static int lib_setState(lua_State *L)
 			}
 		} 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);
@@ -1123,12 +1126,10 @@ static int state_set(lua_State *L)
 		}
 	} 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")) {
 		CHECK_ACTION_VAL_TYPE(3);
-		Action_FreeValue(st->vars[1]);
 		LUA_ValueToActionVal(L, 3, &st->vars[1]);
 	}
 	else if (fastcmp(field,"vars")) {
@@ -1171,7 +1172,6 @@ static int statevars_set(lua_State *L)
 	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;
 }
diff --git a/src/p_action.c b/src/p_action.c
index 637fdbb20e8576a81418a78f2e2cbea6e3e13432..1b8a9fbd8e77825e7603b92166d9c312858116c6 100644
--- a/src/p_action.c
+++ b/src/p_action.c
@@ -26,27 +26,60 @@
 #include "lua_hook.h"
 #include "m_cond.h" // SECRET_SKIN
 
-INT32 modulothing;
+#if 0
+static void Action_FreeStringChars(action_string_t *str)
+{
+	Z_Free(str->chars);
+	str->chars = NULL;
+}
+#endif
 
-//
-// ACTION ROUTINES
-//
-void Action_FreeValue(action_val_t value)
+static action_string_t *actionstrings = NULL;
+static UINT32 numactionstrings = 0;
+
+UINT32 Action_FindMatchingString(const char *str, unsigned length)
 {
-	if (ACTION_VAL_IS_STRING(value))
-		Action_FreeStringChars(&value.v_string);
+	UINT32 hash = quickncasehash(str, strlen(str));
+
+	for (UINT32 i = 0; i < numactionstrings; i++)
+	{
+		if (length == actionstrings[i].length
+		&& hash == actionstrings[i].hash
+		&& memcmp(str, actionstrings[i].chars, length) == 0)
+			return i;
+	}
+
+	return ACTION_NO_STRING;
 }
 
-void Action_MakeString(action_string_t *out, char *str)
+UINT32 Action_AddString(const char *str, unsigned length)
 {
-	out->chars = str;
-	out->length = strlen(str);
+	UINT32 hash = quickncasehash(str, length);
+
+	UINT32 id = Action_FindMatchingString(str, length);
+	if (id != ACTION_NO_STRING)
+		return id;
+
+	id = numactionstrings++;
+	actionstrings = Z_Realloc(actionstrings, sizeof(action_string_t) * numactionstrings, PU_STATIC, NULL);
+
+	action_string_t *astr = &actionstrings[id];
+	astr->length = length;
+	astr->hash = hash;
+
+	astr->chars = ZZ_Alloc(length + 1);
+	memcpy(astr->chars, str, length);
+	astr->chars[length] = '\0';
+
+	return id;
 }
 
-void Action_FreeStringChars(action_string_t *str)
+action_string_t *Action_GetString(UINT32 string_id)
 {
-	Z_Free(str->chars);
-	str->chars = NULL;
+	if (string_id >= numactionstrings)
+		return NULL;
+
+	return &actionstrings[string_id];
 }
 
 static const char *Action_GetTypeName(UINT8 type)
@@ -92,9 +125,10 @@ INT32 Action_ValueToInteger(action_val_t value)
 
 char *Action_ValueToString(action_val_t value)
 {
+	size_t bufsize = 128;
+
 	if (ACTION_VAL_IS_INTEGER(value))
 	{
-		size_t bufsize = 128;
 		char *buffer = Z_Malloc(bufsize, PU_STATIC, NULL);
 		snprintf(buffer, bufsize, "<integer> %d", Action_ValueToInteger(value));
 		return buffer;
@@ -102,7 +136,11 @@ char *Action_ValueToString(action_val_t value)
 	else if (ACTION_VAL_IS_BOOLEAN(value))
 		return ACTION_VAL_AS_BOOLEAN(value) ? Z_StrDup("<boolean> true") : Z_StrDup("<boolean> false");
 	else if (ACTION_VAL_IS_STRING(value))
-		return Z_StrDup("<string>");
+	{
+		char *buffer = Z_Malloc(bufsize, PU_STATIC, NULL);
+		snprintf(buffer, bufsize, "<string> %d", ACTION_VAL_AS_STRING(value));
+		return buffer;
+	}
 	else if (ACTION_VAL_IS_NULL(value))
 		return Z_StrDup("<null>");
 
@@ -129,6 +167,12 @@ static INT32 GetInteger(action_val_t *args, unsigned argcount, unsigned argnum,
 
 #define LUA_CALL_ACTION(action) LUA_CallAction(action, actor, args, argcount)
 
+//
+// ACTION ROUTINES
+//
+
+INT32 modulothing;
+
 // Function: A_Look
 //
 // Description: Look for a player and set your target to them.
diff --git a/src/p_action.h b/src/p_action.h
index fcf95f7fdac8ac4f62e5e649efee319965387de1..49c5e298f6ed111404af5d60ba5e78ffe3dce6c7 100644
--- a/src/p_action.h
+++ b/src/p_action.h
@@ -16,14 +16,16 @@
 
 #include "p_mobj.h"
 
+#define ACTION_NO_STRING 0xFFFFFFFF
+
 INT32 Action_ValueToInteger(action_val_t value);
 
 char *Action_ValueToString(action_val_t value);
 
-void Action_FreeValue(action_val_t value);
+UINT32 Action_FindMatchingString(const char *str, unsigned length);
+UINT32 Action_AddString(const char *str, unsigned length);
 
-void Action_MakeString(action_string_t *out, char *str);
-void Action_FreeStringChars(action_string_t *str);
+action_string_t *Action_GetString(UINT32 string_id);
 
 // IMPORTANT NOTE: If you add/remove from this list of action
 // functions, don't forget to update them in deh_tables.c!