diff --git a/src/dehacked.c b/src/dehacked.c
index d4f27b61c4d7cac2bfc8cfb074d08f1f1f3e1b15..463a71253e00b37915a75a8ddd7f5f9cfb69b86c 100644
--- a/src/dehacked.c
+++ b/src/dehacked.c
@@ -316,6 +316,10 @@ static boolean findFreeSlot(INT32 *num)
 	// Redesign your logo. (See M_DrawSetupChoosePlayerMenu in m_menu.c...)
 	description[*num].picname[0] = '\0';
 	description[*num].nametag[0] = '\0';
+	description[*num].displayname[0] = '\0';
+	description[*num].oppositecolor = SKINCOLOR_NONE;
+	description[*num].tagtextcolor = SKINCOLOR_NONE;
+	description[*num].tagoutlinecolor = SKINCOLOR_NONE;
 
 	// Found one! ^_^
 	return (description[*num].used = true);
@@ -328,9 +332,16 @@ static void readPlayer(MYFILE *f, INT32 num)
 	char *s = Z_Malloc(MAXLINELEN, PU_STATIC, NULL);
 	char *word;
 	char *word2;
+	char *displayname = ZZ_Alloc(MAXLINELEN+1);
 	INT32 i;
 	boolean slotfound = false;
 
