diff --git a/source/common/engine/namedef.h b/source/common/engine/namedef.h
index 70080a965..e76186f21 100644
--- a/source/common/engine/namedef.h
+++ b/source/common/engine/namedef.h
@@ -182,6 +182,7 @@ xx(Voidptr)
 xx(StateLabel)
 xx(SpriteID)
 xx(TextureID)
+xx(TranslationID)
 xx(Overlay)
 xx(IsValid)
 xx(IsNull)
@@ -272,6 +273,7 @@ xx(BuiltinFRandom)
 xx(BuiltinNameToClass)
 xx(BuiltinClassCast)
 xx(BuiltinFunctionPtrCast)
+xx(BuiltinFindTranslation)
 
 xx(ScreenJobRunner)
 xx(Action)
diff --git a/source/common/engine/palettecontainer.cpp b/source/common/engine/palettecontainer.cpp
index 800dc31c6..40c73e4a4 100644
--- a/source/common/engine/palettecontainer.cpp
+++ b/source/common/engine/palettecontainer.cpp
@@ -820,4 +820,9 @@ bool FRemapTable::AddColors(int start, int count, const uint8_t*colors, int tran
 
 }
 
+// placeholder
+int R_FindCustomTranslation(FName name)
+{
+	return -1;
+}
 
diff --git a/source/common/engine/palettecontainer.h b/source/common/engine/palettecontainer.h
index a761ae33c..96413fe1f 100644
--- a/source/common/engine/palettecontainer.h
+++ b/source/common/engine/palettecontainer.h
@@ -42,6 +42,46 @@ struct FRemapTable
 private:
 };
 
+
+struct FTranslationID
+{
+public:
+	FTranslationID() = default;
+
+private:
+	constexpr FTranslationID(int id) : ID(id)
+	{
+	}
+public:
+	static constexpr FTranslationID fromInt(int i)
+	{
+		return FTranslationID(i);
+	}
+	FTranslationID(const FTranslationID& other) = default;
+	FTranslationID& operator=(const FTranslationID& other) = default;
+	bool operator !=(FTranslationID other) const
+	{
+		return ID != other.ID;
+	}
+	bool operator ==(FTranslationID other) const
+	{
+		return ID == other.ID;
+	}
+	bool operator ==(int other) const = delete;
+	bool operator !=(int other) const = delete;
+	constexpr int index() const
+	{
+		return ID;
+	}
+	constexpr bool isvalid() const
+	{
+		return ID > 0;
+	}
+private:
+
+	int ID;
+};
+
 // A class that initializes unusued pointers to NULL. This is used so that when
 // the TAutoGrowArray below is expanded, the new elements will be NULLed.
 class FRemapTablePtr
diff --git a/source/common/engine/sc_man_scanner.re b/source/common/engine/sc_man_scanner.re
index 8bca2ccc6..5af7e62f9 100644
--- a/source/common/engine/sc_man_scanner.re
+++ b/source/common/engine/sc_man_scanner.re
@@ -167,7 +167,7 @@ std2:
 		'vector2'					{ RET(TK_Vector2); }
 		'vector3'					{ RET(TK_Vector3); }
 		'map'						{ RET(TK_Map); }
-		'mapiterator'				{ RET(TK_MapIterator); }
+		'mapiterator'				{ RET(ParseVersion >= MakeVersion(4, 10, 0)? TK_MapIterator : TK_Identifier); }
 		'array'						{ RET(TK_Array); }
 		'function'					{ RET(ParseVersion >= MakeVersion(4, 12, 0)? TK_FunctionType : TK_Identifier); }
 		'in'						{ RET(TK_In); }
diff --git a/source/common/engine/serializer.cpp b/source/common/engine/serializer.cpp
index 34fd6956b..71a0031b6 100644
--- a/source/common/engine/serializer.cpp
+++ b/source/common/engine/serializer.cpp
@@ -1196,6 +1196,21 @@ FSerializer &Serialize(FSerializer &arc, const char *key, FTextureID &value, FTe
 	return arc;
 }
 
+//==========================================================================
+//
+//
+//
+//==========================================================================
+
+FSerializer& Serialize(FSerializer& arc, const char* key, FTranslationID& value, FTranslationID* defval)
+{
+	int v = value.index();
+	int* defv = (int*)defval;
+	Serialize(arc, key, v, defv);
+	value = FTranslationID::fromInt(v);
+	return arc;
+}
+
 //==========================================================================
 //
 // This never uses defval and instead uses 'null' as default
diff --git a/source/common/engine/serializer.h b/source/common/engine/serializer.h
index 9328f2f22..ec1d10949 100644
--- a/source/common/engine/serializer.h
+++ b/source/common/engine/serializer.h
@@ -20,6 +20,7 @@ class FSoundID;
 union FRenderStyle;
 class DObject;
 class FTextureID;
