From 85c82184417c1cb9fe4919d16304fbda67b576cc Mon Sep 17 00:00:00 2001
From: Randy Heit <rheit@users.noreply.github.com>
Date: Sat, 2 Apr 2016 23:10:43 -0500
Subject: [PATCH] Added methods for PTypes to serialize their values

- Values are tagged to allow for some measure of changing variable types
  without automatically breaking savegames.
- Use these new methods to serialize the non-native variables in an
  object. This allows for achiving non-ints.
---
 src/dobject.cpp  |  45 +--
 src/dobjtype.cpp | 894 +++++++++++++++++++++++++++++++++++++++++++++++
 src/dobjtype.h   |  85 +++++
 src/farchive.cpp |  36 +-
 src/farchive.h   |   7 +
 src/version.h    |   2 +-
 6 files changed, 1027 insertions(+), 42 deletions(-)

diff --git a/src/dobject.cpp b/src/dobject.cpp
index efb437072..d7e23bdb0 100644
--- a/src/dobject.cpp
+++ b/src/dobject.cpp
@@ -417,46 +417,15 @@ void DObject::SerializeUserVars(FArchive &arc)
 
 	if (arc.IsStoring())
 	{
-		// Write all user variables.
-		for (; symt != NULL; symt = symt->ParentSymbolTable)
-		{
-			PSymbolTable::MapType::Iterator it(symt->Symbols);
-			PSymbolTable::MapType::Pair *pair;
-
-			while (it.NextPair(pair))
-			{
-				PField *var = dyn_cast<PField>(pair->Value);
-				if (var != NULL && !(var->Flags & VARF_Native))
-				{
-					PType *type = var->Type;
-					PArray *arraytype = dyn_cast<PArray>(type);
-					if (arraytype == NULL)
-					{
-						count = 1;
-					}
-					else
-					{
-						count = arraytype->ElementCount;
-						type = arraytype->ElementType;
-					}
-					assert(type == TypeSInt32);
-					varloc = (int *)(reinterpret_cast<BYTE *>(this) + var->Offset);
-
-					arc << var->SymbolName;
-					arc.WriteCount(count);
-					for (j = 0; j < count; ++j)
-					{
-						arc << varloc[j];
-					}
-				}
-			}
-		}
-		// Write terminator.
-		varname = NAME_None;
-		arc << varname;
+		// Write all fields that aren't serialized by native code.
+		GetClass()->WriteValue(arc, this);
+	}
+	else if (SaveVersion >= 4535)
+	{
+		GetClass()->ReadValue(arc, this);
 	}
 	else
-	{
+	{ // Old version that only deals with ints
 		// Read user variables until 'None' is encountered.
 		arc << varname;
 		while (varname != NAME_None)
diff --git a/src/dobjtype.cpp b/src/dobjtype.cpp
index 028b9e82b..1d781c02c 100644
--- a/src/dobjtype.cpp
+++ b/src/dobjtype.cpp
@@ -39,6 +39,7 @@
 
 #include "dobject.h"
 #include "i_system.h"
+#include "farchive.h"
 #include "actor.h"
 #include "templates.h"
 #include "autosegs.h"
@@ -384,6 +385,121 @@ bool PType::VisitedNodeSet::Check(const PType *node)
 	return false;
 }
 
+//==========================================================================
+//
+// PType :: WriteValue
+//
+//==========================================================================
+
+void PType::WriteValue(FArchive &ar, const void *addr) const
+{
+	assert(0 && "Cannot write value for this type");
+}
+
+//==========================================================================
+//
+// PType :: ReadValue
+//
+//==========================================================================
+
+bool PType::ReadValue(FArchive &ar, void *addr) const
+{
+	assert(0 && "Cannot read value for this type");
+	SkipValue(ar);
+	return false;
+}
+
+//==========================================================================
+//
+// PType :: SkipValue												STATIC
+//
+//==========================================================================
+
+void PType::SkipValue(FArchive &ar)
+{
+	BYTE tag;
+	ar << tag;
+	SkipValue(ar, tag);
+}
+
+void PType::SkipValue(FArchive &ar, int tag)
+{
+	assert(ar.IsLoading() && "SkipValue passed an archive that is writing");
+	BYTE buff[8];
+
+	switch (tag)
+	{
+	case VAL_Zero: case VAL_One:
+		break;
+
+	case VAL_Int8: case VAL_UInt8:
+		ar.Read(buff, 1);
+		break;
+
+	case VAL_Int16: case VAL_UInt16:
+		ar.Read(buff, 2);
+		break;
+
+	case VAL_Int32: case VAL_UInt32: case VAL_Float32: case VAL_Fixed: case VAL_BAM:
+		ar.Read(buff, 4);
+		break;
+
+	case VAL_Int64: case VAL_UInt64: case VAL_Float64:
+		ar.Read(buff, 8);
+		break;
+
+	case VAL_Name:
+		ar.ReadName();
+		break;
+
+	case VAL_Object:
+	{
+		DObject *skipper;
+		ar << skipper;
+		break;
+	}
+	case VAL_State:
+	{
+		FState *skipper;
+		ar << skipper;
+		break;
+	}
+	case VAL_String:
+	{
+		FString skipper;
+		ar << skipper;
+		break;
+	}
+	case VAL_Array:
+	{
+		DWORD count = ar.ReadCount();
+		while (count-- > 0)
+		{
+			SkipValue(ar);
+		}
+		break;
+	}
+	case VAL_Struct:
+	{
+		const char *label;
+		for (label = ar.ReadName(); label != NULL; label = ar.ReadName())
+		{
+			SkipValue(ar);
+		}
+		break;
+	}
+	case VAL_Class:
+	{
+		PClass *type;
+		for (ar.UserReadClass(type); type != NULL; ar.UserReadClass(type))
+		{
+			SkipValue(ar, VAL_Struct);
+		}
+		break;
+	}
+	}
+}
+
 //==========================================================================
 //
 // PType :: SetValue
@@ -662,6 +778,177 @@ PInt::PInt(unsigned int size, bool unsign)
 	}
 }
 
