Last active
May 23, 2024 13:33
-
-
Save ArcticEcho/2558770d0ab03242420da46724f7e98b to your computer and use it in GitHub Desktop.
UE4 Save File Accessor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Text; | |
// Only supports string, float, int, and bool properties. | |
// Usage: | |
// var saveFileAccessor = new UnrealEngineSaveFileAccessor("path to .sav file"); | |
// var propName = "SomeFloatVariable"; | |
// var propValue = saveFileAccessor.GetProperty<float>(propName); | |
// saveFileAccessor.SetProperty(propName, 42069F); | |
namespace UESaveFileAccessor | |
{ | |
public static class Extensions | |
{ | |
public static string ReadFixedLengthNullTerminatedString(this BinaryReader reader) | |
{ | |
var length = reader.ReadInt32(); | |
var chars = reader.ReadChars(length - 1); | |
reader.ReadByte(); | |
return new string(chars); | |
} | |
} | |
public class UnrealEngineSaveFileAccessor | |
{ | |
private const int _metaOffset = 1037; | |
private const int _endOfFileOffset = 13; | |
private readonly string _filePath; | |
private Dictionary<string, (long Offset, long LengthPaddingValueLength)> _propertyOffsets; | |
public string SaveDataClass { get; private set; } | |
public UnrealEngineSaveFileAccessor(string saveFilePath) | |
{ | |
_filePath = saveFilePath; | |
UpdatePropertyOffsets(); | |
} | |
public void SetProperty<T>(string name, T data) | |
{ | |
byte[] valueBytes; | |
var isBool = false; | |
switch (data) | |
{ | |
case string x: | |
{ | |
x += '\0'; | |
var str = Encoding.ASCII.GetBytes(x); | |
var strLen = BitConverter.GetBytes(str.Length); | |
valueBytes = new byte[4 + str.Length]; | |
Buffer.BlockCopy(strLen, 0, valueBytes, 0, 4); | |
Buffer.BlockCopy(str, 0, valueBytes, 4, str.Length); | |
break; | |
} | |
case float x: | |
{ | |
valueBytes = BitConverter.GetBytes(x); | |
break; | |
} | |
case int x: | |
{ | |
valueBytes = BitConverter.GetBytes(x); | |
break; | |
} | |
case bool x: | |
{ | |
valueBytes = BitConverter.GetBytes((short)(x ? 1 : 0)); | |
isBool = true; | |
break; | |
} | |
default: | |
{ | |
return; | |
} | |
} | |
byte[] lengthAndPaddingBytes; | |
if (isBool) | |
{ | |
// for some reason they decided to set bool property lengths to 0 | |
lengthAndPaddingBytes = BitConverter.GetBytes(0L); | |
} | |
else | |
{ | |
lengthAndPaddingBytes = new byte[9]; | |
// proprty length doesn't include padding | |
Buffer.BlockCopy(BitConverter.GetBytes((long)valueBytes.Length), 0, lengthAndPaddingBytes, 0, 8); | |
} | |
var lengthPaddingValueBytesLength = lengthAndPaddingBytes.Length + valueBytes.Length; | |
var lengthPaddingValueBytes = new byte[lengthPaddingValueBytesLength]; | |
Buffer.BlockCopy(lengthAndPaddingBytes, 0, lengthPaddingValueBytes, 0, lengthAndPaddingBytes.Length); | |
Buffer.BlockCopy(valueBytes, 0, lengthPaddingValueBytes, lengthAndPaddingBytes.Length, valueBytes.Length); | |
byte[] fileBytes; | |
using (var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read)) | |
using (var reader = new BinaryReader(fileStream, Encoding.ASCII)) | |
{ | |
var oldPropLengthOffset = (int)_propertyOffsets[name].Offset; | |
var oldPropLengthPaddingValueLength = (int)_propertyOffsets[name].LengthPaddingValueLength; | |
var newFileLength = (reader.BaseStream.Length - oldPropLengthPaddingValueLength) + lengthPaddingValueBytesLength; | |
fileBytes = new byte[newFileLength]; | |
// Copy bytes before property | |
fileStream.Read(fileBytes, 0, oldPropLengthOffset); | |
// Copy the new property | |
Buffer.BlockCopy(lengthPaddingValueBytes, 0, fileBytes, oldPropLengthOffset, lengthPaddingValueBytesLength); | |
// Skip the old property | |
fileStream.Position += oldPropLengthPaddingValueLength; | |
// Copy the rest of the file | |
fileStream.Read(fileBytes, oldPropLengthOffset + lengthPaddingValueBytesLength, (int)(fileStream.Length - fileStream.Position)); | |
} | |
File.WriteAllBytes(_filePath, fileBytes); | |
UpdatePropertyOffsets(); | |
} | |
public T GetProperty<T>(string name) | |
{ | |
using var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read); | |
using var reader = new BinaryReader(fileStream, Encoding.ASCII); | |
reader.BaseStream.Position = _metaOffset; // meta data | |
reader.ReadFixedLengthNullTerminatedString(); // save data class path | |
// Iterate over the properties until we reach the end of the file. | |
while (reader.BaseStream.Position < reader.BaseStream.Length - _endOfFileOffset) | |
{ | |
var propName = reader.ReadFixedLengthNullTerminatedString(); | |
var propTypeString = reader.ReadFixedLengthNullTerminatedString(); | |
var propDataLength = reader.ReadInt64(); | |
object propValue = null; | |
Type propType = null; | |
switch (propTypeString) | |
{ | |
case "StrProperty": | |
{ | |
reader.ReadByte(); // padding | |
propValue = reader.ReadFixedLengthNullTerminatedString(); | |
propType = typeof(string); | |
break; | |
} | |
case "IntProperty": | |
{ | |
reader.ReadByte(); // padding | |
propValue = reader.ReadInt32(); | |
propType = typeof(int); | |
break; | |
} | |
case "FloatProperty": | |
{ | |
reader.ReadByte(); // padding | |
propValue = reader.ReadSingle(); | |
propType = typeof(float); | |
break; | |
} | |
case "BoolProperty": | |
{ | |
// Bool properties don't have a padding byte | |
propValue = reader.ReadInt16() == 1; | |
propType = typeof(bool); | |
break; | |
} | |
default: | |
{ | |
// Assume the property is padded. | |
reader.BaseStream.Position += propDataLength + 1; | |
break; | |
} | |
} | |
if (propName == name) | |
{ | |
if (propValue == null || propType != typeof(T)) | |
{ | |
return default; | |
} | |
return (T)propValue; | |
} | |
} | |
return default; | |
} | |
private void UpdatePropertyOffsets() | |
{ | |
using var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read); | |
using var reader = new BinaryReader(fileStream, Encoding.ASCII); | |
var propOffsets = new Dictionary<string, (long, long)>(); | |
reader.BaseStream.Position = _metaOffset; // meta data | |
SaveDataClass = reader.ReadFixedLengthNullTerminatedString(); // save data class path | |
// Iterate over the properties until we reach the end of the file. | |
while (reader.BaseStream.Position < reader.BaseStream.Length - _endOfFileOffset) | |
{ | |
var name = reader.ReadFixedLengthNullTerminatedString(); | |
var typeString = reader.ReadFixedLengthNullTerminatedString(); | |
var offset = reader.BaseStream.Position; | |
var lengthPaddingValueLength = reader.ReadInt64() + 8; | |
switch (typeString) | |
{ | |
case "StrProperty": | |
{ | |
reader.ReadByte(); // padding | |
reader.ReadFixedLengthNullTerminatedString(); // value | |
lengthPaddingValueLength++; // the length field doesn't account for padding. | |
break; | |
} | |
case "IntProperty": | |
{ | |
reader.ReadByte(); // padding | |
reader.ReadInt32(); // value | |
lengthPaddingValueLength++; // the length field doesn't account for padding. | |
break; | |
} | |
case "FloatProperty": | |
{ | |
reader.ReadByte(); // padding | |
reader.ReadSingle(); // value | |
lengthPaddingValueLength++; // the length field doesn't account for padding. | |
break; | |
} | |
case "BoolProperty": | |
{ | |
// Bool properties don't have a padding byte | |
lengthPaddingValueLength += 2; // for some reason they decided to set bool property lengths to 0 | |
reader.ReadInt16(); // value | |
break; | |
} | |
default: | |
{ | |
// Assume the property is padded. | |
reader.BaseStream.Position += lengthPaddingValueLength + 1; | |
break; | |
} | |
} | |
propOffsets[name] = (offset, lengthPaddingValueLength); | |
} | |
_propertyOffsets = propOffsets; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment