diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index 4b3c9c014ef6aa4e65853119961276c77e0c304d..52a875e50c0c550f2c10dbb1acd56647955baf01 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -14,6 +14,7 @@
 #include "fastcmp.h"
 #include "r_defs.h"
 #include "r_local.h"
+#include "r_translation.h"
 #include "st_stuff.h" // hudinfo[]
 #include "g_game.h"
 #include "i_video.h" // rendermode
@@ -1125,7 +1126,10 @@ static int libd_getColormap(lua_State *L)
 	INT32 skinnum = TC_DEFAULT;
 	skincolornum_t color = luaL_optinteger(L, 2, 0);
 	UINT8* colormap = NULL;
+	int translation_id = -1;
+
 	HUDONLY
+
 	if (lua_isnoneornil(L, 1))
 		; // defaults to TC_DEFAULT
 	else if (lua_type(L, 1) == LUA_TNUMBER) // skin number
@@ -1144,9 +1148,21 @@ static int libd_getColormap(lua_State *L)
 			skinnum = i;
 	}
 
+	if (!lua_isnoneornil(L, 3))
+	{
+		const char *translation_name = luaL_checkstring(L, 3);
+		translation_id = R_FindCustomTranslation(translation_name);
+		if (translation_id == -1)
+			return luaL_error(L, "invalid translation '%s'.", translation_name);
+	}
+
 	// all was successful above, now we generate the colormap at last!
+	if (translation_id != -1)
+		colormap = R_GetTranslationRemap(translation_id, color, skinnum);
+
+	if (colormap == NULL)
+		colormap = R_GetTranslationColormap(skinnum, color, GTC_CACHE);
 
-	colormap = R_GetTranslationColormap(skinnum, color, GTC_CACHE);
 	LUA_PushUserdata(L, colormap, META_COLORMAP); // push as META_COLORMAP userdata, specifically for patches to use!
 	return 1;
 }
diff --git a/src/lua_infolib.c b/src/lua_infolib.c
index 3facec82b82da21c92d5041aba81fb1f9f543deb..b2730c5931f0c00db2eca2848827526661631084 100644
--- a/src/lua_infolib.c
+++ b/src/lua_infolib.c
@@ -1900,32 +1900,6 @@ static int colorramp_len(lua_State *L)
 	return 1;
 }
 
-//////////////////////
-// TRANSLATION INFO //
-//////////////////////
-
-// Arbitrary translations[] table index -> colormap_t *
-static int lib_getTranslation(lua_State *L)
-{
-	lua_remove(L, 1);
-
-	const char *name = luaL_checkstring(L, 1);
-	remaptable_t *tr = R_GetTranslationByID(R_FindCustomTranslation(name));
-	if (tr)
-		LUA_PushUserdata(L, &tr->remap, META_COLORMAP);
-	else
-		lua_pushnil(L);
-
-	return 1;
-}
-
-// #translations -> R_NumCustomTranslations()
-static int lib_translationslen(lua_State *L)
-{
-	lua_pushinteger(L, R_NumCustomTranslations());
-	return 1;
-}
-
 //////////////////////////////
 //
 // Now push all these functions into the Lua state!
@@ -1958,7 +1932,6 @@ int LUA_InfoLib(lua_State *L)
 	LUA_RegisterGlobalUserdata(L, "spr2defaults", lib_getSpr2default, lib_setSpr2default, lib_spr2namelen);
 	LUA_RegisterGlobalUserdata(L, "states", lib_getState, lib_setState, lib_statelen);
 	LUA_RegisterGlobalUserdata(L, "mobjinfo", lib_getMobjInfo, lib_setMobjInfo, lib_mobjinfolen);
-	LUA_RegisterGlobalUserdata(L, "translations", lib_getTranslation, NULL, lib_translationslen);
 	LUA_RegisterGlobalUserdata(L, "skincolors", lib_getSkinColor, lib_setSkinColor, lib_skincolorslen);
 	LUA_RegisterGlobalUserdata(L, "spriteinfo", lib_getSpriteInfo, lib_setSpriteInfo, lib_spriteinfolen);
 	LUA_RegisterGlobalUserdata(L, "sfxinfo", lib_getSfxInfo, lib_setSfxInfo, lib_sfxlen);
diff --git a/src/m_misc.c b/src/m_misc.c
index 5815d17c54619f46afe6fc29c1277a3621ecc77b..1b6a90c50acd6230cb9d8c56f99df7178926c777 100644
--- a/src/m_misc.c
+++ b/src/m_misc.c
@@ -2259,6 +2259,44 @@ boolean M_IsStringEmpty(const char *s)
 	return true;
 }
 
+// Converts a string containing a whole number into an int. Returns false if the conversion failed.
+boolean M_StringToNumber(const char *input, int *out)
+{
+	char *end_position = NULL;
+
+	errno = 0;
+
+	int result = strtol(input, &end_position, 10);
+	if (end_position == input || *end_position != '\0')
+		return false;
+
+	if (errno == ERANGE)
+		return false;
+
+	*out = result;
+
+	return true;
+}
+
+// Converts a string containing a number into a double. Returns false if the conversion failed.
+boolean M_StringToDecimal(const char *input, double *out)
+{
+	char *end_position = NULL;
+
+	errno = 0;
+
+	double result = strtod(input, &end_position);
+	if (end_position == input || *end_position != '\0')
+		return false;
+
+	if (errno == ERANGE)
+		return false;
+
+	*out = result;
+
+	return true;
+}
+
 // Rounds off floating numbers and checks for 0 - 255 bounds
 int M_RoundUp(double number)
 {
diff --git a/src/m_misc.h b/src/m_misc.h
index 753991e70465c075fa874ce7662d6aa6603e7b6a..04ac66ca65e1ff92d44172b12e1186cdaa04b648 100644
--- a/src/m_misc.h
+++ b/src/m_misc.h
@@ -109,6 +109,12 @@ const char * M_Ftrim (double);
 // Returns true if the string is empty.
 boolean M_IsStringEmpty(const char *s);
 
+// Converts a string containing a whole number into an int. Returns false if the conversion failed.
+boolean M_StringToNumber(const char *input, int *out);
+
+// Converts a string containing a number into a double. Returns false if the conversion failed.
+boolean M_StringToDecimal(const char *input, double *out);
+
 // counting bits, for weapon ammo code, usually
 FUNCMATH UINT8 M_CountBits(UINT32 num, UINT8 size);
 
diff --git a/src/m_tokenizer.c b/src/m_tokenizer.c
index 26275881d3f81fe8db6c22d2e35226839c75af56..f36f7f6f323133c51c4975992beea9919c27e4bd 100644
--- a/src/m_tokenizer.c
+++ b/src/m_tokenizer.c
@@ -21,6 +21,7 @@ tokenizer_t *Tokenizer_Open(const char *inputString, unsigned numTokens)
 	tokenizer->endPos = 0;
 	tokenizer->inputLength = 0;
 	tokenizer->inComment = 0;
+	tokenizer->inString = 0;
 	tokenizer->get = Tokenizer_Read;
 
 	if (numTokens < 1)
@@ -53,7 +54,18 @@ void Tokenizer_Close(tokenizer_t *tokenizer)
 	Z_Free(tokenizer);
 }
 
-static void Tokenizer_DetectComment(tokenizer_t *tokenizer, UINT32 *pos)
+static boolean DetectLineBreak(tokenizer_t *tokenizer, size_t pos)
+{
+	if (tokenizer->input[pos] == '\n')
+	{
+		tokenizer->line++;
+		return true;
+	}
+
+	return false;
+}
+
+static void DetectComment(tokenizer_t *tokenizer, UINT32 *pos)
 {
 	if (tokenizer->inComment)
 		return;
@@ -94,8 +106,31 @@ const char *Tokenizer_Read(tokenizer_t *tokenizer, UINT32 i)
 
 	tokenizer->startPos = tokenizer->endPos;
 
+	// If in a string, return the entire string within quotes, except without the quotes.
+	if (tokenizer->inString == 1)
+	{
+		while (tokenizer->input[tokenizer->endPos] != '"' && tokenizer->endPos < tokenizer->inputLength)
+		{
+			DetectLineBreak(tokenizer, tokenizer->endPos);
+			tokenizer->endPos++;
+		}
+
+		Tokenizer_ReadTokenString(tokenizer, i);
+		tokenizer->inString = 2;
+		return tokenizer->token[i];
+	}
+	// If just ended a string, return only a quotation mark.
+	else if (tokenizer->inString == 2)
+	{
+		tokenizer->endPos = tokenizer->startPos + 1;
+		tokenizer->token[i][0] = tokenizer->input[tokenizer->startPos];
+		tokenizer->token[i][1] = '\0';
+		tokenizer->inString = 0;
+		return tokenizer->token[i];
+	}
+
 	// Try to detect comments now, in case we're pointing right at one
-	Tokenizer_DetectComment(tokenizer, &tokenizer->startPos);
+	DetectComment(tokenizer, &tokenizer->startPos);
 
 	// Find the first non-whitespace char, or else the end of the string trying
 	while ((tokenizer->input[tokenizer->startPos] == ' '
@@ -106,8 +141,10 @@ const char *Tokenizer_Read(tokenizer_t *tokenizer, UINT32 i)
 			|| tokenizer->inComment != 0)
 			&& tokenizer->startPos < tokenizer->inputLength)
 	{
+		boolean inLineBreak = DetectLineBreak(tokenizer, tokenizer->startPos);
+
 		// Try to detect comment endings now
-		if (tokenizer->inComment == 1	&& tokenizer->input[tokenizer->startPos] == '\n')
+		if (tokenizer->inComment == 1 && inLineBreak)
 			tokenizer->inComment = 0; // End of line for a single-line comment
 		else if (tokenizer->inComment == 2
 			&& tokenizer->startPos < tokenizer->inputLength - 1
@@ -120,11 +157,12 @@ const char *Tokenizer_Read(tokenizer_t *tokenizer, UINT32 i)
 		}
 
 		tokenizer->startPos++;
-		Tokenizer_DetectComment(tokenizer, &tokenizer->startPos);
+		DetectComment(tokenizer, &tokenizer->startPos);
 	}
 
 	// If the end of the string is reached, no token is to be read
-	if (tokenizer->startPos == tokenizer->inputLength) {
+	if (tokenizer->startPos == tokenizer->inputLength)
+	{
 		tokenizer->endPos = tokenizer->inputLength;
 		return NULL;
 	}
@@ -136,22 +174,16 @@ const char *Tokenizer_Read(tokenizer_t *tokenizer, UINT32 i)
 			|| tokenizer->input[tokenizer->startPos] == ']'
 			|| tokenizer->input[tokenizer->startPos] == '='
 			|| tokenizer->input[tokenizer->startPos] == ':'
-			|| tokenizer->input[tokenizer->startPos] == '%')
+			|| tokenizer->input[tokenizer->startPos] == '%'
+			|| tokenizer->input[tokenizer->startPos] == '@'
+			|| tokenizer->input[tokenizer->startPos] == '"')
 	{
 		tokenizer->endPos = tokenizer->startPos + 1;
 		tokenizer->token[i][0] = tokenizer->input[tokenizer->startPos];
 		tokenizer->token[i][1] = '\0';
-		return tokenizer->token[i];
-	}
-	// Return entire string within quotes, except without the quotes.
-	else if (tokenizer->input[tokenizer->startPos] == '"')
-	{
-		tokenizer->endPos = ++tokenizer->startPos;
-		while (tokenizer->input[tokenizer->endPos] != '"' && tokenizer->endPos < tokenizer->inputLength)
-			tokenizer->endPos++;
+		if (tokenizer->input[tokenizer->startPos] == '"')
+			tokenizer->inString = 1;
 
-		Tokenizer_ReadTokenString(tokenizer, i);
-		tokenizer->endPos++;
 		return tokenizer->token[i];
 	}
 
@@ -169,12 +201,14 @@ const char *Tokenizer_Read(tokenizer_t *tokenizer, UINT32 i)
 			&& tokenizer->input[tokenizer->endPos] != '='
 			&& tokenizer->input[tokenizer->endPos] != ':'
 			&& tokenizer->input[tokenizer->endPos] != '%'
+			&& tokenizer->input[tokenizer->endPos] != '@'
+			&& tokenizer->input[tokenizer->endPos] != ';'
 			&& tokenizer->inComment == 0)
 			&& tokenizer->endPos < tokenizer->inputLength)
 	{
 		tokenizer->endPos++;
 		// Try to detect comment starts now; if it's in a comment, we don't want it in this token
-		Tokenizer_DetectComment(tokenizer, &tokenizer->endPos);
+		DetectComment(tokenizer, &tokenizer->endPos);
 	}
 
 	Tokenizer_ReadTokenString(tokenizer, i);
@@ -189,7 +223,7 @@ const char *Tokenizer_SRB2Read(tokenizer_t *tokenizer, UINT32 i)
 	tokenizer->startPos = tokenizer->endPos;
 
 	// Try to detect comments now, in case we're pointing right at one
-	Tokenizer_DetectComment(tokenizer, &tokenizer->startPos);
+	DetectComment(tokenizer, &tokenizer->startPos);
 
 	// Find the first non-whitespace char, or else the end of the string trying
 	while ((tokenizer->input[tokenizer->startPos] == ' '
@@ -201,8 +235,10 @@ const char *Tokenizer_SRB2Read(tokenizer_t *tokenizer, UINT32 i)
 			|| tokenizer->inComment != 0)
 			&& tokenizer->startPos < tokenizer->inputLength)
 	{
+		boolean inLineBreak = DetectLineBreak(tokenizer, tokenizer->startPos);
+
 		// Try to detect comment endings now
-		if (tokenizer->inComment == 1	&& tokenizer->input[tokenizer->startPos] == '\n')
+		if (tokenizer->inComment == 1 && inLineBreak)
 			tokenizer->inComment = 0; // End of line for a single-line comment
 		else if (tokenizer->inComment == 2
 			&& tokenizer->startPos < tokenizer->inputLength - 1
@@ -215,7 +251,7 @@ const char *Tokenizer_SRB2Read(tokenizer_t *tokenizer, UINT32 i)
 		}
 
 		tokenizer->startPos++;
-		Tokenizer_DetectComment(tokenizer, &tokenizer->startPos);
+		DetectComment(tokenizer, &tokenizer->startPos);
 	}
 
 	// If the end of the string is reached, no token is to be read
@@ -238,7 +274,10 @@ const char *Tokenizer_SRB2Read(tokenizer_t *tokenizer, UINT32 i)
 	{
 		tokenizer->endPos = ++tokenizer->startPos;
 		while (tokenizer->input[tokenizer->endPos] != '"' && tokenizer->endPos < tokenizer->inputLength)
+		{
+			DetectLineBreak(tokenizer, tokenizer->endPos);
 			tokenizer->endPos++;
+		}
 
 		Tokenizer_ReadTokenString(tokenizer, i);
 		tokenizer->endPos++;
@@ -260,7 +299,7 @@ const char *Tokenizer_SRB2Read(tokenizer_t *tokenizer, UINT32 i)
 	{
 		tokenizer->endPos++;
 		// Try to detect comment starts now; if it's in a comment, we don't want it in this token
-		Tokenizer_DetectComment(tokenizer, &tokenizer->endPos);
+		DetectComment(tokenizer, &tokenizer->endPos);
 	}
 
 	Tokenizer_ReadTokenString(tokenizer, i);
diff --git a/src/m_tokenizer.h b/src/m_tokenizer.h
index 88cb2a566907d27b0d70508fb20c9ab1dff84fbb..f5111730194915c69fea455a53ba1da3cf66068b 100644
--- a/src/m_tokenizer.h
+++ b/src/m_tokenizer.h
@@ -24,6 +24,8 @@ typedef struct Tokenizer
 	UINT32 endPos;
 	UINT32 inputLength;
 	UINT8 inComment; // 0 = not in comment, 1 = // Single-line, 2 = /* Multi-line */
+	UINT8 inString; // 0 = not in string, 1 = in string, 2 = just left string
+	int line;
 	const char *(*get)(struct Tokenizer*, UINT32);
 } tokenizer_t;
 
diff --git a/src/r_defs.h b/src/r_defs.h
index 39e6765cd7af35d6478e690de8c48045d063b505..16c660b01938ed86669550f709f7b65336cfaebc 100644
--- a/src/r_defs.h
+++ b/src/r_defs.h
@@ -26,9 +26,6 @@
 
 #include "taglist.h"
 
-// Amount of colors in the palette
-#define NUM_PALETTE_ENTRIES 256
-
 //
 // ClipWallSegment
 // Clips the given range of columns
diff --git a/src/r_draw.c b/src/r_draw.c
index b87a8404ebc3ea5895425dd81b1c78dc6040fdfa..ff2e43df31d9291ed6ae07facf2da0c1beaaa4e1 100644
--- a/src/r_draw.c
+++ b/src/r_draw.c
@@ -125,49 +125,37 @@ UINT32 nflatxshift, nflatyshift, nflatshiftup, nflatmask;
 //                       TRANSLATION COLORMAP CODE
 // =========================================================================
 