+//==========================================================================
+//
+// PInt :: WriteValue
+//
+// Write the value using the minimum byte size needed to represent it. This
+// means that the value as written is not necessarily of the same type as
+// stored, but the signedness information is preserved.
+//
+//==========================================================================
+
+void PInt::WriteValue(FArchive &ar, const void *addr) const
+{
+	BYTE bval;
+
+	// The process for bytes is the same whether signed or unsigned, since
+	// they can't be compacted into a representation with fewer bytes.
+	if (Size == 1)
+	{
+		bval = *(BYTE *)addr;
+	}
+	else if (Unsigned)
+	{
+		unsigned val;
+		if (Size == 8)
+		{
+			QWORD qval = *(QWORD *)addr;
+			if (qval & 0xFFFFFFFF00000000llu)
+			{ // Value needs 64 bits
+				ar.WriteByte(VAL_UInt64);
+				ar.WriteInt64(qval);
+				return;
+			}
+			// Value can fit in 32 bits or less
+			val = (unsigned)qval;
+			goto check_u32;
+		}
+		else if (Size == 4)
+		{
+			val = *(DWORD *)addr;
+check_u32:	if (val & 0xFFFF0000u)
+			{ // Value needs 32 bits
+				ar.WriteByte(VAL_UInt32);
+				ar.WriteInt32(val);
+				return;
+			}
+			// Value can fit in 16 bits or less
+			goto check_u16;
+		}
+		else// if (Size == 2)
+		{
+			val = *(WORD *)addr;
+check_u16:	if (val & 0xFFFFFF00u)
+			{ // Value needs 16 bits
+				ar.WriteByte(VAL_UInt16);
+				ar.WriteInt16(val);
+				return;
+			}
+			// Value can fit in 8 bits
+			bval = (BYTE)val;
+		}
+	}
+	else // Signed
+	{
+		int val;
+		if (Size == 8)
+		{
+			SQWORD qval = *(SQWORD *)addr;
+			INT_MIN;
+			if (qval < (-0x7FFFFFFF - 1) || qval > 0x7FFFFFFF)
+			{ // Value needs 64 bits
+				ar.WriteByte(VAL_Int64);
+				ar.WriteInt64(qval);
+				return;
+			}
+			// Value can fit in 32 bits or less
+			val = (int)qval;
+			goto check_s32;
+		}
+		else if (Size == 4)
+		{
+			val = *(SDWORD *)addr;
+check_s32:	if (val < -0x8000 || val > 0x7FFF)
+			{ // Value needs 32 bits
+				ar.WriteByte(VAL_Int32);
+				ar.WriteInt32(val);
+				return;
+			}
+			// Value can fit in 16 bits or less
+			goto check_s16;
+		}
+		else// if (Size == 2)
+		{
+			val = *(SWORD *)addr;
+check_s16:	if (val < -0x80 || val > 0x7F)
+			{ // Value needs 16 bits
+				ar.WriteByte(VAL_Int16);
+				ar.WriteInt16(val);
+				return;
+			}
+			// Value can fit in 8 bits
+			bval = (BYTE)val;
+		}
+	}
+	// If we get here, the value fits in a byte. Values of 0 and 1 are
+	// optimized away into the tag so they don't require any extra space
+	// to store.
+	if (bval & 0xFE)
+	{
+		BYTE out[2] = { Unsigned ? VAL_UInt8 : VAL_Int8, bval };
+		ar.Write(out, 2);
+	}
+	else
+	{
+		ar.WriteByte(VAL_Zero + bval);
+	}
+}
+
+//==========================================================================
+//
+// PInt :: ReadValue
+//
+//==========================================================================
+
+bool PInt::ReadValue(FArchive &ar, void *addr) const
+{
+	union
+	{
+		QWORD uval;
+		SQWORD sval;
+	};
+	BYTE tag;
+	union
+	{
+		BYTE val8;
+		WORD val16;
+		DWORD val32;
+		fixed_t fix;
+		float single;
+		double dbl;
+		angle_t ang;
+	};
+
+	ar << tag;
+	switch (tag)
+	{
+	case VAL_Zero:		uval = 0; break;
+	case VAL_One:		uval = 1; break;
+	case VAL_Int8:		ar << val8;	sval = (SBYTE)val8;	break;
+	case VAL_UInt8:		ar << val8; uval = val8; break;
+	case VAL_Int16:		ar << val16; sval = (SWORD)val16; break;
+	case VAL_UInt16:	ar << val16; uval = val16; break;
+	case VAL_Int32:		ar << val32; sval = (SDWORD)val32; break;
+	case VAL_UInt32:	ar << val32; uval = val32; break;
+	case VAL_Int64:		ar << sval; break;
+	case VAL_UInt64:	ar << uval; break;
+	case VAL_Fixed:		ar << fix; sval = fix >> FRACBITS; break;	// fixed -> int
+	case VAL_BAM:		ar << ang; uval = ang / ANGLE_1; break;		// BAM -> degrees
+	case VAL_Float32:	ar << single; sval = (SQWORD)single; break;
+	case VAL_Float64:	ar << dbl; sval = (SQWORD)dbl; break;
+	default:			SkipValue(ar, tag); return false;		// Incompatible type
+	}
+	switch (Size)
+	{
+	case 1:	*(BYTE *)addr = (BYTE)uval; break;
+	case 2: *(WORD *)addr = (WORD)uval; break;
+	case 4: *(DWORD *)addr = (DWORD)uval; break;
+	case 8: *(QWORD *)addr = uval; break;
+	}
+	return true;
+}
+
 //==========================================================================
 //
 // PInt :: SetValue
@@ -947,6 +1234,97 @@ void PFloat::SetSymbols(const PFloat::SymbolInitI *sym, size_t count)
 	}
 }
 
+//==========================================================================
+//
+// PFloat :: WriteValue
+//
+//==========================================================================
+
+void PFloat::WriteValue(FArchive &ar, const void *addr) const
+{
+	float singleprecision;
+	if (Size == 8)
+	{
+		// If it can be written as single precision without information
+		// loss, then prefer that over writing a full-sized double.
+		double doubleprecision = *(double *)addr;
+		singleprecision = (float)doubleprecision;
+		if (singleprecision != doubleprecision)
+		{
+			ar.WriteByte(VAL_Float64);
+			ar << doubleprecision;
+		}
+	}
+	else
+	{
+		singleprecision = *(float *)addr;
+	}
+	ar.WriteByte(VAL_Float32);
+	ar << singleprecision;
+}
+
+//==========================================================================
+//
+// PFloat :: ReadValue
+//
+//==========================================================================
+
+static bool ReadValueDbl(FArchive &ar, double *addr, unsigned tag)
+{
+	double val;
+	union
+	{
+		BYTE val8;
+		WORD val16;
+		DWORD val32;
+		QWORD val64;
+		fixed_t fix;
+		float single;
+		angle_t ang;
+	};
+
+	switch (tag)
+	{
+	case VAL_Zero:		val = 0; break;
+	case VAL_One:		val = 1; break;
+	case VAL_Int8:		ar << val8;	val = (SBYTE)val8;	break;
+	case VAL_UInt8:		ar << val8; val = val8; break;
+	case VAL_Int16:		ar << val16; val = (SWORD)val16; break;
+	case VAL_UInt16:	ar << val16; val = val16; break;
+	case VAL_Int32:		ar << val32; val = (SDWORD)val32; break;
+	case VAL_UInt32:	ar << val32; val = val32; break;
+	case VAL_Int64:		ar << val64; val = (double)(SQWORD)val64; break;
+	case VAL_UInt64:	ar << val64; val = (double)val64; break;
+	case VAL_Fixed:		ar << fix; val = FIXED2DBL(fix); break;
+	case VAL_BAM:		ar << ang; val = ang * (90.0 / ANGLE_90); break;		// BAM -> degrees
+	case VAL_Float32:	ar << single; val = single; break;
+	case VAL_Float64:	ar << val; break;
+	default:			PType::SkipValue(ar, tag); return false;	// Incompatible type
+	}
+	*(double *)addr = val;
+	return true;
+}
+
+bool PFloat::ReadValue(FArchive &ar, void *addr) const
+{
+	BYTE tag;
+	ar << tag;
+	double val;
+	if (ReadValueDbl(ar, &val, tag))
+	{
+		if (Size == 4)
+		{
+			*(float *)addr = (float)val;
+		}
+		else
+		{
+			*(double *)addr = val;
+		}
+		return true;
+	}
+	return false;
+}
+
 //==========================================================================
 //
 // PFloat :: SetValue
@@ -1080,6 +1458,45 @@ int PString::GetRegType() const
 	return REGT_STRING;
 }
 
+//==========================================================================
+//
+// PString :: WriteValue
+//
+//==========================================================================
+
+void PString::WriteValue(FArchive &ar, const void *addr) const
+{
+	ar.WriteByte(VAL_String);
+	ar.WriteString(*(const FString *)addr);
+}
+
+//==========================================================================
+//
+// PString :: ReadValue
+//
+//==========================================================================
+
+bool PString::ReadValue(FArchive &ar, void *addr) const
+{
+	BYTE tag;
+	ar << tag;
+	if (tag == VAL_String)
+	{
+		ar << *(FString *)addr;
+	}
+	else if (tag == VAL_Name)
+	{
+		const char *str = ar.ReadName();
+		*(FString *)addr = str;
+	}
+	else
+	{
+		SkipValue(ar, tag);
+		return false;
+	}
+	return true;
+}
+
 /* PName ******************************************************************/
 
 IMPLEMENT_CLASS(PName)
@@ -1096,6 +1513,46 @@ PName::PName()
 	assert(sizeof(FName) == __alignof(FName));
 }
 
+//==========================================================================
+//
+// PName :: WriteValue
+//
+//==========================================================================
+
+void PName::WriteValue(FArchive &ar, const void *addr) const
+{
+	ar.WriteByte(VAL_Name);
+	ar.WriteName(((const FName *)addr)->GetChars());
+}
+
+//==========================================================================
+//
+// PName :: ReadValue
+//
+//==========================================================================
+
+bool PName::ReadValue(FArchive &ar, void *addr) const
+{
+	BYTE tag;
+	ar << tag;
+	if (tag == VAL_Name)
+	{
+		*(FName *)addr = FName(ar.ReadName());
+	}
+	else if (tag == VAL_String)
+	{
+		FString str;
+		ar << str;
+		*(FName *)addr = FName(str);
+	}
+	else
+	{
+		SkipValue(ar, tag);
+		return false;
+	}
+	return true;
+}
+
 /* PSound *****************************************************************/
 
 IMPLEMENT_CLASS(PSound)
@@ -1112,6 +1569,48 @@ PSound::PSound()
 	assert(sizeof(FSoundID) == __alignof(FSoundID));
 }
 
+//==========================================================================
+//
+// PSound :: WriteValue
+//
+//==========================================================================
+
+void PSound::WriteValue(FArchive &ar, const void *addr) const
+{
+	ar.WriteByte(VAL_Name);
+	ar.WriteName(*(const FSoundID *)addr);
+}
+
+//==========================================================================
+//
+// PSound :: ReadValue
+//
+//==========================================================================
+
+bool PSound::ReadValue(FArchive &ar, void *addr) const
+{
+	BYTE tag;
+
+	ar << tag;
+	if (tag == VAL_Name)
+	{
+		const char *str = ar.ReadName();
+		*(FSoundID *)addr = FSoundID(str);
+	}
+	else if (tag == VAL_String)
+	{
+		FString str;
+		ar << str;
+		*(FSoundID *)addr = FSoundID(str);
+	}
+	else
+	{
+		SkipValue(ar, tag);
+		return false;
+	}
+	return true;
+}
+
 /* PColor *****************************************************************/
 
 IMPLEMENT_CLASS(PColor)
@@ -1143,6 +1642,45 @@ PFixed::PFixed()
 {
 }
 
+//==========================================================================
+//
+// PFixed :: WriteValue
+//
+//==========================================================================
+
+void PFixed::WriteValue(FArchive &ar, const void *addr) const
+{
+	ar.WriteByte(VAL_Fixed);
+	ar << *(fixed_t *)addr;
+}
+
+//==========================================================================
+//
+// PFixed :: ReadValue
+//
+//==========================================================================
+
+bool PFixed::ReadValue(FArchive &ar, void *addr) const
+{
+	BYTE tag;
+	ar << tag;
+	if (tag == VAL_Fixed)
+	{
+		ar << *(fixed_t *)addr;
+		return true;
+	}
+	else
+	{
+		double val;
+		if (ReadValueDbl(ar, &val, tag))
+		{
+			*(fixed_t *)addr = FLOAT2FIXED(val);
+			return true;
+		}
+		return false;
+	}
+}
+
 //==========================================================================
 //
 // PFixed :: SetValue
@@ -1222,6 +1760,45 @@ PAngle::PAngle()
 {
 }
 
