diff --git a/src/m_tokenizer.c b/src/m_tokenizer.c
index 876dc6b2001ab8e6dd55638d9b61236f6473de5a..f36f7f6f323133c51c4975992beea9919c27e4bd 100644
--- a/src/m_tokenizer.c
+++ b/src/m_tokenizer.c
@@ -175,6 +175,7 @@ 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->endPos = tokenizer->startPos + 1;
@@ -200,6 +201,7 @@ 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)
diff --git a/src/r_things.c b/src/r_things.c
index 084871700a3fc4ec07dae329431046981035b09b..d135386a0833fe26f66d1908a39865bd0879e788 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 d732f6b0834360aa831c5e569c3d8603afec6194..3799c87cd5c968d23c6c6f4da8a188f7a4e8c153 100644
--- a/src/r_translation.c
+++ b/src/r_translation.c
@@ -27,56 +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;
 
-	char *error;
-};
+	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();
@@ -162,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)
 {
@@ -229,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;
@@ -241,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;
 	}
 
@@ -251,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;
@@ -294,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
 	{
@@ -304,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;
@@ -316,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;
@@ -345,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)))
@@ -361,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;
@@ -379,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
@@ -389,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;
 
@@ -406,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
@@ -416,40 +425,44 @@ 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(remaptable_t *remap, remaptable_t *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 = remap;
-	node->data = data;
 	node->baseTranslation = base_translation;
 
 	if (parsedTranslationListHead == NULL)
@@ -461,7 +474,7 @@ static void AddParsedTranslation(remaptable_t *remap, remaptable_t *base_transla
 	}
 }
 
-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;
@@ -469,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;
 	}
 }
@@ -496,26 +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;
-		if (tr)
-		{
-			if (tr->num_entries == 0)
-			{
-				tr->num_entries = NUM_PALETTE_ENTRIES;
 
-				PaletteRemap_SetIdentity(tr);
+		PaletteRemap_SetIdentity(tr);
 
-				if (node->baseTranslation)
-					memcpy(tr, node->baseTranslation, sizeof(remaptable_t));
-			}
+		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;
@@ -564,12 +572,10 @@ static struct PaletteRemapParseResult *ThrowError(const char *format, ...)
 	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;
 }
 
@@ -640,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)
@@ -695,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)
@@ -724,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)
@@ -734,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, ","))
@@ -754,10 +758,10 @@ 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
@@ -772,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;
 	}
 
@@ -884,10 +888,16 @@ static void PrepareNewTranslations(struct NewTranslation *list, size_t count)
 
 		// 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++)
-			AddParsedTranslation(tr, base_translation, entry->results[j]);
+		{
+			memcpy(&tr->sources[j], &entry->results[j]->remap, sizeof(paletteremap_t));
+			Z_Free(entry->results[j]);
+		}
 
-		tr->num_entries = 0;
+		AddParsedTranslation(tr, base_translation);
 
 		Z_Free(base_translation_name);
 		Z_Free(entry->results);
@@ -1093,6 +1103,36 @@ remaptable_t *R_GetTranslationByID(int id)
 	return paletteremaps[id];
 }
 
+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_remap)
+		tr->skincolor_remap = Z_Calloc(NUM_PALETTE_ENTRIES * MAXSKINCOLORS, PU_LEVEL, &tr->skincolor_remap);
+
+	if (!tr->skincolor_remap[skincolor])
+	{
+		UINT8 *remap = Z_Calloc(NUM_PALETTE_ENTRIES, PU_LEVEL, NULL);
+
+		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]);
+
+		tr->skincolor_remap[skincolor] = remap;
+	}
+
+	return tr->skincolor_remap[skincolor];
+}
+
 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..6219278f66ce906e5f6c382944fadb4574620228 100644
--- a/src/r_translation.h
+++ b/src/r_translation.h
@@ -15,10 +15,59 @@
 
 #include "doomdef.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];
 	unsigned num_entries;
+
+	paletteremap_t *sources;
+	unsigned num_sources;
+
+	// A remap is 256 bytes long, and there is a max of 1182.
+	// This means allocating (1182 * 256) bytes, which equals 302592, or ~302kb of memory for every translation.
+	// So we allocate a list instead.
+	UINT8 **skincolor_remap;
 } remaptable_t;
 
 void PaletteRemap_Init(void);
@@ -35,6 +84,7 @@ 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);
 boolean R_TranslationIsValid(int id);
 
 void R_ParseTrnslate(INT32 wadNum, UINT16 lumpnum);