Skip to content

Instantly share code, notes, and snippets.

@ArcticEcho
Last active May 23, 2024 13:33
Show Gist options
  • Save ArcticEcho/2558770d0ab03242420da46724f7e98b to your computer and use it in GitHub Desktop.
Save ArcticEcho/2558770d0ab03242420da46724f7e98b to your computer and use it in GitHub Desktop.
UE4 Save File Accessor
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