+//==========================================================================
+//
+// PAngle :: WriteValue
+//
+//==========================================================================
+
+void PAngle::WriteValue(FArchive &ar, const void *addr) const
+{
+	ar.WriteByte(VAL_BAM);
+	ar.WriteInt32(*(angle_t *)addr);
+}
+
+//==========================================================================
+//
+// PAngle :: ReadValue
+//
+//==========================================================================
+
+bool PAngle::ReadValue(FArchive &ar, void *addr) const
+{
+	BYTE tag;
+	ar << tag;
+	if (tag == VAL_BAM)
+	{
+		ar << *(angle_t *)addr;
+		return true;
+	}
+	else
+	{
+		double val;
+		if (ReadValueDbl(ar, &val, tag))
+		{
+			*(angle_t *)addr = FLOAT2ANGLE(val);
+			return true;
+		}
+		return false;
+	}
+}
+
 //==========================================================================
 //
 // PAngle :: SetValue
@@ -1334,6 +1911,37 @@ int PStatePointer::GetRegType() const
 	return REGT_POINTER;
 }
 
+//==========================================================================
+//
+// PStatePointer :: WriteValue
+//
+//==========================================================================
+
+void PStatePointer::WriteValue(FArchive &ar, const void *addr) const
+{
+	ar.WriteByte(VAL_State);
+	ar << *(FState **)addr;
+}
+
+//==========================================================================
+//
+// PStatePointer :: ReadValue
+//
+//==========================================================================
+
+bool PStatePointer::ReadValue(FArchive &ar, void *addr) const
+{
+	BYTE tag;
+	ar << tag;
+	if (tag == VAL_State)
+	{
+		ar << *(FState **)addr;
+		return true;
+	}
+	SkipValue(ar, tag);
+	return false;
+}
+
 /* PPointer ***************************************************************/
 
 IMPLEMENT_POINTY_CLASS(PPointer)
@@ -1421,6 +2029,45 @@ void PPointer::GetTypeIDs(intptr_t &id1, intptr_t &id2) const
 	id2 = 0;
 }
 
+//==========================================================================
+//
+// PPointer :: WriteValue
+//
+//==========================================================================
+
+void PPointer::WriteValue(FArchive &ar, const void *addr) const
+{
+	if (PointedType->IsKindOf(RUNTIME_CLASS(PClass)))
+	{
+		ar.WriteByte(VAL_Object);
+		ar << *(DObject **)addr;
+	}
+	else
+	{
+		assert(0 && "Pointer points to a type we don't handle");
+		I_Error("Attempt to save pointer to unhandled type");
+	}
+}
+
+//==========================================================================
+//
+// PPointer :: ReadValue
+//
+//==========================================================================
+
+bool PPointer::ReadValue(FArchive &ar, void *addr) const
+{
+	BYTE tag;
+	ar << tag;
+	if (tag == VAL_Object && PointedType->IsKindOf(RUNTIME_CLASS(PClass)))
+	{
+		ar << *(DObject **)addr;
+		return true;
+	}
+	SkipValue(ar, tag);
+	return false;
+}
+
 //==========================================================================
 //
 // NewPointer
@@ -1626,6 +2273,61 @@ void PArray::GetTypeIDs(intptr_t &id1, intptr_t &id2) const
 	id2 = ElementCount;
 }
 
+//==========================================================================
+//
+// PArray :: WriteValue
+//
+//==========================================================================
+
+void PArray::WriteValue(FArchive &ar, const void *addr) const
+{
+	ar.WriteByte(VAL_Array);
+	ar.WriteCount(ElementCount);
+	const BYTE *addrb = (const BYTE *)addr;
+	for (unsigned i = 0; i < ElementCount; ++i)
+	{
+		ElementType->WriteValue(ar, addrb);
+		addrb += ElementSize;
+	}
+}
+
+//==========================================================================
+//
+// PArray :: ReadValue
+//
+//==========================================================================
+
+bool PArray::ReadValue(FArchive &ar, void *addr) const
+{
+	bool readsomething = false;
+	BYTE tag;
+
+	ar << tag;
+	if (tag == VAL_Array)
+	{
+		unsigned count = ar.ReadCount();
+		unsigned i;
+		BYTE *addrb = (BYTE *)addr;
+		for (i = 0; i < MIN(count, ElementCount); ++i)
+		{
+			readsomething |= ElementType->ReadValue(ar, addrb);
+			addrb += ElementSize;
+		}
+		if (i < ElementCount)
+		{
+			DPrintf("Array on disk (%u) is bigger than in memory (%u)\n",
+				count, ElementCount);
+			for (; i < ElementCount; ++i)
+			{
+				SkipValue(ar);
+			}
+		}
+		return readsomething;
+	}
+	SkipValue(ar, tag);
+	return false;
+}
+
 //==========================================================================
 //
 // NewArray
@@ -1879,6 +2581,95 @@ PStruct::PStruct(FName name, DObject *outer)
 {
 }
 