+	#define SLOTFOUND \
+		if (!slotfound && (slotfound = findFreeSlot(&num)) == false) \
+			goto done;
+
+	displayname[MAXLINELEN] = '\0';
+
 	do
 	{
 		if (myfgets(s, MAXLINELEN, f))
@@ -338,6 +349,17 @@ static void readPlayer(MYFILE *f, INT32 num)
 			if (s[0] == '\n')
 				break;
 
+			for (i = 0; i < MAXLINELEN-3; i++)
+			{
+				char *tmp;
+				if (s[i] == '=')
+				{
+					tmp = &s[i+2];
+					strncpy(displayname, tmp, SKINNAMESIZE);
+					break;
+				}
+			}
+
 			word = strtok(s, " ");
 			if (word)
 				strupr(word);
@@ -348,8 +370,7 @@ static void readPlayer(MYFILE *f, INT32 num)
 			{
 				char *playertext = NULL;
 
-				if (!slotfound && (slotfound = findFreeSlot(&num)) == false)
-					goto done;
+				SLOTFOUND
 
 				for (i = 0; i < MAXLINELEN-3; i++)
 				{
@@ -397,18 +418,54 @@ static void readPlayer(MYFILE *f, INT32 num)
 
 			if (fastcmp(word, "PICNAME"))
 			{
-				if (!slotfound && (slotfound = findFreeSlot(&num)) == false)
-					goto done;
-
+				SLOTFOUND
 				strncpy(description[num].picname, word2, 8);
 			}
-			else if (fastcmp(word, "NAMETAG") || fastcmp(word, "TAGNAME"))
+			// new character select
+			else if (fastcmp(word, "DISPLAYNAME"))
 			{
-				if (!slotfound && (slotfound = findFreeSlot(&num)) == false)
-					goto done;
+				SLOTFOUND
+				// replace '#' with line breaks
+				// (also remove any '\n')
+				{
+					char *cur = NULL;
+
+					// remove '\n'
+					cur = strchr(displayname, '\n');
+					if (cur)
+						*cur = '\0';
 
+					// turn '#' into '\n'
+					cur = strchr(displayname, '#');
+					while (cur)
+					{
+						*cur = '\n';
+						cur = strchr(cur, '#');
+					}
+				}
+				// copy final string
+				strncpy(description[num].displayname, displayname, SKINNAMESIZE);
+			}
+			else if (fastcmp(word, "OPPOSITECOLOR") || fastcmp(word, "OPPOSITECOLOUR"))
+			{
+				SLOTFOUND
+				description[num].oppositecolor = (UINT8)get_number(word2);
+			}
+			else if (fastcmp(word, "NAMETAG") || fastcmp(word, "TAGNAME"))
+			{
+				SLOTFOUND
 				strncpy(description[num].nametag, word2, 8);
 			}
+			else if (fastcmp(word, "TAGTEXTCOLOR") || fastcmp(word, "TAGTEXTCOLOUR"))
+			{
+				SLOTFOUND
+				description[num].tagtextcolor = (UINT8)get_number(word2);
+			}
+			else if (fastcmp(word, "TAGOUTLINECOLOR") || fastcmp(word, "TAGOUTLINECOLOUR"))
+			{
+				SLOTFOUND
+				description[num].tagoutlinecolor = (UINT8)get_number(word2);
+			}
 			else if (fastcmp(word, "STATUS"))
 			{
 				/*
@@ -426,9 +483,7 @@ static void readPlayer(MYFILE *f, INT32 num)
 			else if (fastcmp(word, "SKINNAME"))
 			{
 				// Send to free slot.
-				if (!slotfound && (slotfound = findFreeSlot(&num)) == false)
-					goto done;
-
+				SLOTFOUND
 				strlcpy(description[num].skinname, word2, sizeof description[num].skinname);
 				strlwr(description[num].skinname);
 			}
@@ -436,8 +491,9 @@ static void readPlayer(MYFILE *f, INT32 num)
 				deh_warning("readPlayer %d: unknown word '%s'", num, word);
 		}
 	} while (!myfeof(f)); // finish when the line is empty
-
+	#undef SLOTFOUND
 done:
+	Z_Free(displayname);
 	Z_Free(s);
 }
 
@@ -9057,6 +9113,7 @@ struct {
 	{"V_OFFSET",V_OFFSET},
 	{"V_ALLOWLOWERCASE",V_ALLOWLOWERCASE},
 	{"V_FLIP",V_FLIP},
+	{"V_CENTERNAMETAG",V_CENTERNAMETAG},
 	{"V_SNAPTOTOP",V_SNAPTOTOP},
 	{"V_SNAPTOBOTTOM",V_SNAPTOBOTTOM},
 	{"V_SNAPTOLEFT",V_SNAPTOLEFT},
diff --git a/src/lua_hudlib.c b/src/lua_hudlib.c
index 8c1134bca7c942ed4fe18398570e0713ec9fe686..865b61e8fcb6cd1206beb4b8d4485dea99cf59c4 100644
--- a/src/lua_hudlib.c
+++ b/src/lua_hudlib.c
@@ -637,6 +637,68 @@ static int libd_drawString(lua_State *L)
 	return 0;
 }
 
+static int libd_drawNameTag(lua_State *L)
+{
+	INT32 x;
+	INT32 y;
+	const char *str;
+	INT32 flags;
+	UINT8 basecolor;
+	UINT8 outlinecolor;
+	UINT8 *basecolormap = NULL;
+	UINT8 *outlinecolormap = NULL;
+
+	HUDONLY
+
+	x = luaL_checkinteger(L, 1);
+	y = luaL_checkinteger(L, 2);
+	str = luaL_checkstring(L, 3);
+	flags = luaL_optinteger(L, 4, 0);
+	basecolor = luaL_optinteger(L, 5, SKINCOLOR_BLUE);
+	outlinecolor = luaL_optinteger(L, 6, SKINCOLOR_ORANGE);
+	if (basecolor != SKINCOLOR_NONE)
+		basecolormap = R_GetTranslationColormap(TC_DEFAULT, basecolor, GTC_CACHE);
+	if (outlinecolor != SKINCOLOR_NONE)
+		outlinecolormap = R_GetTranslationColormap(TC_DEFAULT, outlinecolor, GTC_CACHE);
+
+	flags &= ~V_PARAMMASK; // Don't let crashes happen.
+	V_DrawNameTag(x, y, flags, FRACUNIT, basecolormap, outlinecolormap, str);
+	return 0;
+}
+
+static int libd_drawScaledNameTag(lua_State *L)
+{
+	fixed_t x;
+	fixed_t y;
+	const char *str;
+	INT32 flags;
+	fixed_t scale;
+	UINT8 basecolor;
+	UINT8 outlinecolor;
+	UINT8 *basecolormap = NULL;
+	UINT8 *outlinecolormap = NULL;
+
+	HUDONLY
+
+	x = luaL_checkfixed(L, 1);
+	y = luaL_checkfixed(L, 2);
+	str = luaL_checkstring(L, 3);
+	flags = luaL_optinteger(L, 4, 0);
+	scale = luaL_optinteger(L, 5, FRACUNIT);
+	if (scale < 0)
+		return luaL_error(L, "negative scale");
+	basecolor = luaL_optinteger(L, 6, SKINCOLOR_BLUE);
+	outlinecolor = luaL_optinteger(L, 7, SKINCOLOR_ORANGE);
+	if (basecolor != SKINCOLOR_NONE)
+		basecolormap = R_GetTranslationColormap(TC_DEFAULT, basecolor, GTC_CACHE);
+	if (outlinecolor != SKINCOLOR_NONE)
+		outlinecolormap = R_GetTranslationColormap(TC_DEFAULT, outlinecolor, GTC_CACHE);
+
+	flags &= ~V_PARAMMASK; // Don't let crashes happen.
+	V_DrawNameTag(FixedInt(x), FixedInt(y), flags, scale, basecolormap, outlinecolormap, str);
+	return 0;
+}
+
 static int libd_stringWidth(lua_State *L)
 {
 	const char *str = luaL_checkstring(L, 1);
@@ -659,6 +721,13 @@ static int libd_stringWidth(lua_State *L)
 	return 1;
 }
 
+static int libd_nameTagWidth(lua_State *L)
+{
+	HUDONLY
+	lua_pushinteger(L, V_NameTagWidth(luaL_checkstring(L, 1)));
+	return 1;
+}
+
 static int libd_getColormap(lua_State *L)
 {
 	INT32 skinnum = TC_DEFAULT;
@@ -837,9 +906,12 @@ static luaL_Reg lib_draw[] = {
 	{"drawPaddedNum", libd_drawPaddedNum},
 	{"drawFill", libd_drawFill},
 	{"drawString", libd_drawString},
+	{"drawNameTag", libd_drawNameTag},
+	{"drawScaledNameTag", libd_drawScaledNameTag},
 	{"fadeScreen", libd_fadeScreen},
 	// misc
 	{"stringWidth", libd_stringWidth},
+	{"nameTagWidth", libd_nameTagWidth},
 	// m_random
 	{"RandomFixed",libd_RandomFixed},
 	{"RandomByte",libd_RandomByte},
diff --git a/src/m_menu.c b/src/m_menu.c
index 894b2956cf4afdfb2331612625d6f27ed35067b0..958efaaa2b605294a1cbb1f64d499f73472029d2 100644
--- a/src/m_menu.c
+++ b/src/m_menu.c
@@ -3637,10 +3637,13 @@ void M_InitCharacterTables(void)
 		description[i].used = false;
 		strcpy(description[i].notes, "???");
 		strcpy(description[i].picname, "");
+		strcpy(description[i].nametag, "");
 		strcpy(description[i].skinname, "");
+		strcpy(description[i].displayname, "");
 		description[i].prev = description[i].next = 0;
 		description[i].charpic = NULL;
 		description[i].namepic = NULL;
+		description[i].oppositecolor = description[i].tagtextcolor = description[i].tagoutlinecolor = 0;
 	}
 }
 
@@ -7942,19 +7945,11 @@ static void M_SetupChoosePlayer(INT32 choice)
 						description[i].namepic = W_CachePatchNum(sprframe->lumppat[0], PU_CACHE);
 					}
 					else
-					{
-						// If no name tag patch was provided,
-						// the character select screen
-						// will simply not draw anything.
 						description[i].namepic = NULL;
-					}
 				}
-				else
+				else if (description[i].nametag[0])
 				{
 					const char *nametag = description[i].nametag;
-					// If no name tag patch was provided,
-					// the character select screen
-					// will simply not draw anything.
 					description[i].namepic = NULL;
 					if (W_LumpExists(nametag))
 						description[i].namepic = W_CachePatchName(nametag, PU_CACHE);
@@ -8129,7 +8124,9 @@ static void M_DrawSetupChoosePlayerMenu(void)
 		charskin = &skins[skinnum];
 
 	// Use the opposite of the character's skincolor
-	col = Color_Opposite[charskin->prefcolor - 1][0];
+	col = description[char_on].oppositecolor;
+	if (!col)
+		col = Color_Opposite[charskin->prefcolor - 1][0];
 
 	// Make the translation colormap
 	colormap = R_GetTranslationColormap(skinnum, col, 0);
@@ -8180,43 +8177,136 @@ static void M_DrawSetupChoosePlayerMenu(void)
 		INT32 ox, x, y;
 		INT32 oxsh = FixedInt(FixedMul(BASEVIDWIDTH*FRACUNIT, FixedDiv(char_scroll, 128*FRACUNIT))), txsh;
 		patch_t *curpatch = NULL, *prevpatch = NULL, *nextpatch = NULL;
+		const char *curtext = NULL, *prevtext = NULL, *nexttext = NULL;
+		UINT8 curtextcolor = 0, prevtextcolor = 0, nexttextcolor = 0;
+		UINT8 curoutlinecolor = 0, prevoutlinecolor = 0, nextoutlinecolor = 0;
+
+		// Name tag
+		curtext = description[char_on].displayname;
+		curtextcolor = description[char_on].tagtextcolor;
+		curoutlinecolor = description[char_on].tagoutlinecolor;
+		if (curtext[0] == '\0')
+			curpatch = description[char_on].namepic;
+		if (skinnum != -1)
+		{
+			if (!curtextcolor)
+				curtextcolor = charskin->prefcolor;
+			if (!curoutlinecolor)
+				curoutlinecolor = Color_Opposite[charskin->prefcolor - 1][0];
+		}
 
-		// Name tag patches
-		curpatch = description[char_on].namepic;
-		if (prev != -1) prevpatch = description[prev].namepic;
-		if (next != -1) nextpatch = description[next].namepic;
+		// previous character
+		if (prev != -1)
+		{
+			prevtext = description[prev].displayname;
+			prevtextcolor = description[prev].tagtextcolor;
+			prevoutlinecolor = description[prev].tagoutlinecolor;
+			if (prevtext[0] == '\0')
+				prevpatch = description[prev].namepic;
+			// Find skin number from description[]
+			skinnum = getskinfromdescription(prev);
+			if (skinnum != -1)
+			{
+				charskin = &skins[skinnum];
+				if (!prevtextcolor)
+					prevtextcolor = charskin->prefcolor;
+				if (!prevoutlinecolor)
+					prevoutlinecolor = Color_Opposite[charskin->prefcolor - 1][0];
+			}
+		}
+
+		// next character
+		if (next != -1)
+		{
+			nexttext = description[next].displayname;
+			nexttextcolor = description[next].tagtextcolor;
+			nextoutlinecolor = description[next].tagoutlinecolor;
+			if (nexttext[0] == '\0')
+				nextpatch = description[next].namepic;
+			// Find skin number from description[]
+			skinnum = getskinfromdescription(next);
+			if (skinnum != -1)
+			{
+				charskin = &skins[skinnum];
+				if (!nexttextcolor)
+					nexttextcolor = charskin->prefcolor;
+				if (!nextoutlinecolor)
+					nextoutlinecolor = Color_Opposite[charskin->prefcolor - 1][0];
+			}
+		}
 
 		txsh = oxsh;
 		ox = 8 + SHORT((description[char_on].charpic)->width)/2;
-		if (curpatch)
-			ox -= (SHORT(curpatch->width)/2);
 		y = my + 144;
 
 		if (char_scroll)
 		{
 			// prev
-			if (prevpatch && char_scroll < 0)
+			if ((prev != -1) && char_scroll < 0)
 			{
+				INT32 ox2 = ox;
+				if (prevpatch)
+					ox2 -= (SHORT(prevpatch->width)/2);
 				// Why does this work?
-				x = (ox - txsh) - BASEVIDWIDTH;
-				V_DrawScaledPatch(x, y, 0, prevpatch);
+				x = (ox2 - txsh) - BASEVIDWIDTH;
+				if (prevtext[0] != '\0')
+				{
+					skinnum = getskinfromdescription(prev);
+					V_DrawNameTag(
+						x, y, V_CENTERNAMETAG, FRACUNIT,
+						R_GetTranslationColormap(skinnum, prevtextcolor, 0),
+						R_GetTranslationColormap(skinnum, prevoutlinecolor, 0),
+						prevtext
+					);
+				}
+				else if (prevpatch)
+					V_DrawScaledPatch(x, y, 0, prevpatch);
 			}
 			// next
-			else if (nextpatch && char_scroll > 0)
+			else if ((next != -1) && char_scroll > 0)
 			{
-				x = (ox - txsh) + BASEVIDWIDTH;
+				INT32 ox2 = ox;
+				if (nextpatch)
+					ox2 -= (SHORT(nextpatch->width)/2);
+				x = (ox2 - txsh) + BASEVIDWIDTH;
 				if (x < BASEVIDWIDTH)
-					V_DrawScaledPatch(x, y, 0, nextpatch);
+				{
+					if (nexttext[0] != '\0')
+					{
+						skinnum = getskinfromdescription(next);
+						V_DrawNameTag(
+							x, y, V_CENTERNAMETAG, FRACUNIT,
+							R_GetTranslationColormap(skinnum, nexttextcolor, 0),
+							R_GetTranslationColormap(skinnum, nextoutlinecolor, 0),
+							nexttext
+						);
+					}
+					else if (nextpatch)
+						V_DrawScaledPatch(x, y, 0, nextpatch);
+				}
 			}
 		}
 
 		// cur
-		x = ox - txsh;
-		//if (curpatch)
-		//	V_DrawScaledPatch(x, y, 0, curpatch);
-
-		// Dummy string to be removed when finalized
-		V_DrawNameTag(x, y, 0, R_GetTranslationColormap(skinnum, SKINCOLOR_BLUE, 0), R_GetTranslationColormap(skinnum, SKINCOLOR_YELLOW, 0), "Sonic\n&Tails.");
+		skinnum = getskinfromdescription(next);
+		if (skinnum != -1)
+		{
+			INT32 ox2 = ox;
+			if (curpatch)
+				ox2 -= (SHORT(curpatch->width)/2);
+			x = ox2 - txsh;
+			if (curtext[0] != '\0')
+			{
+				V_DrawNameTag(
+					x, y, V_CENTERNAMETAG, FRACUNIT,
+					R_GetTranslationColormap(skinnum, curtextcolor, 0),
+					R_GetTranslationColormap(skinnum, curoutlinecolor, 0),
+					curtext
+				);
+			}
+			else if (curpatch)
+				V_DrawScaledPatch(x, y, 0, curpatch);
+		}
 	}
 
 	// Alternative menu header
diff --git a/src/m_menu.h b/src/m_menu.h
index 409ef4e085efbcf6a24c081bf812ba0a9d5a053d..ad29cf2e74d417b023ccad692f59d3f2c377d717 100644
--- a/src/m_menu.h
+++ b/src/m_menu.h
@@ -322,12 +322,18 @@ typedef struct
 	boolean used;
 	char notes[441];
 	char picname[8];
-	char nametag[8];
 	char skinname[SKINNAMESIZE*2+2]; // skin&skin\0
 	patch_t *charpic;
-	patch_t *namepic;
 	UINT8 prev;
 	UINT8 next;
+
+	// new character select
+	char displayname[SKINNAMESIZE+1];
+	UINT8 oppositecolor;
+	char nametag[8];
+	patch_t *namepic;
+	UINT8 tagtextcolor;
+	UINT8 tagoutlinecolor;
 } description_t;
 
 // level select platter
diff --git a/src/v_video.c b/src/v_video.c
index c39f708ee406a72b00b30e5eaac3fb780ff21671..8f44ca8cdbe8b0dc603766e3121ec9327ccc2ca7 100644
--- a/src/v_video.c
+++ b/src/v_video.c
@@ -2630,18 +2630,32 @@ void V_DrawCreditString(fixed_t x, fixed_t y, INT32 option, const char *string)
 
 // Draw a string using the nt_font
 // Note that the outline is a seperate font set
-void V_DrawNameTag(INT32 x, INT32 y, INT32 option, UINT8 *basecolormap, UINT8 *outlinecolormap, const char *string)
+static void V_DrawNameTagLine(INT32 x, INT32 y, INT32 option, fixed_t scale, UINT8 *basecolormap, UINT8 *outlinecolormap, const char *string)
 {
-	INT32 w, c, cx = x, cy = y, dupx, dupy, scrwidth, left = 0;
+	fixed_t cx, cy, w;
+	INT32 c, dupx, dupy, scrwidth, left = 0;
 	const char *ch = string;
-	INT32 spacewidth = 4;
-	INT32 lowercase = (option & V_ALLOWLOWERCASE);
-	option &= ~V_FLIP; // which is also shared with V_ALLOWLOWERCASE...
 
-	dupx = dupy = 1;
-	scrwidth = vid.width/vid.dupx;
-	left = (scrwidth - BASEVIDWIDTH)/2;
-	scrwidth -= left;
+	if (option & V_CENTERNAMETAG)
+		x -= FixedInt(FixedMul((V_NameTagWidth(string)/2)*FRACUNIT, scale));
+	option &= ~V_CENTERNAMETAG; // which is also shared with V_ALLOWLOWERCASE...
+
+	cx = x<<FRACBITS;
+	cy = y<<FRACBITS;
+
+	if (option & V_NOSCALESTART)
+	{
+		dupx = vid.dupx;
+		dupy = vid.dupy;
+		scrwidth = vid.width;
+	}
+	else
+	{
+		dupx = dupy = 1;
+		scrwidth = vid.width/vid.dupx;
+		left = (scrwidth - BASEVIDWIDTH)/2;
+		scrwidth -= left;
+	}
 
 	for (;;ch++)
 	{
@@ -2649,41 +2663,168 @@ void V_DrawNameTag(INT32 x, INT32 y, INT32 option, UINT8 *basecolormap, UINT8 *o
 			break;
 		if (*ch == '\n')
 		{
-			cx = x;
-			cy += 17*dupy;
-
+			cx = x<<FRACBITS;
+			cy += FixedMul((21*dupy)*FRACUNIT, scale);
 			continue;
 		}
 
-		c = *ch;
-		if (!lowercase)
-			c = toupper(c);
+		c = toupper(*ch);
 		c -= NT_FONTSTART;
 
 		// character does not exist or is a space
 		if (c < 0 || c >= NT_FONTSIZE || !ntb_font[c] || !nto_font[c])
 		{
-			cx += spacewidth * dupx;
+			cx += FixedMul((4 * dupx)*FRACUNIT, scale);
 			continue;
 		}
 
-		w = SHORT(ntb_font[c]->width)+4 * dupx;
+		w = FixedMul((SHORT(ntb_font[c]->width)+2 * dupx) * FRACUNIT, scale);
 
-		if (cx > scrwidth)
+		if (FixedInt(cx) > scrwidth)
 			continue;
-		if (cx+left + w < 0) //left boundary check
+		if (cx+(left*FRACUNIT) + w < 0) // left boundary check
 		{
 			cx += w;
 			continue;
 		}
 
-		V_DrawFixedPatch((cx)<<FRACBITS, cy<<FRACBITS, FRACUNIT, option, nto_font[c], outlinecolormap);
-		V_DrawFixedPatch((cx+2)<<FRACBITS, (cy+2)<<FRACBITS, FRACUNIT, option, ntb_font[c], basecolormap);
+		V_DrawFixedPatch(cx, cy, scale, option, nto_font[c], outlinecolormap);
+		V_DrawFixedPatch(cx, cy, scale, option, ntb_font[c], basecolormap);
 
 		cx += w;
 	}
 }
 
+// Looks familiar.
+void V_DrawNameTag(INT32 x, INT32 y, INT32 option, fixed_t scale, UINT8 *basecolormap, UINT8 *outlinecolormap, const char *string)
+{
+	char *text = (char *)string;
+	char *first_token = text;
+	char *last_token = strchr(text, '\n');
+	const INT32 lbreakheight = 21;
+	INT32 lines;
+
+	if (option & V_CENTERNAMETAG)
+	{
+		lines = V_CountNameTagLines(string);
+		y -= FixedInt(FixedMul(((lbreakheight/2) * (lines-1))*FRACUNIT, scale));
+	}
+
+	// No line breaks?
+	// Draw entire string
+	if (!last_token)
+		V_DrawNameTagLine(x, y, option, scale, basecolormap, outlinecolormap, string);
+	// Split string by the line break character
+	else
+	{
+		char *string = NULL;
+		INT32 len;
+		while (true)
+		{
+			// There are still lines left to draw
+			if (last_token)
+			{
+				size_t shift = 0;
+				// Free this line
+				if (string)
+					Z_Free(string);
+				// Find string length, do a malloc...
+				len = (last_token-first_token)+1;
+				string = ZZ_Alloc(len);
+				// Copy the line
+				strncpy(string, first_token, len-1);
+				string[len-1] = '\0';
+				// Don't leave a line break character
+				// at the start of the string!
+				if ((strlen(string) >= 2) && (string[0] == '\n') && (string[1] != '\n'))
+					shift++;
+				// Then draw it
+				V_DrawNameTagLine(x, y, option, scale, basecolormap, outlinecolormap, string+shift);
+			}
+			// No line break character was found
+			else
+			{
+				// Don't leave a line break character
+				// at the start of the string!
+				if ((strlen(first_token) >= 2) && (first_token[0] == '\n') && (first_token[1] != '\n'))
+					first_token++;
+				// Then draw it
+				V_DrawNameTagLine(x, y, option, scale, basecolormap, outlinecolormap, first_token);
+				break;
+			}
+
+			// Next line
+			y += FixedInt(FixedMul(lbreakheight*FRACUNIT, scale));
+			if ((last_token-text)+1 >= strlen(text))
+				last_token = NULL;
+			else
+			{
+				first_token = last_token;
+				last_token = strchr(first_token+1, '\n');
+			}
+		}
+		// Free this line
+		if (string)
+			Z_Free(string);
+	}
+}
+
+// Count the amount of lines in name tag string
+INT32 V_CountNameTagLines(const char *string)
+{
+	INT32 lines = 1;
+	char *text = (char *)string;
+	char *first_token = text;
+	char *last_token = strchr(text, '\n');
+
+	// No line breaks?
+	if (!last_token)
+		return lines;
+	// Split string by the line break character
+	else
+	{
+		while (true)
+		{
+			if (last_token)
+				lines++;
+			// No line break character was found
+			else
+				break;
+
+			// Next line
+			if ((last_token-text)+1 >= strlen(text))
+				last_token = NULL;
+			else
+			{
+				first_token = last_token;
+				last_token = strchr(first_token+1, '\n');
+			}
+		}
+	}
+	return lines;
+}
+
+INT32 V_NameTagWidth(const char *string)
+{
+	INT32 c, w = 0;
+	size_t i;
+
+	// It's possible for string to be a null pointer
+	if (!string)
+		return 0;
+
+	for (i = 0; i < strlen(string); i++)
+	{
+		c = toupper(string[i]) - NT_FONTSTART;
+		if (c < 0 || c >= NT_FONTSIZE || !ntb_font[c] || !nto_font[c])
+			w += 4;
+		else
+			w += SHORT(ntb_font[c]->width)+2;
+	}
+
+	return w;
+}
+
 // Find string width from cred_font chars
 //
 INT32 V_CreditStringWidth(const char *string)
diff --git a/src/v_video.h b/src/v_video.h
index ad1454cb73c4ae0140de8c98b3e7f313b197691d..01d50cd57957479dfe74700aba40212caa215996 100644
--- a/src/v_video.h
+++ b/src/v_video.h
@@ -112,6 +112,7 @@ extern RGBA_t *pMasterPalette;
 #define V_OFFSET             0x00400000 // account for offsets in patches
 #define V_ALLOWLOWERCASE     0x00800000 // (strings only) allow fonts that have lowercase letters to use them
 #define V_FLIP               0x00800000 // (patches only) Horizontal flip
+#define V_CENTERNAMETAG      0x00800000 // (nametag only) center nametag lines
 
 #define V_SNAPTOTOP          0x01000000 // for centering
 #define V_SNAPTOBOTTOM       0x02000000 // for centering
@@ -203,11 +204,12 @@ INT32 V_LevelNameHeight(const char *string);
 INT32 V_LevelActNumWidth(INT32 num); // act number width
 
 void V_DrawCreditString(fixed_t x, fixed_t y, INT32 option, const char *string);
+INT32 V_CreditStringWidth(const char *string);
 
 // Draw a string using the nt_font
-void V_DrawNameTag(INT32 x, INT32 y, INT32 option, UINT8 *basecolormap, UINT8 *outlinecolormap, const char *string);
-
-INT32 V_CreditStringWidth(const char *string);
+void V_DrawNameTag(INT32 x, INT32 y, INT32 option, fixed_t scale, UINT8 *basecolormap, UINT8 *outlinecolormap, const char *string);
+INT32 V_CountNameTagLines(const char *string);
+INT32 V_NameTagWidth(const char *string);
 
 // Find string width from hu_font chars
 INT32 V_StringWidth(const char *string, INT32 option);