-enum
-{
-	DEFAULT_TT_CACHE_INDEX,
-	BOSS_TT_CACHE_INDEX,
-	METALSONIC_TT_CACHE_INDEX,
-	ALLWHITE_TT_CACHE_INDEX,
-	RAINBOW_TT_CACHE_INDEX,
-	BLINK_TT_CACHE_INDEX,
-	DASHMODE_TT_CACHE_INDEX,
-
-	TT_CACHE_SIZE
-};
-
-static UINT8 **translationtablecache[TT_CACHE_SIZE] = {NULL};
-static UINT8 **skintranslationcache[NUM_PALETTE_ENTRIES] = {NULL};
+static colorcache_t **translationtablecache[TT_CACHE_SIZE] = {NULL};
 
 boolean skincolor_modified[MAXSKINCOLORS];
 
-static INT32 TranslationToCacheIndex(INT32 translation)
+static INT32 SkinToCacheIndex(INT32 translation)
 {
 	switch (translation)
 	{
+		case TC_DEFAULT:    return DEFAULT_TT_CACHE_INDEX;
 		case TC_BOSS:       return BOSS_TT_CACHE_INDEX;
 		case TC_METALSONIC: return METALSONIC_TT_CACHE_INDEX;
 		case TC_ALLWHITE:   return ALLWHITE_TT_CACHE_INDEX;
 		case TC_RAINBOW:    return RAINBOW_TT_CACHE_INDEX;
 		case TC_BLINK:      return BLINK_TT_CACHE_INDEX;
 		case TC_DASHMODE:   return DASHMODE_TT_CACHE_INDEX;
-		default:            return DEFAULT_TT_CACHE_INDEX;
+		default:            return translation;
 	}
 }
 
-static INT32 CacheIndexToTranslation(INT32 index)
+static INT32 CacheIndexToSkin(INT32 index)
 {
 	switch (index)
 	{
+		case DEFAULT_TT_CACHE_INDEX:    return TC_DEFAULT;
 		case BOSS_TT_CACHE_INDEX:       return TC_BOSS;
 		case METALSONIC_TT_CACHE_INDEX: return TC_METALSONIC;
 		case ALLWHITE_TT_CACHE_INDEX:   return TC_ALLWHITE;
 		case RAINBOW_TT_CACHE_INDEX:    return TC_RAINBOW;
 		case BLINK_TT_CACHE_INDEX:      return TC_BLINK;
 		case DASHMODE_TT_CACHE_INDEX:   return TC_DASHMODE;
-		default:                        return TC_DEFAULT;
+		default:                        return index;
 	}
 }
 