+//==========================================================================
+//
+// PStruct :: WriteValue
+//
+//==========================================================================
+
+void PStruct::WriteValue(FArchive &ar, const void *addr) const
+{
+	ar.WriteByte(VAL_Struct);
+	WriteFields(ar, addr, Fields);
+}
+
+//==========================================================================
+//
+// PStruct :: ReadValue
+//
+//==========================================================================
+
+bool PStruct::ReadValue(FArchive &ar, void *addr) const
+{
+	BYTE tag;
+	ar << tag;
+	if (tag == VAL_Struct)
+	{
+		return ReadFields(ar, addr);
+	}
+	SkipValue(ar, tag);
+	return true;
+}
+
+//==========================================================================
+//
+// PStruct :: WriteFields											STATIC
+//
+//==========================================================================
+
+void PStruct::WriteFields(FArchive &ar, const void *addr, const TArray<PField *> &fields)
+{
+	for (unsigned i = 0; i < fields.Size(); ++i)
+	{
+		const PField *field = fields[i];
+		// Skip fields with native serialization
+		if (!(field->Flags & VARF_Native))
+		{
+			ar.WriteName(field->SymbolName);
+			field->Type->WriteValue(ar, (const BYTE *)addr + field->Offset);
+		}
+	}
+	ar.WriteName(NULL);
+}
+
+//==========================================================================
+//
+// PStruct :: ReadFields
+//
+//==========================================================================
+
+bool PStruct::ReadFields(FArchive &ar, void *addr) const
+{
+	bool readsomething = false;
+	const char *label = ar.ReadName();
+	if (label == NULL)
+	{ // If there is nothing to restore, we count it as success.
+		return true;
+	}
+	for (; label != NULL; label = ar.ReadName())
+	{
+		const PSymbol *sym = Symbols.FindSymbol(FName(label, true), true);
+		if (sym == NULL)
+		{
+			DPrintf("Cannot find field %s in %s\n",
+				label, TypeName.GetChars());
+			SkipValue(ar);
+		}
+		else if (!sym->IsKindOf(RUNTIME_CLASS(PField)))
+		{
+			DPrintf("Symbol %s in %s is not a field\n",
+				label, TypeName.GetChars());
+			SkipValue(ar);
+		}
+		else
+		{
+			readsomething |= static_cast<const PField *>(sym)->Type->ReadValue(ar,
+				(BYTE *)addr + static_cast<const PField *>(sym)->Offset);
+		}
+	}
+	return readsomething;
+}
+
 //==========================================================================
 //
 // PStruct :: AddField
@@ -2091,6 +2882,89 @@ IMPLEMENT_POINTY_CLASS(PClass)
  DECLARE_POINTER(ParentClass)
 END_POINTERS
 
+//==========================================================================
+//
+// PClass :: WriteValue
+//
+// Similar to PStruct's version, except it also needs to traverse parent
+// classes.
+//
+//==========================================================================
+
+static void RecurseWriteFields(const PClass *type, FArchive &ar, const void *addr)
+{
+	if (type != NULL)
+	{
+		RecurseWriteFields(type->ParentClass, ar, addr);
+		// Don't write this part if it has no non-native variables
+		for (unsigned i = 0; i < type->Fields.Size(); ++i)
+		{
+			if (!(type->Fields[i]->Flags & VARF_Native))
+			{
+				// Tag this section with the class it came from in case
+				// a more-derived class has variables that shadow a less-
+				// derived class. Whether or not that is a language feature
+				// that will actually be allowed remains to be seen.
+				ar.UserWriteClass(const_cast<PClass *>(type));
+				PStruct::WriteFields(ar, addr, type->Fields);
+				break;
+			}
+		}
+	}
+}
+
+void PClass::WriteValue(FArchive &ar, const void *addr) const
+{
+	ar.WriteByte(VAL_Class);
+	RecurseWriteFields(this, ar, addr);
+	ar.UserWriteClass(NULL);
+}
+
+//==========================================================================
+//
+// PClass :: ReadValue
+//
+//==========================================================================
+
+bool PClass::ReadValue(FArchive &ar, void *addr) const
+{
+	BYTE tag;
+	ar << tag;
+	if (tag != VAL_Class)
+	{
+		SkipValue(ar, tag);
+		return false;
+	}
+	else
+	{
+		bool readsomething = false;
+		PClass *type;
+		for (ar.UserReadClass(type); type != NULL; ar.UserReadClass(type))
+		{
+			// Only read it if the type is related to this one.
+			const PClass *parent;
+			for (parent = this; parent != NULL; parent = parent->ParentClass)
+			{
+				if (parent == type)
+				{
+					break;
+				}
+			}
+			if (parent != NULL)
+			{
+				readsomething |= type->ReadFields(ar, addr);
+			}
+			else
+			{
+				DPrintf("Unknown superclass %s of class %s\n",
+					type->TypeName.GetChars(), TypeName.GetChars());
+				SkipValue(ar, VAL_Struct);
+			}
+		}
+		return readsomething;
+	}
+}
+
 //==========================================================================
 //
 // cregcmp
