Skip to content
100 changes: 100 additions & 0 deletions STROOP.Core/GameMemoryAccess/SavestateDecompression.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System.IO.Compression;

namespace STROOP.Core.GameMemoryAccess;

public static class SavestateDecompression
{
static readonly byte[] Lz4Magic = { 0x04, 0x22, 0x4D, 0x18 };

public static bool IsLz4(byte[] data) => data.Length >= 7 && data.AsSpan(0, 4).SequenceEqual(Lz4Magic);

public static byte[] Decompress(byte[] data)
{
if (IsLz4(data))
return DecompressLz4Frame(data);

using MemoryStream input = new MemoryStream(data);
using GZipStream gzipStream = new GZipStream(input, CompressionMode.Decompress);
using MemoryStream unzip = new MemoryStream();
gzipStream.CopyTo(unzip);
return unzip.ToArray();
}

static byte[] DecompressLz4Frame(byte[] data)
{
int p = 4;
byte flg = data[p];
p += 2;
bool blockChecksum = (flg & 0x10) != 0;
bool contentSize = (flg & 0x08) != 0;
bool dictId = (flg & 0x01) != 0;
if (contentSize) p += 8;
if (dictId) p += 4;
p += 1;

MemoryStream output = new MemoryStream(data.Length * 5);
while (p + 4 <= data.Length)
{
uint blockSize = BitConverter.ToUInt32(data, p);
p += 4;
if (blockSize == 0)
break;
bool uncompressed = (blockSize & 0x80000000) != 0;
int size = (int)(blockSize & 0x7FFFFFFF);
if (uncompressed)
output.Write(data, p, size);
else
DecodeBlock(data, p, size, output);
p += size;
if (blockChecksum) p += 4;
}

return output.ToArray();
}

static void DecodeBlock(byte[] src, int start, int length, MemoryStream output)
{
int p = start, end = start + length;
while (p < end)
{
int token = src[p++];
int literals = token >> 4;
if (literals == 15)
{
int b;
do literals += b = src[p++];
while (b == 255);
}

output.Write(src, p, literals);
p += literals;
if (p >= end)
break;

int offset = src[p] | src[p + 1] << 8;
p += 2;
if (offset == 0)
break;
int matchLength = token & 15;
if (matchLength == 15)
{
int b;
do matchLength += b = src[p++];
while (b == 255);
}

matchLength += 4;

byte[] buffer = output.GetBuffer();
int matchStart = (int)output.Length - offset;
if (offset >= matchLength)
output.Write(buffer, matchStart, matchLength);
else
for (int i = 0; i < matchLength; i++)
{
buffer = output.GetBuffer();
output.WriteByte(buffer[matchStart + i]);
}
}
}
}
6 changes: 1 addition & 5 deletions STROOP.Core/GameMemoryAccess/StFileIO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ public StFileIO(string path) : base()

private void LoadMemory()
{
using FileStream? fileStream = new FileStream(_path, FileMode.Open);
using GZipStream? gzipStream = new GZipStream(fileStream, CompressionMode.Decompress);
using MemoryStream unzip = new MemoryStream();
gzipStream.CopyTo(unzip);
_data = unzip.ToArray();
_data = SavestateDecompression.Decompress(File.ReadAllBytes(_path));
}

public void SaveMemory(string path)
Expand Down
7 changes: 7 additions & 0 deletions STROOP.Core/ProcessStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,13 @@ public bool RefreshRam()
}
}

public void CopyCachedRam(byte[] destination)
{
var ram = _ram;
lock (ram)
Buffer.BlockCopy(ram, 0, destination, 0, Math.Min(ram.Length, destination.Length));
}

public bool GetAllRam(out byte[] allRam)
{
allRam = new byte[MAX_RAM_SIZE];
Expand Down
66 changes: 60 additions & 6 deletions STROOP.Variables/SM64MemoryLayout/MappingConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@ public static class MappingConfig
{
private static readonly Dictionary<uint, string> mappingUS = GetMappingDictionary(@"Mappings/MappingUS.map");
private static readonly Dictionary<uint, string> mappingJP = GetMappingDictionary(@"Mappings/MappingJP.map");
private static readonly Dictionary<string, uint> mappingUSReversed = GeneralUtilities.ReverseDictionary(mappingUS);
private static readonly Dictionary<string, uint> mappingJPReversed = GeneralUtilities.ReverseDictionary(mappingJP);
private static readonly Dictionary<string, uint> mappingUSReversed = Reverse(mappingUS);
private static readonly Dictionary<string, uint> mappingJPReversed = Reverse(mappingJP);

private static Dictionary<uint, string> mappingCurrent = null;
private static Dictionary<string, uint> mappingCurrentReversed = null;

public static bool IsAutoMapping { get; private set; }

public static bool HasUserMapping => mappingCurrent != null && !IsAutoMapping;

private static Dictionary<string, uint> Reverse(Dictionary<uint, string> mapping)
{
var reversed = new Dictionary<string, uint>();
foreach (var (address, name) in mapping)
reversed.TryAdd(name, address);
return reversed;
}

private static List<string> ReadFileLines(string filePath)
{
List<string> lines = new List<string>();
Expand All @@ -36,14 +48,14 @@ public static Dictionary<uint, string> GetMappingDictionary(string filePath)
foreach (string line in lines)
{
// splits by whitespace, see the #Remarks section of https://learn.microsoft.com/en-us/dotnet/api/system.string.split?view=net-8.0
string[] parts = line.Split(null);
string[] parts = line.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);

if (parts.Length != 2) continue;
string part1 = parts[0];
string part2 = parts[1];
if (!part1.StartsWith("0x00000000")) continue;
string addressString = "0x" + part1.Substring(10);
uint address = uint.Parse(addressString, NumberStyles.HexNumber);
if (!uint.TryParse(part1.Substring(10), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint address))
continue;
dictionary[address] = part2;
}

Expand All @@ -53,13 +65,55 @@ public static Dictionary<uint, string> GetMappingDictionary(string filePath)
public static void OpenMapping(string fileName)
{
mappingCurrent = GetMappingDictionary(fileName);
mappingCurrentReversed = GeneralUtilities.ReverseDictionary(mappingCurrent);
mappingCurrentReversed = Reverse(mappingCurrent);
IsAutoMapping = false;
}

public static void ClearMapping()
{
mappingCurrent = null;
mappingCurrentReversed = null;
IsAutoMapping = false;
}

public static void SetAutoMapping(IReadOnlyDictionary<uint, uint> vanillaToCurrent)
{
if (HasUserMapping)
return;
var current = new Dictionary<uint, string>();
foreach (var (vanilla, address) in vanillaToCurrent)
if (mappingUS.TryGetValue(vanilla, out string name))
current[address] = name;
mappingCurrent = current;
mappingCurrentReversed = Reverse(current);
IsAutoMapping = true;
}

public static void ClearAutoMapping()
{
if (IsAutoMapping)
ClearMapping();
}

public static bool IsMapped(uint vanillaAddress)
{
if (mappingCurrent == null)
return true;
var original = RomVersionConfig.Version == RomVersion.JP ? mappingJP : mappingUS;
return original.TryGetValue(vanillaAddress, out string name) && mappingCurrentReversed.ContainsKey(name);
}

public static uint? GetSymbolAddress(string name)
{
if (mappingCurrentReversed != null)
return mappingCurrentReversed.TryGetValue(name, out uint mapped) ? mapped : null;
var versionMapping = RomVersionConfig.Version switch
{
RomVersion.US => mappingUSReversed,
RomVersion.JP => mappingJPReversed,
_ => null,
};
return versionMapping != null && versionMapping.TryGetValue(name, out uint address) ? address : null;
}

public static uint HandleMapping(uint address)
Expand Down
6 changes: 6 additions & 0 deletions STROOP.Variables/SM64MemoryLayout/RomVersionConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ public static class RomVersionConfig
public static uint RomVersionTellValueSH = 0x8F250004;
public static uint RomVersionTellValueEU = 0x0C0BD4AC;

public static bool IsKnownBuild()
{
uint tell = ProcessStream.Instance.GetUInt32(RomVersionTellAddress);
return tell == RomVersionTellValueUS || tell == RomVersionTellValueJP || tell == RomVersionTellValueSH || tell == RomVersionTellValueEU;
}

public static RomVersion? GetRomVersionUsingTell()
{
uint tell = ProcessStream.Instance.GetUInt32(RomVersionTellAddress);
Expand Down
2 changes: 2 additions & 0 deletions STROOP/Forms/StroopMainForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,8 @@ private void UpdateGlobalConfig()
else if (Enum.TryParse<RomVersion>(comboBoxRomVersion.SelectedItem as string ?? "", out var explicitVersion))
RomVersionConfig.Version = explicitVersion;

DecompHackDetector.Update();

// Readonly / Read+Write
Config.Stream.Readonly = (ReadWriteMode)comboBoxReadWriteMode.SelectedItem == ReadWriteMode.ReadOnly;
}
Expand Down
39 changes: 39 additions & 0 deletions STROOP/Resources/Shaders/LevelModel.frag.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#version 330 core

in vec2 fs_texCoord;
in vec4 fs_color;
in float fs_worldY;
in float fs_fog;

uniform sampler2D tex;
uniform bool useTexture;
uniform bool decal;
uniform float alphaCutoff;
uniform bool opaque;
uniform float maxY;
uniform vec3 fogColor;
uniform bool recolorRed;

layout(location=0) out vec4 color;

void main()
{
if (fs_worldY > maxY)
discard;

vec4 c = fs_color;
if (useTexture)
{
vec4 t = texture(tex, fs_texCoord);
c = decal ? vec4(mix(fs_color.rgb, t.rgb, t.a), fs_color.a) : t * fs_color;
}

if (c.a < alphaCutoff)
discard;
if (recolorRed && c.r > 0.15 && c.r > 1.5 * c.g && c.r > 1.5 * c.b)
c.rg = c.gr;
c.rgb = mix(c.rgb, fogColor, fs_fog);
if (opaque)
c.a = 1;
color = c;
}
55 changes: 55 additions & 0 deletions STROOP/Resources/Shaders/LevelModel.vert.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#version 330 core

layout (location = 0) in vec3 vs_position;
layout (location = 1) in vec2 vs_texCoord;
layout (location = 2) in vec4 vs_color;
layout (location = 3) in vec4 vs_ambient;
layout (location = 4) in vec3 vs_normal;
layout (location = 5) in vec3 vs_lightDir;

uniform mat4 viewProjection;
uniform mat4 model;
uniform mat3 viewRotation;
uniform mat3 uvTransform = mat3(1.0);
uniform int texGen;
uniform int fogEnabled;
uniform vec2 fogFactors;
uniform vec2 fogPlanes;

out vec2 fs_texCoord;
out vec4 fs_color;
out float fs_worldY;
out float fs_fog;

void main()
{
vec4 world = model * vec4(vs_position, 1);
gl_Position = viewProjection * world;
vec2 uv = vs_texCoord;
if (texGen != 0)
{
vec3 n = viewRotation * (mat3(model) * vs_normal);
vec2 d = length(n) > 0.0 ? n.xy / length(n) : vec2(0.0);
uv *= texGen == 2 ? acos(clamp(-d, -1.0, 1.0)) / 3.14159265 : (d + 1.0) * 0.5;
}
fs_texCoord = (uvTransform * vec3(uv, 1)).xy;
fs_worldY = world.y;

fs_fog = 0.0;
if (fogEnabled != 0)
{
float n = fogPlanes.x, f = fogPlanes.y, d = max(gl_Position.w, n);
float z = (f + n) / (f - n) - 2.0 * f * n / ((f - n) * d);
fs_fog = clamp((z * fogFactors.x + fogFactors.y) / 256.0, 0.0, 1.0);
}

vec4 color = vs_color;
if (vs_ambient.a > 0.5)
{
vec3 n = viewRotation * (mat3(model) * vs_normal);
float nLength = length(n), lLength = length(vs_lightDir);
float k = nLength > 0.0 && lLength > 0.0 ? max(0.0, dot(n / nLength, vs_lightDir / lLength)) : 0.0;
color.rgb = min(vec3(1.0), vs_ambient.rgb + vs_color.rgb * k);
}
fs_color = color;
}
Loading
Loading