+struct FTranslationID;
 
 inline bool nullcmp(const void *buffer, size_t length)
 {
@@ -240,6 +241,7 @@ FSerializer &Serialize(FSerializer &arc, const char *key, FSoundID &sid, FSoundI
 FSerializer &Serialize(FSerializer &arc, const char *key, FString &sid, FString *def);
 FSerializer &Serialize(FSerializer &arc, const char *key, NumericValue &sid, NumericValue *def);
 FSerializer &Serialize(FSerializer &arc, const char *key, struct ModelOverride &sid, struct ModelOverride *def);
+FSerializer& Serialize(FSerializer& arc, const char* key, FTranslationID& value, FTranslationID* defval);
 
 void SerializeFunctionPointer(FSerializer &arc, const char *key, FunctionPointerValue *&p);
 
diff --git a/source/common/scripting/backend/codegen.cpp b/source/common/scripting/backend/codegen.cpp
index 3ff45c335..7f3a410ca 100644
--- a/source/common/scripting/backend/codegen.cpp
+++ b/source/common/scripting/backend/codegen.cpp
@@ -43,11 +43,13 @@
 #include "texturemanager.h"
 #include "m_random.h"
 #include "v_font.h"
+#include "palettecontainer.h"
 
 
 extern FRandom pr_exrandom;
 FMemArena FxAlloc(65536);
 CompileEnvironment compileEnvironment;
+int R_FindCustomTranslation(FName name);
 
 struct FLOP
 {
@@ -161,6 +163,12 @@ void FCompileContext::CheckReturn(PPrototype *proto, FScriptPosition &pos)
 			PType* expected = ReturnProto->ReturnTypes[i];
 			PType* actual = proto->ReturnTypes[i];
 			if (swapped) std::swap(expected, actual);
+			// this must pass for older ZScripts.
+			if (Version < MakeVersion(4, 12, 0))
+			{
+				if (expected == TypeTranslationID) expected = TypeSInt32;
+				if (actual == TypeTranslationID) actual = TypeSInt32;
+			}
 
 			if (expected != actual && !AreCompatiblePointerTypes(expected, actual))
 			{ // Incompatible
@@ -993,6 +1001,19 @@ FxExpression *FxIntCast::Resolve(FCompileContext &ctx)
 
 	if (basex->ValueType->GetRegType() == REGT_INT)
 	{
+		if (basex->ValueType == TypeTranslationID)
+		{
+			// translation IDs must be entirely incompatible with ints, not even allowing an explicit conversion, 
+			// but since the type was only introduced in version 4.12, older ZScript versions must allow this conversion.
+			if (ctx.Version < MakeVersion(4, 12, 0))
+			{
+				FxExpression* x = basex;
+				x->ValueType = ValueType;
+				basex = nullptr;
+				delete this;
+				return x;
+			}
+		}
 		if (basex->ValueType->isNumeric() || Explicit)	// names can be converted to int, but only with an explicit type cast.
 		{
 			FxExpression *x = basex;
@@ -1006,7 +1027,7 @@ FxExpression *FxIntCast::Resolve(FCompileContext &ctx)
 			// Ugh. This should abort, but too many mods fell into this logic hole somewhere, so this serious error needs to be reduced to a warning. :(
 			// At least in ZScript, MSG_OPTERROR always means to report an error, not a warning so the problem only exists in DECORATE.
 			if (!basex->isConstant())	
-				ScriptPosition.Message(MSG_OPTERROR, "Numeric type expected, got a name");
+				ScriptPosition.Message(MSG_OPTERROR, "Numeric type expected, got a %s", basex->ValueType->DescriptiveName());
 			else ScriptPosition.Message(MSG_OPTERROR, "Numeric type expected, got \"%s\"", static_cast<FxConstant*>(basex)->GetValue().GetName().GetChars());
 			FxExpression * x = new FxConstant(0, ScriptPosition);
 			delete this;
@@ -1127,7 +1148,8 @@ FxExpression *FxFloatCast::Resolve(FCompileContext &ctx)
 		{
 			// Ugh. This should abort, but too many mods fell into this logic hole somewhere, so this seroious error needs to be reduced to a warning. :(
 			// At least in ZScript, MSG_OPTERROR always means to report an error, not a warning so the problem only exists in DECORATE.
-			if (!basex->isConstant()) ScriptPosition.Message(MSG_OPTERROR, "Numeric type expected, got a name");
+			if (!basex->isConstant()) 
+				ScriptPosition.Message(MSG_OPTERROR, "Numeric type expected, got a %s", basex->ValueType->DescriptiveName());
 			else ScriptPosition.Message(MSG_OPTERROR, "Numeric type expected, got \"%s\"", static_cast<FxConstant*>(basex)->GetValue().GetName().GetChars());
 			FxExpression *x = new FxConstant(0.0, ScriptPosition);
 			delete this;
@@ -1534,6 +1556,107 @@ ExpEmit FxSoundCast::Emit(VMFunctionBuilder *build)
 	return to;
 }
 
+//==========================================================================
+//
+//
+//
+//==========================================================================
+
+FxTranslationCast::FxTranslationCast(FxExpression* x)
+	: FxExpression(EFX_TranslationCast, x->ScriptPosition)
+{
+	basex = x;
+	ValueType = TypeTranslationID;
+}
+
+//==========================================================================
+//
+//
+//
+//==========================================================================
+
+FxTranslationCast::~FxTranslationCast()
+{
+	SAFE_DELETE(basex);
+}
+
+//==========================================================================
+//
+//
+//
+//==========================================================================
+
+FxExpression* FxTranslationCast::Resolve(FCompileContext& ctx)
+{
+	CHECKRESOLVED();
+	SAFE_RESOLVE(basex, ctx);
+
+	if (basex->ValueType->isInt())
+	{
+		// 0 is a valid constant for translations, meaning 'no translation at all'. note that this conversion ONLY allows a constant!
+		if (basex->isConstant() && static_cast<FxConstant*>(basex)->GetValue().GetInt() == 0)
+		{
+			FxExpression* x = basex;
+			x->ValueType = TypeTranslationID;
+			basex = nullptr;
+			delete this;
+			return x;
+		}
+		if (ctx.Version < MakeVersion(4, 12, 0))
+		{
+			// only allow this conversion as a fallback
+			FxExpression* x = basex;
+			x->ValueType = TypeTranslationID;
+			basex = nullptr;
+			delete this;
+			return x;
+		}
+	}
+	else if (basex->ValueType == TypeString || basex->ValueType == TypeName)
+	{
+		if (basex->isConstant())
+		{
+			ExpVal constval = static_cast<FxConstant*>(basex)->GetValue();
+			FxExpression* x = new FxConstant(R_FindCustomTranslation(constval.GetName()), ScriptPosition);
+			x->ValueType = TypeTranslationID;
+			delete this;
+			return x;
+		}
+		else if (basex->ValueType == TypeString)
+		{
+			basex = new FxNameCast(basex, true);
+			basex = basex->Resolve(ctx);
+		}
+		return this;
+	}
+	ScriptPosition.Message(MSG_ERROR, "Cannot convert to translation ID");
+	delete this;
+	return nullptr;
+}
+
+//==========================================================================
+//
+//
+//
+//==========================================================================
+
+ExpEmit FxTranslationCast::Emit(VMFunctionBuilder* build)
+{
+	ExpEmit to(build, REGT_POINTER);
+
+	VMFunction* callfunc;
+	auto sym = FindBuiltinFunction(NAME_BuiltinFindTranslation);
+
+	assert(sym);
+	callfunc = sym->Variants[0].Implementation;
+
+	FunctionCallEmitter emitters(callfunc);
+	emitters.AddParameter(build, basex);
+	emitters.AddReturn(REGT_INT);
+	return emitters.EmitCall(build);
+}
+
+
 //==========================================================================
 //
 //
@@ -1766,6 +1889,14 @@ FxExpression *FxTypeCast::Resolve(FCompileContext &ctx)
 		delete this;
 		return x;
 	}
+	else if (ValueType == TypeTranslationID)
+	{
+		FxExpression* x = new FxTranslationCast(basex);
+		x = x->Resolve(ctx);
+		basex = nullptr;
+		delete this;
+		return x;
+	}
 	else if (ValueType == TypeColor)
 	{
 		FxExpression *x = new FxColorCast(basex);
@@ -4639,6 +4770,7 @@ ExpEmit FxConcat::Emit(VMFunctionBuilder *build)
 		else if (left->ValueType == TypeColor) cast = CAST_Co2S;
 		else if (left->ValueType == TypeSpriteID) cast = CAST_SID2S;
 		else if (left->ValueType == TypeTextureID) cast = CAST_TID2S;
+		else if (left->ValueType == TypeTranslationID) cast = CAST_U2S;
 		else if (op1.RegType == REGT_POINTER) cast = CAST_P2S;
 		else if (op1.RegType == REGT_INT) cast = CAST_I2S;
 		else assert(false && "Bad type for string concatenation");
@@ -4672,6 +4804,7 @@ ExpEmit FxConcat::Emit(VMFunctionBuilder *build)
 		else if (right->ValueType == TypeColor) cast = CAST_Co2S;
 		else if (right->ValueType == TypeSpriteID) cast = CAST_SID2S;
 		else if (right->ValueType == TypeTextureID) cast = CAST_TID2S;
+		else if (right->ValueType == TypeTranslationID) cast = CAST_U2S;
 		else if (op2.RegType == REGT_POINTER) cast = CAST_P2S;
 		else if (op2.RegType == REGT_INT) cast = CAST_I2S;
 		else assert(false && "Bad type for string concatenation");
@@ -5143,11 +5276,24 @@ FxExpression *FxConditional::Resolve(FCompileContext& ctx)
 		ValueType = truex->ValueType;
 	else if (falsex->IsPointer() && truex->ValueType == TypeNullPtr)
 		ValueType = falsex->ValueType;
+	// translation IDs need a bit of glue for compatibility and the 0 literal.
+	else if (truex->IsInteger() && falsex->ValueType == TypeTranslationID)
+	{
+		truex = new FxTranslationCast(truex);
+		truex = truex->Resolve(ctx);
+		ValueType = ctx.Version < MakeVersion(4, 12, 0)? TypeSInt32 : TypeTranslationID;
+	}
+	else if (falsex->IsInteger() && truex->ValueType == TypeTranslationID)
+	{
+		falsex = new FxTranslationCast(falsex);
+		falsex = falsex->Resolve(ctx);
+		ValueType = ctx.Version < MakeVersion(4, 12, 0) ? TypeSInt32 : TypeTranslationID;
+	}
+
 	else
 		ValueType = TypeVoid;
-	//else if (truex->ValueType != falsex->ValueType)
 
-	if (ValueType->GetRegType() == REGT_NIL)
+	if (truex == nullptr || falsex == nullptr || ValueType->GetRegType() == REGT_NIL)
 	{
 		ScriptPosition.Message(MSG_ERROR, "Incompatible types for ?: operator");
 		delete this;
@@ -8279,6 +8425,7 @@ FxExpression *FxFunctionCall::Resolve(FCompileContext& ctx)
 				MethodName == NAME_Name ? TypeName :
 				MethodName == NAME_SpriteID ? TypeSpriteID :
 				MethodName == NAME_TextureID ? TypeTextureID :
+				MethodName == NAME_TranslationID ? TypeTranslationID :
 				MethodName == NAME_State ? TypeState :
 				MethodName == NAME_Color ? TypeColor : (PType*)TypeSound;
 
diff --git a/source/common/scripting/backend/codegen.h b/source/common/scripting/backend/codegen.h
index 6aff3b75d..e1ad37856 100644
--- a/source/common/scripting/backend/codegen.h
+++ b/source/common/scripting/backend/codegen.h
@@ -232,6 +232,7 @@ enum EFxType
 	EFX_StringCast,
 	EFX_ColorCast,
 	EFX_SoundCast,
+	EFX_TranslationCast,
 	EFX_TypeCast,
 	EFX_PlusSign,
 	EFX_MinusSign,
@@ -715,6 +716,19 @@ public:
 	ExpEmit Emit(VMFunctionBuilder *build);
 };
 
+class FxTranslationCast : public FxExpression
+{
+	FxExpression* basex;
+
+public:
+
+	FxTranslationCast(FxExpression* x);
+	~FxTranslationCast();
+	FxExpression* Resolve(FCompileContext&);
+
+	ExpEmit Emit(VMFunctionBuilder* build);
+};
+
 class FxFontCast : public FxExpression
 {
 	FxExpression *basex;
diff --git a/source/common/scripting/core/types.cpp b/source/common/scripting/core/types.cpp
index efe4dfec5..b9949a5ea 100644
--- a/source/common/scripting/core/types.cpp
+++ b/source/common/scripting/core/types.cpp
@@ -41,6 +41,7 @@
 #include "printf.h"
 #include "textureid.h"
 #include "maps.h"
+#include "palettecontainer.h"
 
 
 FTypeTable TypeTable;
@@ -58,6 +59,7 @@ PName *TypeName;
 PSound *TypeSound;
 PColor *TypeColor;
 PTextureID *TypeTextureID;
+PTranslationID* TypeTranslationID;
 PSpriteID *TypeSpriteID;
 PStatePointer *TypeState;
 PPointer *TypeFont;
@@ -322,6 +324,7 @@ void PType::StaticInit()
 	TypeTable.AddType(TypeNullPtr = new PPointer, NAME_Pointer);
 	TypeTable.AddType(TypeSpriteID = new PSpriteID, NAME_SpriteID);
 	TypeTable.AddType(TypeTextureID = new PTextureID, NAME_TextureID);
+	TypeTable.AddType(TypeTranslationID = new PTranslationID, NAME_TranslationID);
 
 	TypeVoidPtr = NewPointer(TypeVoid, false);
 	TypeRawFunction = new PPointer;
@@ -1321,6 +1324,48 @@ bool PTextureID::ReadValue(FSerializer &ar, const char *key, void *addr) const
 	return true;
 }
 
+/* PTranslationID ******************************************************************/
+
+//==========================================================================
+//
+// PTranslationID Default Constructor
+//
+//==========================================================================
+
+PTranslationID::PTranslationID()
+	: PInt(sizeof(FTranslationID), true, false)
+{
+	mDescriptiveName = "TranslationID";
+	Flags |= TYPE_IntNotInt;
+	static_assert(sizeof(FTranslationID) == alignof(FTranslationID), "TranslationID not properly aligned");
+}
+
+//==========================================================================
+//
+// PTranslationID :: WriteValue
+//
+//==========================================================================
+
+void PTranslationID::WriteValue(FSerializer& ar, const char* key, const void* addr) const
+{
+	FTranslationID val = *(FTranslationID*)addr;
+	ar(key, val);
+}
+
+//==========================================================================
+//
+// PTranslationID :: ReadValue
+//
+//==========================================================================
+
+bool PTranslationID::ReadValue(FSerializer& ar, const char* key, void* addr) const
+{
+	FTranslationID val;
+	ar(key, val);
+	*(FTranslationID*)addr = val;
+	return true;
+}
+
 /* PSound *****************************************************************/
 
 //==========================================================================
diff --git a/source/common/scripting/core/types.h b/source/common/scripting/core/types.h
index 701244ad5..369df05c8 100644
--- a/source/common/scripting/core/types.h
+++ b/source/common/scripting/core/types.h
@@ -383,6 +383,15 @@ public:
 	bool ReadValue(FSerializer &ar, const char *key, void *addr) const override;
 };
 
+class PTranslationID : public PInt
+{
+public:
+	PTranslationID();
+
+	void WriteValue(FSerializer& ar, const char* key, const void* addr) const override;
+	bool ReadValue(FSerializer& ar, const char* key, void* addr) const override;
+};
+
 class PColor : public PInt
 {
 public:
@@ -712,6 +721,7 @@ extern PName *TypeName;
 extern PSound *TypeSound;
 extern PColor *TypeColor;
 extern PTextureID *TypeTextureID;
+extern PTranslationID* TypeTranslationID;
 extern PSpriteID *TypeSpriteID;
 extern PStruct* TypeVector2;
 extern PStruct* TypeVector3;
diff --git a/source/common/scripting/frontend/zcc_compile.cpp b/source/common/scripting/frontend/zcc_compile.cpp
index b8827ebae..c01935b44 100644
--- a/source/common/scripting/frontend/zcc_compile.cpp
+++ b/source/common/scripting/frontend/zcc_compile.cpp
@@ -1874,6 +1874,12 @@ PType *ZCCCompiler::DetermineType(PType *outertype, ZCC_TreeNode *field, FName n
 				retval = TypeTextureID;
 				break;
 
+			case NAME_TranslationID:
+				retval = TypeTranslationID;
+				break;
+
+
+
 			default:
 				retval = ResolveUserType(btype, btype->UserType, outertype ? &outertype->Symbols : nullptr, false);
 				break;
diff --git a/source/common/scripting/interface/vmnatives.cpp b/source/common/scripting/interface/vmnatives.cpp
index 5ab3051d4..6242d556b 100644
--- a/source/common/scripting/interface/vmnatives.cpp
+++ b/source/common/scripting/interface/vmnatives.cpp
@@ -1372,3 +1372,38 @@ DEFINE_ACTION_FUNCTION_NATIVE(DObject, FindFunction, FindFunctionPointer)
 	ACTION_RETURN_POINTER(FindFunctionPointer(cls, fn.GetIndex()));
 }
 
+int R_FindCustomTranslation(FName name);
+
+static int ZFindTranslation(int intname)
+{
+	return R_FindCustomTranslation(ENamedName(intname));
+}
+
+static int MakeTransID(int g, int s)
+{
+	return TRANSLATION(g, s);
+}
+
+DEFINE_ACTION_FUNCTION_NATIVE(_Translation, GetID, ZFindTranslation)
+{
+	PARAM_PROLOGUE;
+	PARAM_INT(t);
+	ACTION_RETURN_INT(ZFindTranslation(t));
+}
+
+// same as above for the compiler which needs a class to look this up.
+DEFINE_ACTION_FUNCTION_NATIVE(DObject, BuiltinFindTranslation, ZFindTranslation)
+{
+	PARAM_PROLOGUE;
+	PARAM_INT(t);
+	ACTION_RETURN_INT(ZFindTranslation(t));
+}
+
+DEFINE_ACTION_FUNCTION_NATIVE(_Translation, MakeID, MakeTransID)
+{
+	PARAM_PROLOGUE;
+	PARAM_INT(g);
+	PARAM_INT(t);
+	ACTION_RETURN_INT(MakeTransID(g, t));
+}
+
diff --git a/source/core/version.h b/source/core/version.h
index 1e195b3b6..90acfefda 100644
--- a/source/core/version.h
+++ b/source/core/version.h
@@ -49,7 +49,7 @@ const char *GetVersionString();
 #define RC_PRODUCTVERSION2 VERSIONSTR
 // These are for content versioning.
 #define VER_MAJOR 4
-#define VER_MINOR 10
+#define VER_MINOR 12
 #define VER_REVISION 0
 
 #define ENG_MAJOR 1
diff --git a/wadsrc/static/zscript.txt b/wadsrc/static/zscript.txt
index bddab7c60..4df9e5ca5 100644
--- a/wadsrc/static/zscript.txt
+++ b/wadsrc/static/zscript.txt
@@ -1,10 +1,14 @@
-version "4.10"
+version "4.12"
+
+// Generic engine code
 #include "zscript/engine/base.zs"
 #include "zscript/engine/dynarrays.zs"
-#include "zscript/engine/inputevents.zs"
+#include "zscript/engine/maps.zs"
 #include "zscript/engine/dictionary.zs"
-#include "zscript/engine/screenjob.zs"
+#include "zscript/engine/inputevents.zs"
+#include "zscript/engine/service.zs"
 #include "zscript/engine/ppshader.zs"
+#include "zscript/engine/screenjob.zs"
 
 #include "zscript/engine/ui/menu/colorpickermenu.zs"
 #include "zscript/engine/ui/menu/joystickmenu.zs"
diff --git a/wadsrc/static/zscript/alt_hud.zs b/wadsrc/static/zscript/alt_hud.zs
index d7060a6ff..25bccc490 100644
--- a/wadsrc/static/zscript/alt_hud.zs
+++ b/wadsrc/static/zscript/alt_hud.zs
@@ -53,7 +53,7 @@ struct HudStats
 	int ammoselect;
 	
 	Array<String> keyicons;
-	Array<int> keytranslations;
+	Array<TranslationID> keytranslations;
 
 	Array<String> inventoryicons;
 	Array<int> inventoryamounts;	// negative values can be used for special states (-1: "ON", -2: "OFF", -3: "AUTO")
@@ -128,7 +128,7 @@ class AltHud ui
 	//
 	//---------------------------------------------------------------------------
 
-	void DrawImageToBox(TextureID tex, int x, int y, int w, int h, double trans = 0.75, bool animate = false, int translation = 0)
+	void DrawImageToBox(TextureID tex, int x, int y, int w, int h, double trans = 0.75, bool animate = false, TranslationID translation = 0)
 	{
 		double scale1, scale2;
 
@@ -328,7 +328,7 @@ class AltHud ui
 
 		if (icon.isValid())
 		{
-			int trans = 0;
+			TranslationID trans = 0;
 			if (currentStats.keytranslations.Size()) trans = currentStats.keytranslations[keyindex];
 			DrawImageToBox(icon, x, y, 8, 10, 0.75, false, trans);
 			return true;
diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs
index be1b43ade..b9c27de55 100644
--- a/wadsrc/static/zscript/engine/base.zs
+++ b/wadsrc/static/zscript/engine/base.zs
@@ -184,6 +184,7 @@ struct Vector3
 struct _ native	// These are the global variables, the struct is only here to avoid extending the parser for this.
 {
 	native readonly Array<class> AllClasses;
+    native internal readonly Map<Name , Service> AllServices;
 	native readonly bool multiplayer;
 	native @KeyBindings Bindings;
 	native @KeyBindings AutomapBindings;
@@ -752,6 +753,7 @@ class Object native
 	private native static void BuiltinRandomSeed(voidptr rng, int seed);
 	private native static Class<Object> BuiltinNameToClass(Name nm, Class<Object> filter);
 	private native static Object BuiltinClassCast(Object inptr, Class<Object> test);
+	private native static Function<void> BuiltinFunctionPtrCast(Function<void> inptr, voidptr newtype);
 	
 	native static uint MSTime();
 	native static double MSTimeF();
@@ -922,10 +924,11 @@ struct StringStruct native
 
 struct Translation version("2.4")
 {
-	static int MakeID(int group, int num)
-	{
-		return (group << 16) + num;
-	}
+	Color colors[256];
+	
+	//native TranslationID AddTranslation(); // this still needs work.
+	native static TranslationID MakeID(int group, int num);
+	native static TranslationID GetID(Name transname);
 }
 
 // Convenient way to attach functions to Quat
diff --git a/wadsrc/static/zscript/engine/screenjob.zs b/wadsrc/static/zscript/engine/screenjob.zs
index a65516a48..5dd2018eb 100644
--- a/wadsrc/static/zscript/engine/screenjob.zs
+++ b/wadsrc/static/zscript/engine/screenjob.zs
@@ -141,12 +141,12 @@ class BlackScreen : ScreenJob
 class ImageScreen : SkippableScreenJob
 {
 	int tilenum;
-	int trans;
+	TranslationID trans;
 	int waittime; // in ms.
 	bool cleared;
 	TextureID texid;
 
-	ScreenJob Init(TextureID tile, int fade = fadein | fadeout, int wait = 3000, int translation = 0)
+	ScreenJob Init(TextureID tile, int fade = fadein | fadeout, int wait = 3000, TranslationID translation = 0)
 	{
 		Super.Init(fade);
 		waittime = wait;
@@ -156,7 +156,7 @@ class ImageScreen : SkippableScreenJob
 		return self;
 	}
 
-	ScreenJob InitNamed(String tex, int fade = fadein | fadeout, int wait = 3000, int translation = 0)
+	ScreenJob InitNamed(String tex, int fade = fadein | fadeout, int wait = 3000, TranslationID translation = 0)
 	{
 		Super.Init(fade);
 		waittime = wait;
@@ -166,12 +166,12 @@ class ImageScreen : SkippableScreenJob
 		return self;
 	}
 
-	static ScreenJob Create(TextureID tile, int fade = fadein | fadeout, int wait = 3000, int translation = 0)
+	static ScreenJob Create(TextureID tile, int fade = fadein | fadeout, int wait = 3000, TranslationID translation = 0)
 	{
 		return new("ImageScreen").Init(tile, fade, wait, translation);
 	}
 
-	static ScreenJob CreateNamed(String tex, int fade = fadein | fadeout, int wait = 3000, int translation = 0)
+	static ScreenJob CreateNamed(String tex, int fade = fadein | fadeout, int wait = 3000, TranslationID translation = 0)
 	{
 		return new("ImageScreen").InitNamed(tex, fade, wait, translation);
 	}
@@ -622,13 +622,13 @@ class TextOverlay
 {
 	int nHeight;
 	double nCrawlY;
-	int palette;
+	TranslationID palette;
 	int crange;
 	bool drawclean;
 	BrokenLines screentext;
 	Font myfont;
 
-	void Init(String text, int cr = Font.CR_NATIVEPAL, int pal = 0, bool clean = false)
+	void Init(String text, int cr = Font.CR_NATIVEPAL, TranslationID pal = 0, bool clean = false)
 	{
 		myfont = SmallFont; // todo
 		screentext = myfont.BreakLines(StringTable.Localize(text), 320);
diff --git a/wadsrc/static/zscript/engine/service.zs b/wadsrc/static/zscript/engine/service.zs
new file mode 100644
index 000000000..6df17f126
--- /dev/null
+++ b/wadsrc/static/zscript/engine/service.zs
@@ -0,0 +1,155 @@
+/**
+ * This is Service interface.
+ */
+class Service abstract
+{
+	deprecated("4.6.1", "Use GetString() instead") virtual play String Get(String request)
+	{
+		return "";
+	}
+
+	deprecated("4.6.1", "Use GetStringUI() instead") virtual ui String UiGet(String request)
+	{
+		return "";
+	}
+
+	// Play variants
+	virtual play String GetString(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return "";
+	}
+
+	virtual play int GetInt(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return 0;
+	}
+
+	virtual play double GetDouble(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return 0.0;
+	}
+
+	virtual play Object GetObject(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return null;
+	}
+
+	virtual play Name GetName(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return '';
+	}
+
+
+	// UI variants
+	virtual ui String GetStringUI(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return "";
+	}
+
+	virtual ui int GetIntUI(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return 0;
+	}
+
+	virtual ui double GetDoubleUI(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return 0.0;
+	}
+
+	virtual ui Object GetObjectUI(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return null;
+	}
+
+	virtual ui Name GetNameUI(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return '';
+	}
+
+	// data/clearscope variants
+	virtual clearscope String GetStringData(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return "";
+	}
+
+	virtual clearscope int GetIntData(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return 0;
+	}
+
+	virtual clearscope double GetDoubleData(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return 0.0;
+	}
+
+	virtual clearscope Object GetObjectData(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return null;
+	}
+
+	virtual clearscope Name GetNameData(String request, string stringArg = "", int intArg = 0, double doubleArg = 0, Object objectArg = null, Name nameArg = '')
+	{
+		return '';
+	}
+    
+    static Service Find(class<Service> serviceName){
+        return AllServices.GetIfExists(serviceName.GetClassName());
+    }
+}
+
+/**
+ * Use this class to find and iterate over services.
+ *
+ * Example usage:
+ *
+ * @code
+ * ServiceIterator i = ServiceIterator.Find("MyService");
+ *
+ * Service s;
+ * while (s = i.Next())
+ * {
+ * 	String request = ...
+ * 	String answer  = s.Get(request);
+ * 	...
+ * }
+ * @endcode
+ *
+ * If no services are found, the all calls to Next() will return NULL.
+ */
+class ServiceIterator
+{
+	/**
+	 * Creates a Service iterator for a service name. It will iterate over all existing Services
+	 * with names that match @a serviceName or have it as a part of their names.
+	 *
+	 * Matching is case-independent.
+	 *
+	 * @param serviceName class name of service to find.
+	 */
+	static ServiceIterator Find(String serviceName)
+	{
+		let result = new("ServiceIterator");
+		result.mServiceName = serviceName.MakeLower();
+		result.it.Init(AllServices);
+		return result;
+	}
+
+	/**
+	 * Gets the service and advances the iterator.
+	 *
+	 * @returns service instance, or NULL if no more services found.
+	 */
+	Service Next()
+	{
+		while(it.Next())
+		{
+			String cName = it.GetKey();
+			if(cName.MakeLower().IndexOf(mServiceName) != -1)
+				return it.GetValue();
+		}
+		return null;
+	}
+
+	private MapIterator<Name, Service> it;
+	private String mServiceName;
+}
diff --git a/wadsrc/static/zscript/engine/ui/menu/menu.zs b/wadsrc/static/zscript/engine/ui/menu/menu.zs
index 1c1877041..488629b5d 100644
--- a/wadsrc/static/zscript/engine/ui/menu/menu.zs
+++ b/wadsrc/static/zscript/engine/ui/menu/menu.zs
@@ -36,7 +36,7 @@
 struct KeyBindings native version("2.4")
 {
 	native static String NameKeys(int k1, int k2);
-	native static String NameAllKeys(array<int> list);
+	native static String NameAllKeys(array<int> list, bool colors = true);
 
 	native int, int GetKeysForCommand(String cmd);
 	native void GetAllKeysForCommand(out array<int> list, String cmd);
@@ -349,16 +349,16 @@ class Menu : Object native ui version("2.4")
 		return OptionFont().GetHeight();
 	}
 
-	static int OptionWidth(String s)
+	static int OptionWidth(String s, bool localize = true)
 	{
-		return OptionFont().StringWidth(s);
+		return OptionFont().StringWidth(s, localize);
 	}
 
-	static void DrawOptionText(int x, int y, int color, String text, bool grayed = false)
+	static void DrawOptionText(int x, int y, int color, String text, bool grayed = false, bool localize = true)
 	{
-		String label = Stringtable.Localize(text);
+		String label = localize ? Stringtable.Localize(text) : text;
 		int overlay = grayed? Color(96,48,0,0) : 0;
-		screen.DrawText (OptionFont(), color, x, y, text, DTA_CleanNoMove_1, true, DTA_ColorOverlay, overlay);
+		screen.DrawText (OptionFont(), color, x, y, text, DTA_CleanNoMove_1, true, DTA_ColorOverlay, overlay, DTA_Localize, localize);
 	}
 
 
diff --git a/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs b/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs
index 4733d5b3b..898dd1fac 100644
--- a/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs
+++ b/wadsrc/static/zscript/engine/ui/menu/optionmenuitems.zs
@@ -61,9 +61,9 @@ class OptionMenuItem : MenuItemBase
 		return x;
 	}
 
-	protected void drawValue(int indent, int y, int color, String text, bool grayed = false)
+	protected void drawValue(int indent, int y, int color, String text, bool grayed = false, bool localize = true)
 	{
-		Menu.DrawOptionText(indent + CursorSpace(), y, color, text, grayed);
+		Menu.DrawOptionText(indent + CursorSpace(), y, color, text, grayed, localize);
 	}
 
 
@@ -999,7 +999,7 @@ class OptionMenuFieldBase : OptionMenuItem
 	{
 		bool grayed = mGrayCheck != null && !mGrayCheck.GetInt();
 		drawLabel(indent, y, selected ? OptionMenuSettings.mFontColorSelection : OptionMenuSettings.mFontColor, grayed);
-		drawValue(indent, y, OptionMenuSettings.mFontColorValue, Represent(), grayed);
+		drawValue(indent, y, OptionMenuSettings.mFontColorValue, Represent(), grayed, false);
 		return indent;
 	}
 
@@ -1068,7 +1068,7 @@ class OptionMenuItemTextField : OptionMenuFieldBase
 		{
 			// reposition the text so that the cursor is visible when in entering mode.
 			String text = Represent();
-			int tlen = Menu.OptionWidth(text) * CleanXfac_1;
+			int tlen = Menu.OptionWidth(text, false) * CleanXfac_1;
 			int newindent = screen.GetWidth() - tlen - CursorSpace();
 			if (newindent < indent) indent = newindent;
 		}
diff --git a/wadsrc/static/zscript/engine/ui/statusbar/statusbarcore.zs b/wadsrc/static/zscript/engine/ui/statusbar/statusbarcore.zs
index b00af3fde..bbcf23343 100644
--- a/wadsrc/static/zscript/engine/ui/statusbar/statusbarcore.zs
+++ b/wadsrc/static/zscript/engine/ui/statusbar/statusbarcore.zs
@@ -108,13 +108,13 @@ class StatusBarCore native ui
 
 	native static String FormatNumber(int number, int minsize = 0, int maxsize = 0, int format = 0, String prefix = "");
 	native double, double, double, double StatusbarToRealCoords(double x, double y=0, double w=0, double h=0);
-	native void DrawTexture(TextureID texture, Vector2 pos, int flags = 0, double Alpha = 1., Vector2 box = (-1, -1), Vector2 scale = (1, 1), ERenderStyle style = STYLE_Translucent, Color col = 0xffffffff, int translation = 0, double clipwidth = -1);
-	native void DrawImage(String texture, Vector2 pos, int flags = 0, double Alpha = 1., Vector2 box = (-1, -1), Vector2 scale = (1, 1), ERenderStyle style = STYLE_Translucent, Color col = 0xffffffff, int translation = 0, double clipwidth = -1);
+	native void DrawTexture(TextureID texture, Vector2 pos, int flags = 0, double Alpha = 1., Vector2 box = (-1, -1), Vector2 scale = (1, 1), ERenderStyle style = STYLE_Translucent, Color col = 0xffffffff, TranslationID translation = 0, double clipwidth = -1);
+	native void DrawImage(String texture, Vector2 pos, int flags = 0, double Alpha = 1., Vector2 box = (-1, -1), Vector2 scale = (1, 1), ERenderStyle style = STYLE_Translucent, Color col = 0xffffffff, TranslationID translation = 0, double clipwidth = -1);
 
-	native void DrawTextureRotated(TextureID texid, Vector2 pos, int flags, double angle, double alpha = 1, Vector2 scale = (1, 1), ERenderStyle style = STYLE_Translucent, Color col = 0xffffffff, int translation = 0);
-	native void DrawImageRotated(String texid, Vector2 pos, int flags, double angle, double alpha = 1, Vector2 scale = (1, 1), ERenderStyle style = STYLE_Translucent, Color col = 0xffffffff, int translation = 0);
+	native void DrawTextureRotated(TextureID texid, Vector2 pos, int flags, double angle, double alpha = 1, Vector2 scale = (1, 1), ERenderStyle style = STYLE_Translucent, Color col = 0xffffffff, TranslationID translation = 0);
+	native void DrawImageRotated(String texid, Vector2 pos, int flags, double angle, double alpha = 1, Vector2 scale = (1, 1), ERenderStyle style = STYLE_Translucent, Color col = 0xffffffff, TranslationID translation = 0);
 
-	native void DrawString(HUDFont font, String string, Vector2 pos, int flags = 0, int translation = Font.CR_UNTRANSLATED, double Alpha = 1., int wrapwidth = -1, int linespacing = 4, Vector2 scale = (1, 1), int pt = 0, ERenderStyle style = STYLE_Translucent);
+	native void DrawString(HUDFont font, String string, Vector2 pos, int flags = 0, int translation = Font.CR_UNTRANSLATED, double Alpha = 1., int wrapwidth = -1, int linespacing = 4, Vector2 scale = (1, 1), TranslationID pt = 0, ERenderStyle style = STYLE_Translucent);
 	native double, double, double, double TransformRect(double x, double y, double w, double h, int flags = 0);
 	native void Fill(Color col, double x, double y, double w, double h, int flags = 0);
 	native void SetClipRect(double x, double y, double w, double h, int flags = 0);
diff --git a/wadsrc/static/zscript/games/blood/ui/menu.zs b/wadsrc/static/zscript/games/blood/ui/menu.zs
index 5f5b9054f..7cbff7baa 100644
--- a/wadsrc/static/zscript/games/blood/ui/menu.zs
+++ b/wadsrc/static/zscript/games/blood/ui/menu.zs
@@ -62,7 +62,7 @@ class ListMenuItemBloodTextItem : ListMenuItemTextItem
 		let gamefont = Raze.PickBigFont();
 		int xpos = mXpos - gamefont.StringWidth(mText) / 2;
 		int cr = generic_ui? Font.CR_GRAY : Font.CR_NATIVEPAL;
-		int trans = generic_ui? 0 : Translation.MakeID(Translation_Remap, pal);
+		TranslationID trans = generic_ui? 0 : Translation.MakeID(Translation_Remap, pal);
 
 		if (selected) shade = 32 - ((MSTime() * 120 / 1000) & 63);
 
diff --git a/wadsrc/static/zscript/games/blood/ui/sbar.zs b/wadsrc/static/zscript/games/blood/ui/sbar.zs
index 86bb9baab..a3b5a6c5a 100644
--- a/wadsrc/static/zscript/games/blood/ui/sbar.zs
+++ b/wadsrc/static/zscript/games/blood/ui/sbar.zs
@@ -232,7 +232,7 @@ class BloodStatusBar : RazeStatusBar
 					DrawTexture(Blood.PowerUpIcon(power), (x, y + powerYoffs[order]), DI_SCREEN_LEFT_CENTER | DI_ITEM_RELCENTER, scale:(powerScale[order], powerScale[order]));
 				}
 
-				DrawStatNumber("%d", remainingSeconds, "SBarNumberInv", x + 15, y, 0, remainingSeconds > warningTime ? 0 : Translation.MakeID(Translation_Remap, 2), 0.5, DI_SCREEN_LEFT_CENTER);
+				DrawStatNumber("%d", remainingSeconds, "SBarNumberInv", x + 15, y, 0, remainingSeconds > warningTime ? 0 : 2, 0.5, DI_SCREEN_LEFT_CENTER);
 				y += 20;
 			}
 		}
@@ -452,8 +452,8 @@ class BloodStatusBar : RazeStatusBar
 		}
 
 		bool meHaveBlueFlag = pPlayer.hasFlag & 1;
-		int trans10 = Translation.MakeID(Translation_Remap, 10);
-		int trans2 = Translation.MakeID(Translation_Remap, 2);
+		let trans10 = Translation.MakeID(Translation_Remap, 10);
+		let trans2 = Translation.MakeID(Translation_Remap, 2);
 		DrawImage(meHaveBlueFlag ? "FlagHave" : "FlagHaveNot", (0, 75 - 100), DI_SCREEN_RIGHT_CENTER|DI_ITEM_RELCENTER, scale:(0.35, 0.35), translation:trans10);
 		if (gBlueFlagDropped)
 			DrawImage("FlagDropped", (305 - 320, 83 - 100), DI_SCREEN_RIGHT_CENTER|DI_ITEM_RELCENTER, translation:trans10);
@@ -507,7 +507,7 @@ class BloodStatusBar : RazeStatusBar
 	//
 	//---------------------------------------------------------------------------
 
-	int DrawStatusBar(BloodPlayer pPlayer, int nPalette)
+	int DrawStatusBar(BloodPlayer pPlayer, TranslationID nPalette)
 	{
 		int th = texHeight("Statusbar");
 		BeginStatusBar(false, 320, 200, th);
@@ -583,7 +583,7 @@ class BloodStatusBar : RazeStatusBar
 	//
 	//---------------------------------------------------------------------------
 
-	int DrawHUD1(BloodPlayer pPlayer, int nPalette)
+	int DrawHUD1(BloodPlayer pPlayer, TranslationID nPalette)
 	{
 		BeginHUD(1, false, 320, 200);
 		DrawImage("FullHUD", (34, 187 - 200), DI_ITEM_RELCENTER, style:STYLE_Normal, col:0xffc0c0c0, translation:nPalette);
@@ -726,19 +726,20 @@ class BloodStatusBar : RazeStatusBar
 
 	override void UpdateStatusBar(SummaryInfo summary)
 	{
-		int nPalette = 0;
+		TranslationID nPalette = 0;
 		let pPlayer = Blood.GetViewPlayer();
 		int y = 0;
 
 		int nGameType = Blood.getGameType();
 		if (nGameType == 3)
 		{
+			int pal;
 			if (pPlayer.teamId & 1)
-				nPalette = 7;
+				pal = 7;
 			else
-				nPalette = 10;
+				pal = 10;
 
-			nPalette = Translation.MakeID(Translation_Remap, nPalette);
+			nPalette = Translation.MakeID(Translation_Remap, pal);
 		}
 
 		if (hud_size == Hud_Full)
diff --git a/wadsrc/static/zscript/games/duke/ui/menu.zs b/wadsrc/static/zscript/games/duke/ui/menu.zs
index cf3fa8475..169a6bb9e 100644
--- a/wadsrc/static/zscript/games/duke/ui/menu.zs
+++ b/wadsrc/static/zscript/games/duke/ui/menu.zs
@@ -168,7 +168,7 @@ class ListMenuItemDukeTextItem : ListMenuItemTextItem
 	override void Draw(bool selected, ListMenuDescriptor desc)
 	{
 		let font = Raze.PickBigFont();
-		int trans = mColorSelected? Translation.MakeID(Translation_Remap, 1) : 0;
+		TranslationID trans = mColorSelected? Translation.MakeID(Translation_Remap, 1) : 0;
 		Color pe;
 		double scale = (gameinfo.gametype & GAMEFLAG_RRALL) ? 0.4 : 1.;
 		let xpos = 160 - font.StringWidth(mText) * scale * 0.5;
diff --git a/wadsrc/static/zscript/games/duke/ui/screens.zs b/wadsrc/static/zscript/games/duke/ui/screens.zs
index 295108823..dbdf5337c 100644
--- a/wadsrc/static/zscript/games/duke/ui/screens.zs
+++ b/wadsrc/static/zscript/games/duke/ui/screens.zs
@@ -55,7 +55,7 @@ class DRealmsScreen : SkippableScreenJob
 	override void Draw(double smoothratio)
 	{
 		let tex = TexMan.CheckForTexture("DREALMS"); 
-		int translation = TexMan.UseGamePalette(tex)? Translation.MakeID(Translation_BasePalette, Duke.DREALMSPAL) : 0;
+		TranslationID translation = TexMan.UseGamePalette(tex)? Translation.MakeID(Translation_BasePalette, Duke.DREALMSPAL) : 0;
 
 		screen.DrawTexture(tex, true, 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, DTA_TranslationIndex, translation, DTA_LegacyRenderStyle, STYLE_Normal);
 	}
@@ -116,11 +116,11 @@ class DukeTitleScreen : SkippableScreenJob
 	override void Draw(double smoothratio)
 	{
 		int clock = (ticks + smoothratio) * 120 / GameTicRate;
-		int etrans = Translation.MakeID(Translation_BasePalette, Duke.TITLEPAL);
+		TranslationID etrans = Translation.MakeID(Translation_BasePalette, Duke.TITLEPAL);
 
 		// Only translate if the image depends on the global palette.
 		let tex = TexMan.CheckForTexture("BETASCREEN"); 
-		int trans = TexMan.UseGamePalette(tex)? etrans : 0;
+		TranslationID trans = TexMan.UseGamePalette(tex)? etrans : 0;
 		screen.DrawTexture(tex, true, 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, DTA_TranslationIndex, trans, DTA_LegacyRenderStyle, STYLE_Normal);
 
 		double scale = clamp(clock - 120, 0, 60) / 64.;
@@ -251,10 +251,10 @@ class Episode1End1 : SkippableScreenJob
 
 	override void Draw(double sr)
 	{
-		int etrans = Translation.MakeID(Translation_BasePalette, Duke.ENDINGPAL);
+		TranslationID etrans = Translation.MakeID(Translation_BasePalette, Duke.ENDINGPAL);
 
 		let tex = TexMan.CheckForTexture("VICTORY1");
-		int trans = TexMan.UseGamePalette(tex)? etrans : 0;
+		TranslationID trans = TexMan.UseGamePalette(tex)? etrans : 0;
 		screen.DrawTexture(tex, false, 0, 50, DTA_FullscreenScale, FSMode_Fit320x200, DTA_TranslationIndex, trans, DTA_LegacyRenderStyle, STYLE_Normal, DTA_TopLeft, true);
 
 		if (bossani.isValid())
diff --git a/wadsrc/static/zscript/games/exhumed/ui/screens.zs b/wadsrc/static/zscript/games/exhumed/ui/screens.zs
index 2578eebed..de24a1660 100644
--- a/wadsrc/static/zscript/games/exhumed/ui/screens.zs
+++ b/wadsrc/static/zscript/games/exhumed/ui/screens.zs
@@ -462,7 +462,7 @@ class Cinema : SkippableScreenJob
 	TextureID cinematile;
 	int currentCinemaPalette;
 	int cdtrack;
-	int palette;
+	TranslationID palette;
 	bool done;
 
 	ScreenJob Init(String bgTexture, String text, int pal, int cdtrk)
diff --git a/wadsrc/static/zscript/games/sw/ui/screens.zs b/wadsrc/static/zscript/games/sw/ui/screens.zs
index 3274fa61c..dba7c260b 100644
--- a/wadsrc/static/zscript/games/sw/ui/screens.zs
+++ b/wadsrc/static/zscript/games/sw/ui/screens.zs
@@ -55,7 +55,7 @@ class SWDRealmsScreen : SkippableScreenJob
 	override void Draw(double sm)
 	{
 		let tex = TexMan.CheckForTexture("THREED_REALMS_PIC", TexMan.Type_Any);
-		int translation = TexMan.UseGamePalette(tex) ? Translation.MakeID(Translation_BasePalette, DREALMSPAL) : 0;
+		TranslationID translation = TexMan.UseGamePalette(tex) ? Translation.MakeID(Translation_BasePalette, DREALMSPAL) : 0;
 		Screen.DrawTexture(tex, false, 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, DTA_TranslationIndex, translation, DTA_LegacyRenderStyle, STYLE_Normal);
 	}
 }