@@ -2354,6 +3228,26 @@ void PClass::InsertIntoHash ()
 	}
 }
 
+//==========================================================================
+//
+// PClass :: FindParentClass
+//
+// Finds a parent class that matches the given name, including itself.
+//
+//==========================================================================
+
+const PClass *PClass::FindParentClass(FName name) const
+{
+	for (const PClass *type = this; type != NULL; type = type->ParentClass)
+	{
+		if (type->TypeName == name)
+		{
+			return type;
+		}
+	}
+	return NULL;
+}
+
 //==========================================================================
 //
 // PClass :: FindClass
diff --git a/src/dobjtype.h b/src/dobjtype.h
index a1fcd13e5..5ad5757f3 100644
--- a/src/dobjtype.h
+++ b/src/dobjtype.h
@@ -186,6 +186,20 @@ public:
 
 	int FindConversion(PType *target, const Conversion **slots, int numslots);
 
+	// Writes the value of a variable of this type at (addr) to an archive, preceded by
+	// a tag indicating its type. The tag is there so that variable types can be changed
+	// without completely breaking savegames, provided that the change isn't between
+	// totally unrelated types.
+	virtual void WriteValue(FArchive &ar, const void *addr) const;
+
+	// Returns true if the stored value was compatible. False otherwise.
+	// If the value was incompatible, then the memory at *addr is unchanged.
+	virtual bool ReadValue(FArchive &ar, void *addr) const;
+
+	// Skips over a value written with WriteValue
+	static void SkipValue(FArchive &ar);
+	static void SkipValue(FArchive &ar, int tag);
+
 	// Sets the value of a variable of this type at (addr)
 	virtual void SetValue(void *addr, int val);
 	virtual void SetValue(void *addr, double val);
@@ -321,6 +335,9 @@ class PInt : public PBasicType
 public:
 	PInt(unsigned int size, bool unsign);
 
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
+
 	virtual void SetValue(void *addr, int val);
 	virtual void SetValue(void *addr, double val);
 	virtual int GetValueInt(void *addr) const;
@@ -347,6 +364,9 @@ class PFloat : public PBasicType
 public:
 	PFloat(unsigned int size);
 
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
+
 	virtual void SetValue(void *addr, int val);
 	virtual void SetValue(void *addr, double val);
 	virtual int GetValueInt(void *addr) const;
@@ -381,6 +401,9 @@ public:
 	PString();
 
 	virtual int GetRegType() const;
+
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
 };
 
 // Variations of integer types ----------------------------------------------
@@ -390,6 +413,9 @@ class PName : public PInt
 	DECLARE_CLASS(PName, PInt);
 public:
 	PName();
+
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
 };
 
 class PSound : public PInt
@@ -397,6 +423,9 @@ class PSound : public PInt
 	DECLARE_CLASS(PSound, PInt);
 public:
 	PSound();
+
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
 };
 
 class PColor : public PInt
@@ -415,6 +444,9 @@ class PFixed : public PFloat
 public:
 	PFixed();
 
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
+
 	virtual void SetValue(void *addr, int val);
 	virtual void SetValue(void *addr, double val);
 	virtual int GetValueInt(void *addr) const;
@@ -429,6 +461,9 @@ class PAngle : public PFloat
 public:
 	PAngle();
 
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
+
 	virtual void SetValue(void *addr, int val);
 	virtual void SetValue(void *addr, double val);
 	virtual int GetValueInt(void *addr) const;
@@ -445,6 +480,9 @@ class PStatePointer : public PBasicType
 public:
 	PStatePointer();
 
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
+
 	virtual int GetStoreOp() const;
 	virtual int GetLoadOp() const;
 	virtual int GetRegType() const;
@@ -465,6 +503,10 @@ public:
 
 	virtual bool IsMatch(intptr_t id1, intptr_t id2) const;
 	virtual void GetTypeIDs(intptr_t &id1, intptr_t &id2) const;
+
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
+
 protected:
 	PPointer();
 };
@@ -531,6 +573,10 @@ public:
 
 	virtual bool IsMatch(intptr_t id1, intptr_t id2) const;
 	virtual void GetTypeIDs(intptr_t &id1, intptr_t &id2) const;
+
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
+
 protected:
 	PArray();
 };
@@ -588,6 +634,12 @@ public:
 	virtual PField *AddField(FName name, PType *type, DWORD flags=0);
 
 	size_t PropagateMark();
+
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
+
+	static void WriteFields(FArchive &ar, const void *addr, const TArray<PField *> &fields);
+	bool ReadFields(FArchive &ar, void *addr) const;
 protected:
 	PStruct();
 };