@@ -553,23 +541,22 @@ static void R_GenerateTranslationColormap(UINT8 *dest_colormap, INT32 translatio
 */
 UINT8* R_GetTranslationColormap(INT32 skinnum, skincolornum_t color, UINT8 flags)
 {
-	UINT8 ***cache = NULL;
-	INT32 index, starttranscolor;
-	UINT8 *ret;
+	colorcache_t *ret;
+	INT32 index = 0;
+	INT32 starttranscolor = DEFAULT_STARTTRANSCOLOR;
 
 	// Adjust if we want the default colormap
 	if (skinnum >= numskins)
 		I_Error("Invalid skin number %d", skinnum);
 	else if (skinnum >= 0)
 	{
-		cache = skintranslationcache;
-		starttranscolor = index = skins[skinnum]->starttranscolor;
+		index = skins[skinnum]->skinnum;
+		starttranscolor = skins[skinnum]->starttranscolor;
 	}
 	else if (skinnum <= TC_DEFAULT)
 	{
-		cache = translationtablecache;
-		starttranscolor = DEFAULT_STARTTRANSCOLOR;
-		index = TranslationToCacheIndex(skinnum);
+		// Do default translation
+		index = SkinToCacheIndex(skinnum);
 	}
 	else
 		I_Error("Invalid translation %d", skinnum);
@@ -577,41 +564,48 @@ UINT8* R_GetTranslationColormap(INT32 skinnum, skincolornum_t color, UINT8 flags
 	if (flags & GTC_CACHE)
 	{
 		// Allocate table for skin if necessary
-		if (!cache[index])
-			cache[index] = Z_Calloc(MAXSKINCOLORS * sizeof(UINT8**), PU_STATIC, NULL);
+		if (!translationtablecache[index])
+			translationtablecache[index] = Z_Calloc(MAXSKINCOLORS * sizeof(colorcache_t**), PU_STATIC, NULL);
 
 		// Get colormap
-		ret = cache[index][color];
+		ret = translationtablecache[index][color];
 
 		// Rebuild the cache if necessary
 		if (skincolor_modified[color])
 		{
-			INT32 i;
-
-			for (i = 0; i < TT_CACHE_SIZE; i++)
-				if (translationtablecache[i] && translationtablecache[i][color])
-					R_GenerateTranslationColormap(translationtablecache[i][color], CacheIndexToTranslation(i), color, starttranscolor);
-			for (i = 0; i < NUM_PALETTE_ENTRIES; i++)
-				if (skintranslationcache[i] && skintranslationcache[i][color])
-					R_GenerateTranslationColormap(skintranslationcache[i][color], 0, color, i);
-
+			// Moved up here so that R_UpdateTranslationRemaps doesn't cause a stack overflow,
+			// since in this situation, it will call R_GetTranslationColormap
 			skincolor_modified[color] = false;
+
+			for (unsigned i = 0; i < TT_CACHE_SIZE; i++)
+			{
+				if (translationtablecache[i])
+				{
+					colorcache_t *cache = translationtablecache[i][color];
+					if (cache)
+					{
+						R_GenerateTranslationColormap(cache->colors, CacheIndexToSkin(i), color, starttranscolor);
+						R_UpdateTranslationRemaps(color, i);
+					}
+				}
+			}
 		}
 	}
-	else ret = NULL;
+	else
+		ret = NULL;
 
 	// Generate the colormap if necessary
 	if (!ret)
 	{
-		ret = Z_MallocAlign(NUM_PALETTE_ENTRIES, (flags & GTC_CACHE) ? PU_LEVEL : PU_STATIC, NULL, 8);
-		R_GenerateTranslationColormap(ret, skinnum, color, starttranscolor);
+		ret = Z_Malloc(sizeof(colorcache_t), (flags & GTC_CACHE) ? PU_LEVEL : PU_STATIC, NULL);
+		R_GenerateTranslationColormap(ret->colors, skinnum, color, starttranscolor);
 
 		// Cache the colormap if desired
 		if (flags & GTC_CACHE)
-			cache[index][color] = ret;
+			translationtablecache[index][color] = ret;
 	}
 
-	return ret;
+	return ret->colors;
 }
 
 /**	\brief	Flushes cache of translation colormaps.
@@ -629,9 +623,6 @@ void R_FlushTranslationColormapCache(void)
 	for (i = 0; i < TT_CACHE_SIZE; i++)
 		if (translationtablecache[i])
 			memset(translationtablecache[i], 0, MAXSKINCOLORS * sizeof(UINT8**));
-	for (i = 0; i < NUM_PALETTE_ENTRIES; i++)
-		if (skintranslationcache[i])
-			memset(skintranslationcache[i], 0, MAXSKINCOLORS * sizeof(UINT8**));
 }
 
 UINT16 R_GetColorByName(const char *name)
diff --git a/src/r_draw.h b/src/r_draw.h
index 9cde3cf54f4421233ba517996ceee173067a42fe..29370015a1c46cb6405bf5b698d6c23425ee79e2 100644
--- a/src/r_draw.h
+++ b/src/r_draw.h
@@ -117,6 +117,27 @@ enum
 	TC_DEFAULT
 };
 
+// Amount of colors in the palette
+#define NUM_PALETTE_ENTRIES 256
+
+typedef struct colorcache_s
+{
+	UINT8 colors[NUM_PALETTE_ENTRIES];
+} colorcache_t;
+
+enum
+{
+	DEFAULT_TT_CACHE_INDEX = MAXSKINS,
+	BOSS_TT_CACHE_INDEX,
+	METALSONIC_TT_CACHE_INDEX,
+	ALLWHITE_TT_CACHE_INDEX,
+	RAINBOW_TT_CACHE_INDEX,
+	BLINK_TT_CACHE_INDEX,
+	DASHMODE_TT_CACHE_INDEX,
+
+	TT_CACHE_SIZE
+};
+
 // Custom player skin translation
 // Initialize color translation tables, for player rendering etc.
 UINT8* R_GetTranslationColormap(INT32 skinnum, skincolornum_t color, UINT8 flags);
diff --git a/src/r_things.c b/src/r_things.c
index 628d8f490c59f2e24d21c620ca3631657300de98..7291594eb92901246bd547442d3b8d963cc9acf8 100644
--- a/src/r_things.c
+++ b/src/r_things.c
@@ -764,6 +764,12 @@ void R_DrawFlippedMaskedColumn(column_t *column)
 
 UINT8 *R_GetTranslationForThing(mobj_t *mobj, skincolornum_t color, UINT16 translation)
 {
+	INT32 skinnum = TC_DEFAULT;
+
+	boolean is_player = mobj->skin && mobj->sprite == SPR_PLAY;
+	if (is_player) // This thing is a player!
+		skinnum = ((skin_t*)mobj->skin)->skinnum;
+
 	if (R_ThingIsFlashing(mobj)) // Bosses "flash"
 	{
 		if (mobj->type == MT_CYBRAKDEMON || mobj->colorized)
@@ -775,9 +781,9 @@ UINT8 *R_GetTranslationForThing(mobj_t *mobj, skincolornum_t color, UINT16 trans
 	}
 	else if (translation != 0)
 	{
-		remaptable_t *tr = R_GetTranslationByID(translation);
+		UINT8 *tr = R_GetTranslationRemap(translation, color, skinnum);
 		if (tr != NULL)
-			return tr->remap;
+			return tr;
 	}
 	else if (color != SKINCOLOR_NONE)
 	{
@@ -793,13 +799,8 @@ UINT8 *R_GetTranslationForThing(mobj_t *mobj, skincolornum_t color, UINT16 trans
 			else
 				return R_GetTranslationColormap(TC_RAINBOW, color, GTC_CACHE);
 		}
-		else if (mobj->skin && mobj->sprite == SPR_PLAY) // This thing is a player!
-		{
-			UINT8 skinnum = ((skin_t*)mobj->skin)->skinnum;
+		else
 			return R_GetTranslationColormap(skinnum, color, GTC_CACHE);
-		}
-		else // Use the defaults
-			return R_GetTranslationColormap(TC_DEFAULT, color, GTC_CACHE);
 	}
 	else if (mobj->sprite == SPR_PLAY) // Looks like a player, but doesn't have a color? Get rid of green sonic syndrome.
 		return R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_BLUE, GTC_CACHE);
diff --git a/src/r_translation.c b/src/r_translation.c
index a4df3cde0c9bdc04702f963e62b3703ca6f42c58..7e1e30d0cdec7afd2d810f6b8d1a6b26da0c42bb 100644
--- a/src/r_translation.c
+++ b/src/r_translation.c
@@ -17,6 +17,7 @@
 #include "z_zone.h"
 #include "w_wad.h"
 #include "m_tokenizer.h"
+#include "m_misc.h"
 
 #include <errno.h>
 
@@ -26,57 +27,39 @@ static unsigned numpaletteremaps = 0;
 static int allWhiteRemap = 0;
 static int dashModeRemap = 0;
 
+static void MakeGrayscaleRemap(void);
+static void MakeInvertRemap(void);
 static void MakeDashModeRemap(void);
 
-static boolean PaletteRemap_AddIndexRange(remaptable_t *tr, int start, int end, int pal1, int pal2);
-static boolean PaletteRemap_AddColorRange(remaptable_t *tr, int start, int end, int r1i, int g1i, int b1i, int r2i, int g2i, int b2i);
-static boolean PaletteRemap_AddDesaturation(remaptable_t *tr, int start, int end, double r1, double g1, double b1, double r2, double g2, double b2);
-static boolean PaletteRemap_AddColourisation(remaptable_t *tr, int start, int end, int r, int g, int b);
-static boolean PaletteRemap_AddTint(remaptable_t *tr, int start, int end, int r, int g, int b, int amount);
-static boolean PaletteRemap_AddInvert(remaptable_t *tr, int start, int end);
+static boolean PaletteRemap_DoIndexRange(UINT8 *remap, int start, int end, int pal1, int pal2);
+static boolean PaletteRemap_DoColorRange(UINT8 *remap, int start, int end, int r1i, int g1i, int b1i, int r2i, int g2i, int b2i);
+static boolean PaletteRemap_DoDesaturation(UINT8 *remap, int start, int end, double r1, double g1, double b1, double r2, double g2, double b2);
+static boolean PaletteRemap_DoColourisation(UINT8 *remap, int start, int end, int r, int g, int b);
+static boolean PaletteRemap_DoTint(UINT8 *remap, int start, int end, int r, int g, int b, int amount);
+static boolean PaletteRemap_DoInvert(UINT8 *remap, int start, int end);
 
-enum PaletteRemapType
+static void PaletteRemap_Apply(UINT8 *remap, paletteremap_t *data);
+
+static void InitSource(paletteremap_t *source, paletteremaptype_t type, int start, int end)
 {
-	REMAP_ADD_INDEXRANGE,
-	REMAP_ADD_COLORRANGE,
-	REMAP_ADD_COLOURISATION,
-	REMAP_ADD_DESATURATION,
-	REMAP_ADD_TINT
-};
+	source->type = type;
+	source->start = start;
+	source->end = end;
+}
 
-struct PaletteRemapParseResult
+static paletteremap_t *AddSource(remaptable_t *tr, paletteremaptype_t type, int start, int end)
 {
-	int start, end;
-	enum PaletteRemapType type;
-	union
-	{
-		struct
-		{
-			int pal1, pal2;
-		} indexRange;
-		struct
-		{
-			int r1, g1, b1;
-			int r2, g2, b2;
-		} colorRange;
-		struct
-		{
-			double r1, g1, b1;
-			double r2, g2, b2;
-		} desaturation;
-		struct
-		{
-			int r, g, b;
-		} colourisation;
-		struct
-		{
-			int r, g, b, amount;
-		} tint;
-	};
+	paletteremap_t *remap = NULL;
 
-	boolean has_error;
-	char error[4096];
-};
+	tr->num_sources++;
+	tr->sources = Z_Realloc(tr->sources, tr->num_sources * sizeof(paletteremap_t), PU_STATIC, NULL);
+
+	remap = &tr->sources[tr->num_sources - 1];
+
+	InitSource(remap, type, start, end);
+
+	return remap;
+}
 
 void PaletteRemap_Init(void)
 {
@@ -86,10 +69,7 @@ void PaletteRemap_Init(void)
 	PaletteRemap_Add(base);
 
 	// Grayscale translation
-	remaptable_t *grayscale = PaletteRemap_New();
-	PaletteRemap_SetIdentity(grayscale);
-	PaletteRemap_AddDesaturation(grayscale, 0, 255, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0);
-	R_AddCustomTranslation("Grayscale", PaletteRemap_Add(grayscale));
+	MakeGrayscaleRemap();
 
 	// All white (TC_ALLWHITE)
 	remaptable_t *allWhite = PaletteRemap_New();
@@ -103,10 +83,7 @@ void PaletteRemap_Init(void)
 	R_AddCustomTranslation("AllBlack", PaletteRemap_Add(allBlack));
 
 	// Invert
-	remaptable_t *invertRemap = PaletteRemap_New();
-	PaletteRemap_SetIdentity(invertRemap);
-	PaletteRemap_AddInvert(invertRemap, 0, 255);
-	R_AddCustomTranslation("Invert", PaletteRemap_Add(invertRemap));
+	MakeInvertRemap();
 
 	// Dash mode (TC_DASHMODE)
 	MakeDashModeRemap();
@@ -155,14 +132,6 @@ boolean PaletteRemap_IsIdentity(remaptable_t *tr)
 
 unsigned PaletteRemap_Add(remaptable_t *tr)
 {
-#if 0
-	for (unsigned i = 0; i < numpaletteremaps; i++)
-	{
-		if (PaletteRemap_Equal(tr, paletteremaps[i]))
-			return i;
-	}
-#endif
-
 	numpaletteremaps++;
 	paletteremaps = Z_Realloc(paletteremaps, sizeof(remaptable_t *) * numpaletteremaps, PU_STATIC, NULL);
 	paletteremaps[numpaletteremaps - 1] = tr;
@@ -170,6 +139,36 @@ unsigned PaletteRemap_Add(remaptable_t *tr)
 	return numpaletteremaps - 1;
 }
 
+static void MakeGrayscaleRemap(void)
+{
+	remaptable_t *grayscale = PaletteRemap_New();
+
+	paletteremap_t *source = AddSource(grayscale, REMAP_ADD_DESATURATION, 0, 255);
+	source->desaturation.r1 = 0.0;
+	source->desaturation.g1 = 0.0;
+	source->desaturation.b1 = 0.0;
+	source->desaturation.r2 = 1.0;
+	source->desaturation.g2 = 1.0;
+	source->desaturation.b2 = 1.0;
+
+	PaletteRemap_SetIdentity(grayscale);
+	PaletteRemap_Apply(grayscale->remap, source);
+
+	R_AddCustomTranslation("Grayscale", PaletteRemap_Add(grayscale));
+}
+
+static void MakeInvertRemap(void)
+{
+	remaptable_t *invertRemap = PaletteRemap_New();
+
+	paletteremap_t *source = AddSource(invertRemap, REMAP_ADD_INVERT, 0, 255);
+
+	PaletteRemap_SetIdentity(invertRemap);
+	PaletteRemap_Apply(invertRemap->remap, source);
+
+	R_AddCustomTranslation("Invert", PaletteRemap_Add(invertRemap));
+}
+
 // This is a long one, because MotorRoach basically hand-picked the indices
 static void MakeDashModeRemap(void)
 {
@@ -237,7 +236,7 @@ static boolean IndicesOutOfRange2(int start1, int end1, int start2, int end2)
 	b = swap; \
 }
 
-static boolean PaletteRemap_AddIndexRange(remaptable_t *tr, int start, int end, int pal1, int pal2)
+static boolean PaletteRemap_DoIndexRange(UINT8 *remap, int start, int end, int pal1, int pal2)
 {
 	if (IndicesOutOfRange2(start, end, pal1, pal2))
 		return false;
@@ -249,7 +248,7 @@ static boolean PaletteRemap_AddIndexRange(remaptable_t *tr, int start, int end,
 	}
 	else if (start == end)
 	{
-		tr->remap[start] = pal1;
+		remap[start] = pal1;
 		return true;
 	}
 
@@ -259,13 +258,13 @@ static boolean PaletteRemap_AddIndexRange(remaptable_t *tr, int start, int end,
 	for (int i = start; i <= end; palcol += palstep, ++i)
 	{
 		double idx = round(palcol);
-		tr->remap[i] = (int)idx;
+		remap[i] = (int)idx;
 	}
 
 	return true;
 }
 
-static boolean PaletteRemap_AddColorRange(remaptable_t *tr, int start, int end, int r1i, int g1i, int b1i, int r2i, int g2i, int b2i)
+static boolean PaletteRemap_DoColorRange(UINT8 *remap, int start, int end, int r1i, int g1i, int b1i, int r2i, int g2i, int b2i)
 {
 	if (IndicesOutOfRange(start, end))
 		return false;
@@ -302,7 +301,7 @@ static boolean PaletteRemap_AddColorRange(remaptable_t *tr, int start, int end,
 
 	if (start == end)
 	{
-		tr->remap[start] = NearestColor(r, g, b);
+		remap[start] = NearestColor(r, g, b);
 	}
 	else
 	{
@@ -312,7 +311,7 @@ static boolean PaletteRemap_AddColorRange(remaptable_t *tr, int start, int end,
 
 		for (int i = start; i <= end; ++i)
 		{
-			tr->remap[i] = NearestColor(r, g, b);
+			remap[i] = NearestColor(r, g, b);
 			r += rs;
 			g += gs;
 			b += bs;
@@ -324,7 +323,7 @@ static boolean PaletteRemap_AddColorRange(remaptable_t *tr, int start, int end,
 
 #define CLAMP(val, minval, maxval) max(min(val, maxval), minval)
 
-static boolean PaletteRemap_AddDesaturation(remaptable_t *tr, int start, int end, double r1, double g1, double b1, double r2, double g2, double b2)
+static boolean PaletteRemap_DoDesaturation(UINT8 *remap, int start, int end, double r1, double g1, double b1, double r2, double g2, double b2)
 {
 	if (IndicesOutOfRange(start, end))
 		return false;
@@ -353,9 +352,11 @@ static boolean PaletteRemap_AddDesaturation(remaptable_t *tr, int start, int end
 
 	for (int c = start; c <= end; c++)
 	{
-		double intensity = (pMasterPalette[c].s.red * 77 + pMasterPalette[c].s.green * 143 + pMasterPalette[c].s.blue * 37) / 255.0;
+		double intensity = (pMasterPalette[remap[c]].s.red * 77
+			+ pMasterPalette[remap[c]].s.green * 143
+			+ pMasterPalette[remap[c]].s.blue * 37) / 255.0;
 
-		tr->remap[c] = NearestColor(
+		remap[c] = NearestColor(
 		    min(255, max(0, (int)(r1 + intensity*r2))),
 		    min(255, max(0, (int)(g1 + intensity*g2))),
 		    min(255, max(0, (int)(b1 + intensity*b2)))
@@ -369,16 +370,16 @@ static boolean PaletteRemap_AddDesaturation(remaptable_t *tr, int start, int end
 
 #undef SWAP
 
-static boolean PaletteRemap_AddColourisation(remaptable_t *tr, int start, int end, int r, int g, int b)
+static boolean PaletteRemap_DoColourisation(UINT8 *remap, int start, int end, int r, int g, int b)
 {
 	if (IndicesOutOfRange(start, end))
 		return false;
 
 	for (int i = start; i < end; ++i)
 	{
-		double br = pMasterPalette[i].s.red;
-		double bg = pMasterPalette[i].s.green;
-		double bb = pMasterPalette[i].s.blue;
+		double br = pMasterPalette[remap[i]].s.red;
+		double bg = pMasterPalette[remap[i]].s.green;
+		double bb = pMasterPalette[remap[i]].s.blue;
 		double grey = (br * 0.299 + bg * 0.587 + bb * 0.114) / 255.0f;
 		if (grey > 1.0)
 			grey = 1.0;
@@ -387,7 +388,7 @@ static boolean PaletteRemap_AddColourisation(remaptable_t *tr, int start, int en
 		bg = g * grey;
 		bb = b * grey;
 
-		tr->remap[i] = NearestColor(
+		remap[i] = NearestColor(
 		    (int)br,
 		    (int)bg,
 		    (int)bb
@@ -397,16 +398,16 @@ static boolean PaletteRemap_AddColourisation(remaptable_t *tr, int start, int en
 	return true;
 }
 
-static boolean PaletteRemap_AddTint(remaptable_t *tr, int start, int end, int r, int g, int b, int amount)
+static boolean PaletteRemap_DoTint(UINT8 *remap, int start, int end, int r, int g, int b, int amount)
 {
 	if (IndicesOutOfRange(start, end))
 		return false;
 
 	for (int i = start; i < end; ++i)
 	{
-		float br = pMasterPalette[i].s.red;
-		float bg = pMasterPalette[i].s.green;
-		float bb = pMasterPalette[i].s.blue;
+		float br = pMasterPalette[remap[i]].s.red;
+		float bg = pMasterPalette[remap[i]].s.green;
+		float bb = pMasterPalette[remap[i]].s.blue;
 		float a = amount * 0.01f;
 		float ia = 1.0f - a;
 
@@ -414,7 +415,7 @@ static boolean PaletteRemap_AddTint(remaptable_t *tr, int start, int end, int r,
 		bg = bg * ia + g * a;
 		bb = bb * ia + b * a;
 
-		tr->remap[i] = NearestColor(
+		remap[i] = NearestColor(
 		    (int)br,
 		    (int)bg,
 		    (int)bb
@@ -424,43 +425,45 @@ static boolean PaletteRemap_AddTint(remaptable_t *tr, int start, int end, int r,
 	return true;
 }
 
-static boolean PaletteRemap_AddInvert(remaptable_t *tr, int start, int end)
+static boolean PaletteRemap_DoInvert(UINT8 *remap, int start, int end)
 {
 	if (IndicesOutOfRange(start, end))
 		return false;
 
 	for (int i = start; i < end; ++i)
 	{
-		tr->remap[i] = NearestColor(
-		    255 - tr->remap[pMasterPalette[i].s.red],
-		    255 - tr->remap[pMasterPalette[i].s.green],
-		    255 - tr->remap[pMasterPalette[i].s.blue]
+		remap[i] = NearestColor(
+		    255 - pMasterPalette[remap[i]].s.red,
+		    255 - pMasterPalette[remap[i]].s.green,
+		    255 - pMasterPalette[remap[i]].s.blue
 		);
 	}
 
 	return true;
 }
 
+struct PaletteRemapParseResult
+{
+	paletteremap_t remap;
+	char *error;
+};
+
 struct ParsedTranslation
 {
 	struct ParsedTranslation *next;
 	remaptable_t *remap;
 	remaptable_t *baseTranslation;
-	struct PaletteRemapParseResult *data;
 };
 
 static struct ParsedTranslation *parsedTranslationListHead = NULL;
 static struct ParsedTranslation *parsedTranslationListTail = NULL;
 
-static void AddParsedTranslation(unsigned id, int base_translation, struct PaletteRemapParseResult *data)
+static void AddParsedTranslation(remaptable_t *remap, remaptable_t *base_translation)
 {
 	struct ParsedTranslation *node = Z_Calloc(sizeof(struct ParsedTranslation), PU_STATIC, NULL);
 
-	node->remap = paletteremaps[id];
-	node->data = data;
-
-	if (base_translation != -1)
-		node->baseTranslation = paletteremaps[base_translation];
+	node->remap = remap;
+	node->baseTranslation = base_translation;
 
 	if (parsedTranslationListHead == NULL)
 		parsedTranslationListHead = parsedTranslationListTail = node;
@@ -471,7 +474,7 @@ static void AddParsedTranslation(unsigned id, int base_translation, struct Palet
 	}
 }
 
-static void PaletteRemap_ApplyResult(remaptable_t *tr, struct PaletteRemapParseResult *data)
+static void PaletteRemap_Apply(UINT8 *remap, paletteremap_t *data)
 {
 	int start = data->start;
 	int end = data->end;
@@ -479,24 +482,27 @@ static void PaletteRemap_ApplyResult(remaptable_t *tr, struct PaletteRemapParseR
 	switch (data->type)
 	{
 	case REMAP_ADD_INDEXRANGE:
-		PaletteRemap_AddIndexRange(tr, start, end, data->indexRange.pal1, data->indexRange.pal2);
+		PaletteRemap_DoIndexRange(remap, start, end, data->indexRange.pal1, data->indexRange.pal2);
 		break;
 	case REMAP_ADD_COLORRANGE:
-		PaletteRemap_AddColorRange(tr, start, end,
+		PaletteRemap_DoColorRange(remap, start, end,
 			data->colorRange.r1, data->colorRange.g1, data->colorRange.b1,
 			data->colorRange.r2, data->colorRange.g2, data->colorRange.b2);
 		break;
 	case REMAP_ADD_COLOURISATION:
-		PaletteRemap_AddColourisation(tr, start, end,
+		PaletteRemap_DoColourisation(remap, start, end,
 			data->colourisation.r, data->colourisation.g, data->colourisation.b);
 		break;
 	case REMAP_ADD_DESATURATION:
-		PaletteRemap_AddDesaturation(tr, start, end,
+		PaletteRemap_DoDesaturation(remap, start, end,
 			data->desaturation.r1, data->desaturation.g1, data->desaturation.b1,
 			data->desaturation.r2, data->desaturation.g2, data->desaturation.b2);
 		break;
 	case REMAP_ADD_TINT:
-		PaletteRemap_AddTint(tr, start, end, data->tint.r, data->tint.g, data->tint.b, data->tint.amount);
+		PaletteRemap_DoTint(remap, start, end, data->tint.r, data->tint.g, data->tint.b, data->tint.amount);
+		break;
+	case REMAP_ADD_INVERT:
+		PaletteRemap_DoInvert(remap, start, end);
 		break;
 	}
 }
@@ -506,18 +512,18 @@ void R_LoadParsedTranslations(void)
 	struct ParsedTranslation *node = parsedTranslationListHead;
 	while (node)
 	{
-		struct PaletteRemapParseResult *result = node->data;
 		struct ParsedTranslation *next = node->next;
 
 		remaptable_t *tr = node->remap;
+
 		PaletteRemap_SetIdentity(tr);
 
 		if (node->baseTranslation)
 			memcpy(tr, node->baseTranslation, sizeof(remaptable_t));
 
-		PaletteRemap_ApplyResult(tr, result);
+		for (unsigned i = 0; i < tr->num_sources; i++)
+			PaletteRemap_Apply(tr->remap, &tr->sources[i]);
 
-		Z_Free(result);
 		Z_Free(node);
 
 		node = next;
@@ -528,72 +534,48 @@ void R_LoadParsedTranslations(void)
 
 static boolean ExpectToken(tokenizer_t *sc, const char *expect)
 {
-	return strcmp(sc->get(sc, 0), expect) == 0;
-}
-
-static boolean StringToNumber(const char *tkn, int *out)
-{
-	char *endPos = NULL;
-
-	errno = 0;
-
-	int result = strtol(tkn, &endPos, 10);
-	if (endPos == tkn || *endPos != '\0')
-		return false;
-
-	if (errno == ERANGE)
+	const char *tkn = sc->get(sc, 0);
+	if (!tkn)
 		return false;
-
-	*out = result;
-
-	return true;
+	return strcmp(tkn, expect) == 0;
 }
 
 static boolean ParseNumber(tokenizer_t *sc, int *out)
 {
-	return StringToNumber(sc->get(sc, 0), out);
+	const char *tkn = sc->get(sc, 0);
+	if (!tkn)
+		return false;
+	return M_StringToNumber(tkn, out);
 }
 
 static boolean ParseDecimal(tokenizer_t *sc, double *out)
 {
 	const char *tkn = sc->get(sc, 0);
-
-	char *endPos = NULL;
-
-	errno = 0;
-
-	double result = strtod(tkn, &endPos);
-	if (endPos == tkn || *endPos != '\0')
-		return false;
-
-	if (errno == ERANGE)
+	if (!tkn)
 		return false;
-
-	*out = result;
-
-	return true;
+	return M_StringToDecimal(tkn, out);
 }
 
 static struct PaletteRemapParseResult *ThrowError(const char *format, ...)
 {
+	const size_t err_size = 512 * sizeof(char);
+
 	struct PaletteRemapParseResult *err = Z_Calloc(sizeof(struct PaletteRemapParseResult), PU_STATIC, NULL);
 
+	err->error = Z_Calloc(err_size, PU_STATIC, NULL);
+
 	va_list argptr;
 	va_start(argptr, format);
-	vsnprintf(err->error, sizeof err->error, format, argptr);
+	vsnprintf(err->error, err_size, format, argptr);
 	va_end(argptr);
 
-	err->has_error = true;
-
 	return err;
 }
 
-static struct PaletteRemapParseResult *MakeResult(enum PaletteRemapType type, int start, int end)
+static struct PaletteRemapParseResult *MakeResult(paletteremaptype_t type, int start, int end)
 {
 	struct PaletteRemapParseResult *tr = Z_Calloc(sizeof(struct PaletteRemapParseResult), PU_STATIC, NULL);
-	tr->type = type;
-	tr->start = start;
-	tr->end = end;
+	InitSource(&tr->remap, type, start, end);
 	return tr;
 }
 
@@ -615,6 +597,9 @@ static struct PaletteRemapParseResult *PaletteRemap_ParseString(tokenizer_t *sc)
 		return ThrowError("expected '='");
 
 	const char *tkn = sc->get(sc, 0);
+	if (tkn == NULL)
+		return ThrowError("unexpected EOF");
+
 	if (strcmp(tkn, "[") == 0)
 	{
 		// translation using RGB values
@@ -661,12 +646,12 @@ static struct PaletteRemapParseResult *PaletteRemap_ParseString(tokenizer_t *sc)
 			return ThrowError("expected ']'");
 
 		struct PaletteRemapParseResult *tr = MakeResult(REMAP_ADD_COLORRANGE, start, end);
-		tr->colorRange.r1 = r1;
-		tr->colorRange.g1 = g1;
-		tr->colorRange.b1 = b1;
-		tr->colorRange.r2 = r2;
-		tr->colorRange.g2 = g2;
-		tr->colorRange.b2 = b2;
+		tr->remap.colorRange.r1 = r1;
+		tr->remap.colorRange.g1 = g1;
+		tr->remap.colorRange.b1 = b1;
+		tr->remap.colorRange.r2 = r2;
+		tr->remap.colorRange.g2 = g2;
+		tr->remap.colorRange.b2 = b2;
 		return tr;
 	}
 	else if (strcmp(tkn, "%") == 0)
@@ -716,12 +701,12 @@ static struct PaletteRemapParseResult *PaletteRemap_ParseString(tokenizer_t *sc)
 			return ThrowError("expected ']'");
 
 		struct PaletteRemapParseResult *tr = MakeResult(REMAP_ADD_DESATURATION, start, end);
-		tr->desaturation.r1 = r1;
-		tr->desaturation.g1 = g1;
-		tr->desaturation.b1 = b1;
-		tr->desaturation.r2 = r2;
-		tr->desaturation.g2 = g2;
-		tr->desaturation.b2 = b2;
+		tr->remap.desaturation.r1 = r1;
+		tr->remap.desaturation.g1 = g1;
+		tr->remap.desaturation.b1 = b1;
+		tr->remap.desaturation.r2 = r2;
+		tr->remap.desaturation.g2 = g2;
+		tr->remap.desaturation.b2 = b2;
 		return tr;
 	}
 	else if (strcmp(tkn, "#") == 0)
@@ -745,9 +730,9 @@ static struct PaletteRemapParseResult *PaletteRemap_ParseString(tokenizer_t *sc)
 			return ThrowError("expected ']'");
 
 		struct PaletteRemapParseResult *tr = MakeResult(REMAP_ADD_COLOURISATION, start, end);
-		tr->colourisation.r = r;
-		tr->colourisation.g = g;
-		tr->colourisation.b = b;
+		tr->remap.colourisation.r = r;
+		tr->remap.colourisation.g = g;
+		tr->remap.colourisation.b = b;
 		return tr;
 	}
 	else if (strcmp(tkn, "@") == 0)
@@ -755,12 +740,10 @@ static struct PaletteRemapParseResult *PaletteRemap_ParseString(tokenizer_t *sc)
 		// Tint translation
 		int a, r, g, b;
 
-		if (!ExpectToken(sc, "["))
-			return ThrowError("expected '[");
 		if (!ParseNumber(sc, &a))
 			return ThrowError("expected a number for amount");
-		if (!ExpectToken(sc, ","))
-			return ThrowError("expected ','");
+		if (!ExpectToken(sc, "["))
+			return ThrowError("expected '[");
 		if (!ParseNumber(sc, &r))
 			return ThrowError("expected a number for red");
 		if (!ExpectToken(sc, ","))
@@ -775,17 +758,17 @@ static struct PaletteRemapParseResult *PaletteRemap_ParseString(tokenizer_t *sc)
 			return ThrowError("expected ']'");
 
 		struct PaletteRemapParseResult *tr = MakeResult(REMAP_ADD_TINT, start, end);
-		tr->tint.r = r;
-		tr->tint.g = g;
-		tr->tint.b = b;
-		tr->tint.amount = a;
+		tr->remap.tint.r = r;
+		tr->remap.tint.g = g;
+		tr->remap.tint.b = b;
+		tr->remap.tint.amount = a;
 		return tr;
 	}
 	else
 	{
 		int pal1, pal2;
 
-		if (!StringToNumber(tkn, &pal1))
+		if (!M_StringToNumber(tkn, &pal1))
 			return ThrowError("expected a number for starting index");
 		if (!ExpectToken(sc, ":"))
 			return ThrowError("expected ':'");
@@ -793,8 +776,8 @@ static struct PaletteRemapParseResult *PaletteRemap_ParseString(tokenizer_t *sc)
 			return ThrowError("expected a number for ending index");
 
 		struct PaletteRemapParseResult *tr = MakeResult(REMAP_ADD_INDEXRANGE, start, end);
-		tr->indexRange.pal1 = pal1;
-		tr->indexRange.pal2 = pal2;
+		tr->remap.indexRange.pal1 = pal1;
+		tr->remap.indexRange.pal2 = pal2;
 		return tr;
 	}
 
@@ -809,8 +792,125 @@ static struct PaletteRemapParseResult *PaletteRemap_ParseTranslation(const char
 	return result;
 }
 
+static void PrintError(const char *name, const char *format, ...)
+{
+	char error[256];
+
+	va_list argptr;
+	va_start(argptr, format);
+	vsnprintf(error, sizeof error, format, argptr);
+	va_end(argptr);
+
+	CONS_Alert(CONS_ERROR, "Error parsing translation '%s': %s\n", name, error);
+}
+
+#define CHECK_EOF() \
+	if (!tkn) \
+	{ \
+		PrintError(name, "Unexpected EOF"); \
+		goto fail; \
+	}
+
+struct NewTranslation
+{
+	int id;
+	char *name;
+	char *base_translation_name;
+	struct PaletteRemapParseResult **results;
+	size_t num_results;
+};
+
+static void AddNewTranslation(struct NewTranslation **list_p, size_t *num, char *name, int id, char *base_translation_name, struct PaletteRemapParseResult *parse_result)
+{
+	struct NewTranslation *list = *list_p;
+
+	size_t count = *num;
+
+	for (size_t i = 0; i < count; i++)
+	{
+		struct NewTranslation *entry = &list[i];
+		if (entry->id == id && strcmp(entry->name, name) == 0)
+		{
+			if (entry->base_translation_name && base_translation_name
+			&& strcmp(entry->base_translation_name, base_translation_name) != 0)
+				continue;
+			entry->num_results++;
+			entry->results = Z_Realloc(entry->results,
+				entry->num_results * sizeof(struct PaletteRemapParseResult **), PU_STATIC, NULL);
+			entry->results[entry->num_results - 1] = parse_result;
+			return;
+		}
+	}
+
+	size_t i = count;
+
+	count++;
+	list = Z_Realloc(list, count * sizeof(struct NewTranslation), PU_STATIC, NULL);
+
+	struct NewTranslation *entry = &list[i];
+	entry->name = name;
+	entry->id = id;
+	entry->base_translation_name = base_translation_name;
+	entry->num_results = 1;
+	entry->results = Z_Realloc(entry->results, 1 * sizeof(struct PaletteRemapParseResult **), PU_STATIC, NULL);
+	entry->results[0] = parse_result;
+
+	*list_p = list;
+	*num = count;
+}
+
+static void PrepareNewTranslations(struct NewTranslation *list, size_t count)
+{
+	if (!list)
+		return;
+
+	for (size_t i = 0; i < count; i++)
+	{
+		struct NewTranslation *entry = &list[i];
+
+		remaptable_t *tr = R_GetTranslationByID(entry->id);
+		if (tr == NULL)
+		{
+			tr = PaletteRemap_New();
+			R_AddCustomTranslation(entry->name, PaletteRemap_Add(tr));
+		}
+
+		remaptable_t *base_translation = NULL;
+		char *base_translation_name = entry->base_translation_name;
+		if (base_translation_name)
+		{
+			int base_translation_id = R_FindCustomTranslation(base_translation_name);
+			if (base_translation_id == -1)
+				PrintError(entry->name, "No translation named '%s'", base_translation_name);
+			else
+				base_translation = R_GetTranslationByID(base_translation_id);
+		}
+
+		// The translation is not generated until later, because the palette may not have been loaded.
+		// We store the result for when it's needed.
+		tr->sources = Z_Malloc(entry->num_results * sizeof(paletteremap_t), PU_STATIC, NULL);
+		tr->num_sources = entry->num_results;
+
+		for (size_t j = 0; j < entry->num_results; j++)
+		{
+			memcpy(&tr->sources[j], &entry->results[j]->remap, sizeof(paletteremap_t));
+			Z_Free(entry->results[j]);
+		}
+
+		AddParsedTranslation(tr, base_translation);
+
+		Z_Free(base_translation_name);
+		Z_Free(entry->results);
+		Z_Free(entry->name);
+	}
+
+	Z_Free(list);
+}
+
 void R_ParseTrnslate(INT32 wadNum, UINT16 lumpnum)
 {
+	tokenizer_t *sc = NULL;
+	const char *tkn = NULL;
 	char *lumpData = (char *)W_CacheLumpNumPwad(wadNum, lumpnum, PU_STATIC);
 	size_t lumpLength = W_LumpLengthPwad(wadNum, lumpnum);
 	char *text = (char *)Z_Malloc((lumpLength + 1), PU_STATIC, NULL);
@@ -818,74 +918,105 @@ void R_ParseTrnslate(INT32 wadNum, UINT16 lumpnum)
 	text[lumpLength] = '\0';
 	Z_Free(lumpData);
 
-	tokenizer_t *sc = Tokenizer_Open(text, 1);
-	const char *tkn = sc->get(sc, 0);
+	sc = Tokenizer_Open(text, 1);
+	tkn = sc->get(sc, 0);
+
+	struct NewTranslation *list = NULL;
+	size_t list_count = 0;
+
 	while (tkn != NULL)
 	{
-		int base_translation = -1;
-
 		char *name = Z_StrDup(tkn);
 
+		char *base_translation_name = NULL;
+
 		tkn = sc->get(sc, 0);
+		CHECK_EOF();
 		if (strcmp(tkn, ":") == 0)
 		{
 			tkn = sc->get(sc, 0);
+			CHECK_EOF();
 
-			base_translation = R_FindCustomTranslation(tkn);
-			if (base_translation == -1)
-			{
-				CONS_Alert(CONS_ERROR, "Error parsing translation '%s': No translation named '%s'\n", name, tkn);
-				goto fail;
-			}
+			base_translation_name = Z_StrDup(tkn);
 
 			tkn = sc->get(sc, 0);
+			CHECK_EOF();
 		}
 
 		if (strcmp(tkn, "=") != 0)
 		{
-			CONS_Alert(CONS_ERROR, "Error parsing translation '%s': Expected '=', got '%s'\n", name, tkn);
+			PrintError(name, "Expected '=', got '%s'", tkn);
+			goto fail;
+		}
+		tkn = sc->get(sc, 0);
+		CHECK_EOF();
+
+		if (strcmp(tkn, "\"") != 0)
+		{
+			PrintError(name, "Expected '\"', got '%s'", tkn);
 			goto fail;
 		}
 		tkn = sc->get(sc, 0);
+		CHECK_EOF();
 
-		struct PaletteRemapParseResult *result = NULL;
+		int existing_id = R_FindCustomTranslation(name);
+
+		// Parse all of the translations
 		do {
-			result = PaletteRemap_ParseTranslation(tkn);
-			if (result->has_error)
+			struct PaletteRemapParseResult *parse_result = PaletteRemap_ParseTranslation(tkn);
+			if (parse_result->error)
 			{
-				CONS_Alert(CONS_ERROR, "Error parsing translation '%s': %s\n", name, result->error);
-				Z_Free(result);
+				PrintError(name, "%s", parse_result->error);
+				Z_Free(parse_result->error);
 				goto fail;
 			}
+			else
+			{
+				AddNewTranslation(&list, &list_count, name, existing_id, base_translation_name, parse_result);
+			}
 
 			tkn = sc->get(sc, 0);
-			if (!tkn)
-				break;
+			if (!tkn || strcmp(tkn, "\"") != 0)
+			{
+				if (tkn)
+					PrintError(name, "Expected '\"', got '%s'", tkn);
+				else
+					PrintError(name, "Expected '\"', got EOF");
+				goto fail;
+			}
 
-			if (strcmp(tkn, ",") != 0)
+			// Get ',' or parse the next line
+			tkn = sc->get(sc, 0);
+			if (!tkn || strcmp(tkn, ",") != 0)
 				break;
 
+			// Get '"'
+			tkn = sc->get(sc, 0);
+			if (!tkn || strcmp(tkn, "\"") != 0)
+			{
+				if (!tkn)
+					PrintError(name, "Expected '\"', got EOF");
+				else
+					PrintError(name, "Expected '\"', got '%s'", tkn);
+				goto fail;
+			}
 			tkn = sc->get(sc, 0);
+			CHECK_EOF();
 		} while (true);
-
-		// Allocate it and register it
-		remaptable_t *tr = PaletteRemap_New();
-		unsigned id = PaletteRemap_Add(tr);
-		R_AddCustomTranslation(name, id);
-
-		// Free this, since it's no longer needed
-		Z_Free(name);
-
-		// The translation is not generated until later, because the palette may not have been loaded.
-		// We store the result for when it's needed.
-		AddParsedTranslation(id, base_translation, result);
 	}
 
 fail:
+	// Now add all of the new translations
+	if (list)
+		PrepareNewTranslations(list, list_count);
+
 	Tokenizer_Close(sc);
+
 	Z_Free(text);
 }
 
+#undef CHECK_EOF
+
 typedef struct CustomTranslation
 {
 	char *name;
@@ -972,6 +1103,61 @@ remaptable_t *R_GetTranslationByID(int id)
 	return paletteremaps[id];
 }
 
+static void R_ApplyTranslationRemap(remaptable_t *tr, UINT8 *remap, skincolornum_t skincolor, INT32 skinnum)
+{
+	UINT8 *base_skincolor = R_GetTranslationColormap(skinnum, skincolor, GTC_CACHE);
+
+	for (unsigned i = 0; i < NUM_PALETTE_ENTRIES; i++)
+		remap[i] = base_skincolor[i];
+
+	for (unsigned i = 0; i < tr->num_sources; i++)
+		PaletteRemap_Apply(remap, &tr->sources[i]);
+}
+
+UINT8 *R_GetTranslationRemap(int id, skincolornum_t skincolor, INT32 skinnum)
+{
+	remaptable_t *tr = R_GetTranslationByID(id);
+	if (!tr)
+		return NULL;
+
+	if (!tr->num_sources || skincolor == SKINCOLOR_NONE)
+		return tr->remap;
+
+	if (!tr->skincolor_remaps)
+		Z_Calloc(sizeof(*tr->skincolor_remaps) * TT_CACHE_SIZE, PU_LEVEL, &tr->skincolor_remaps);
+
+	if (!tr->skincolor_remaps[skinnum])
+		tr->skincolor_remaps[skinnum] = Z_Calloc(NUM_PALETTE_ENTRIES * MAXSKINCOLORS, PU_LEVEL, NULL);
+
+	colorcache_t *cache = tr->skincolor_remaps[skinnum][skincolor];
+	if (!cache)
+	{
+		cache = Z_Calloc(sizeof(colorcache_t), PU_LEVEL, NULL);
+
+		R_ApplyTranslationRemap(tr, cache->colors, skincolor, skinnum);
+
+		tr->skincolor_remaps[skinnum][skincolor] = cache;
+	}
+
+	return cache->colors;
+}
+
+static void R_UpdateTranslation(remaptable_t *tr, skincolornum_t skincolor, INT32 skinnum)
+{
+	if (!tr->num_sources || !tr->skincolor_remaps || !tr->skincolor_remaps[skinnum])
+		return;
+
+	colorcache_t *cache = tr->skincolor_remaps[skinnum][skincolor];
+	if (cache)
+		R_ApplyTranslationRemap(tr, cache->colors, skincolor, skinnum);
+}
+
+void R_UpdateTranslationRemaps(skincolornum_t skincolor, INT32 skinnum)
+{
+	for (unsigned i = 0; i < numpaletteremaps; i++)
+		R_UpdateTranslation(paletteremaps[i], skincolor, skinnum);
+}
+
 boolean R_TranslationIsValid(int id)
 {
 	if (id < 0 || id >= (signed)numpaletteremaps)
diff --git a/src/r_translation.h b/src/r_translation.h
index 70bc2fd27e4e248301029c93de562c24dab467ac..1eb40233d12070708d9bda26bfa8b53792235352 100644
--- a/src/r_translation.h
+++ b/src/r_translation.h
@@ -15,10 +15,61 @@
 
 #include "doomdef.h"
 
+#include "r_draw.h"
+
+typedef enum
+{
+	REMAP_ADD_INDEXRANGE,
+	REMAP_ADD_COLORRANGE,
+	REMAP_ADD_COLOURISATION,
+	REMAP_ADD_DESATURATION,
+	REMAP_ADD_TINT,
+	REMAP_ADD_INVERT
+} paletteremaptype_t;
+
+typedef struct
+{
+	int start, end;
+	paletteremaptype_t type;
+	union
+	{
+		struct
+		{
+			int pal1, pal2;
+		} indexRange;
+		struct
+		{
+			int r1, g1, b1;
+			int r2, g2, b2;
+		} colorRange;
+		struct
+		{
+			double r1, g1, b1;
+			double r2, g2, b2;
+		} desaturation;
+		struct
+		{
+			int r, g, b;
+		} colourisation;
+		struct
+		{
+			int r, g, b, amount;
+		} tint;
+	};
+} paletteremap_t;
+
 typedef struct
 {
-	UINT8 remap[256];
+	UINT8 remap[NUM_PALETTE_ENTRIES];
 	unsigned num_entries;
+
+	paletteremap_t *sources;
+	unsigned num_sources;
+
+	// A typical remap is 256 bytes long, and there is currently a maximum of 1182 skincolors, and 263 possible color cache entries.
+	// This would mean allocating (1182 * 256 * 263) bytes, which equals 79581696 bytes, or ~79mb of memory for every remap.
+	// So instead a few lists are allocated.
+	colorcache_t ***skincolor_remaps;
 } remaptable_t;
 
 void PaletteRemap_Init(void);
@@ -35,6 +86,8 @@ void R_AddCustomTranslation(const char *name, int trnum);
 const char *R_GetCustomTranslationName(unsigned id);
 unsigned R_NumCustomTranslations(void);
 remaptable_t *R_GetTranslationByID(int id);
+UINT8 *R_GetTranslationRemap(int id, skincolornum_t skincolor, INT32 skinnum);
+void R_UpdateTranslationRemaps(skincolornum_t skincolor, INT32 skinnum);
 boolean R_TranslationIsValid(int id);
 
 void R_ParseTrnslate(INT32 wadNum, UINT16 lumpnum);