@@ -650,6 +702,9 @@ public:
 	typedef PClassClass MetaClass;
 	MetaClass *GetClass() const;
 
+	void WriteValue(FArchive &ar, const void *addr) const override;
+	bool ReadValue(FArchive &ar, void *addr) const override;
+
 	virtual void DeriveData(PClass *newclass) {}
 	static void StaticInit();
 	static void StaticShutdown();
@@ -694,6 +749,9 @@ public:
 	}
 
 	// Find a type, given its name.
+	const PClass *FindParentClass(FName name) const;
+	PClass *FindParentClass(FName name) { return const_cast<PClass *>(const_cast<const PClass *>(this)->FindParentClass(name)); }
+
 	static PClass *FindClass(const char *name)			{ return FindClass(FName(name, true)); }
 	static PClass *FindClass(const FString &name)		{ return FindClass(FName(name, true)); }
 	static PClass *FindClass(ENamedName name)			{ return FindClass(FName(name)); }
@@ -844,4 +902,31 @@ public:
 
 void ReleaseGlobalSymbols();
 
+// Enumerations for serializing types in an archive -------------------------
+
+enum ETypeVal : BYTE
+{
+	VAL_Int8,
+	VAL_UInt8,
+	VAL_Int16,
+	VAL_UInt16,
+	VAL_Int32,
+	VAL_UInt32,
+	VAL_Int64,
+	VAL_UInt64,
+	VAL_Zero,
+	VAL_One,
+	VAL_Float32,
+	VAL_Float64,
+	VAL_Fixed,
+	VAL_BAM,
+	VAL_String,
+	VAL_Name,
+	VAL_Struct,
+	VAL_Array,
+	VAL_Object,
+	VAL_State,
+	VAL_Class,
+};
+
 #endif
diff --git a/src/farchive.cpp b/src/farchive.cpp
index 8fd647fd2..1f805ae8e 100644
--- a/src/farchive.cpp
+++ b/src/farchive.cpp
@@ -721,6 +721,29 @@ void FArchive::Close ()
 	}
 }
 
+void FArchive::WriteByte(BYTE val)
+{
+	m_File->Write(&val, 1);
+}
+
+void FArchive::WriteInt16(WORD val)
+{
+	WORD out = LittleShort(val);
+	m_File->Write(&out, 2);
+}
+
+void FArchive::WriteInt32(DWORD val)
+{
+	int out = LittleLong(val);
+	m_File->Write(&out, 4);
+}
+
+void FArchive::WriteInt64(QWORD val)
+{
+	long long out = SWAP_QWORD(val);
+	m_File->Write(&out, 8);
+}
+
 void FArchive::WriteCount (DWORD count)
 {
 	BYTE out;
@@ -832,6 +855,14 @@ void FArchive::WriteString (const char *str)
 	}
 }
 
+void FArchive::WriteString(const FString &str)
+{
+	// The count includes the '\0' terminator, but we don't
+	// actually write it out.
+	WriteCount(str.Len() + 1);
+	Write(str, str.Len());
+}
+
 FArchive &FArchive::operator<< (char *&str)
 {
 	if (m_Storing)
@@ -868,7 +899,7 @@ FArchive &FArchive::operator<< (FString &str)
 {
 	if (m_Storing)
 	{
-		WriteString (str.GetChars());
+		WriteString (str);
 	}
 	else
 	{
@@ -883,8 +914,7 @@ FArchive &FArchive::operator<< (FString &str)
 			char *str2 = (char *)alloca(size*sizeof(char));
 			size--;
 			Read (str2, size);
-			str2[size] = 0;
-			str = str2;
+			str = FString(str2, size);
 		}
 	}
 	return *this;
diff --git a/src/farchive.h b/src/farchive.h
index b646827de..2324c6b2a 100644
--- a/src/farchive.h
+++ b/src/farchive.h
@@ -166,7 +166,14 @@ public:
 virtual	void Write (const void *mem, unsigned int len);
 virtual void Read (void *mem, unsigned int len);
 
+		void WriteString(const FString &str);
 		void WriteString (const char *str);
+
+		void WriteByte(BYTE val);
+		void WriteInt16(WORD val);
+		void WriteInt32(DWORD val);
+		void WriteInt64(QWORD val);
+
 		void WriteCount (DWORD count);
 		DWORD ReadCount ();
 
diff --git a/src/version.h b/src/version.h
index 17fd288b1..17e4fa3f1 100644
--- a/src/version.h
+++ b/src/version.h
@@ -76,7 +76,7 @@ const char *GetVersionString();
 
 // Use 4500 as the base git save version, since it's higher than the
 // SVN revision ever got.
-#define SAVEVER 4534
+#define SAVEVER 4535
 
 #define SAVEVERSTRINGIFY2(x) #x
 #define SAVEVERSTRINGIFY(x) SAVEVERSTRINGIFY2(x)