diff --git a/STROOP.Core/GameMemoryAccess/SavestateDecompression.cs b/STROOP.Core/GameMemoryAccess/SavestateDecompression.cs new file mode 100644 index 000000000..aee800bc1 --- /dev/null +++ b/STROOP.Core/GameMemoryAccess/SavestateDecompression.cs @@ -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]); + } + } + } +} diff --git a/STROOP.Core/GameMemoryAccess/StFileIO.cs b/STROOP.Core/GameMemoryAccess/StFileIO.cs index c598e135c..df706ef6f 100644 --- a/STROOP.Core/GameMemoryAccess/StFileIO.cs +++ b/STROOP.Core/GameMemoryAccess/StFileIO.cs @@ -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) diff --git a/STROOP.Core/ProcessStream.cs b/STROOP.Core/ProcessStream.cs index 71b4b6fa2..9c7a1325f 100644 --- a/STROOP.Core/ProcessStream.cs +++ b/STROOP.Core/ProcessStream.cs @@ -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]; diff --git a/STROOP.Variables/SM64MemoryLayout/MappingConfig.cs b/STROOP.Variables/SM64MemoryLayout/MappingConfig.cs index aac4c3540..2da73c1d0 100644 --- a/STROOP.Variables/SM64MemoryLayout/MappingConfig.cs +++ b/STROOP.Variables/SM64MemoryLayout/MappingConfig.cs @@ -8,12 +8,24 @@ public static class MappingConfig { private static readonly Dictionary mappingUS = GetMappingDictionary(@"Mappings/MappingUS.map"); private static readonly Dictionary mappingJP = GetMappingDictionary(@"Mappings/MappingJP.map"); - private static readonly Dictionary mappingUSReversed = GeneralUtilities.ReverseDictionary(mappingUS); - private static readonly Dictionary mappingJPReversed = GeneralUtilities.ReverseDictionary(mappingJP); + private static readonly Dictionary mappingUSReversed = Reverse(mappingUS); + private static readonly Dictionary mappingJPReversed = Reverse(mappingJP); private static Dictionary mappingCurrent = null; private static Dictionary mappingCurrentReversed = null; + public static bool IsAutoMapping { get; private set; } + + public static bool HasUserMapping => mappingCurrent != null && !IsAutoMapping; + + private static Dictionary Reverse(Dictionary mapping) + { + var reversed = new Dictionary(); + foreach (var (address, name) in mapping) + reversed.TryAdd(name, address); + return reversed; + } + private static List ReadFileLines(string filePath) { List lines = new List(); @@ -36,14 +48,14 @@ public static Dictionary 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; } @@ -53,13 +65,55 @@ public static Dictionary 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 vanillaToCurrent) + { + if (HasUserMapping) + return; + var current = new Dictionary(); + 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) diff --git a/STROOP.Variables/SM64MemoryLayout/RomVersionConfig.cs b/STROOP.Variables/SM64MemoryLayout/RomVersionConfig.cs index 9747d070b..d885731d1 100644 --- a/STROOP.Variables/SM64MemoryLayout/RomVersionConfig.cs +++ b/STROOP.Variables/SM64MemoryLayout/RomVersionConfig.cs @@ -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); diff --git a/STROOP/Forms/StroopMainForm.cs b/STROOP/Forms/StroopMainForm.cs index dc89f6a62..8eb55d20b 100644 --- a/STROOP/Forms/StroopMainForm.cs +++ b/STROOP/Forms/StroopMainForm.cs @@ -438,6 +438,8 @@ private void UpdateGlobalConfig() else if (Enum.TryParse(comboBoxRomVersion.SelectedItem as string ?? "", out var explicitVersion)) RomVersionConfig.Version = explicitVersion; + DecompHackDetector.Update(); + // Readonly / Read+Write Config.Stream.Readonly = (ReadWriteMode)comboBoxReadWriteMode.SelectedItem == ReadWriteMode.ReadOnly; } diff --git a/STROOP/Resources/Shaders/LevelModel.frag.glsl b/STROOP/Resources/Shaders/LevelModel.frag.glsl new file mode 100644 index 000000000..3fd31f370 --- /dev/null +++ b/STROOP/Resources/Shaders/LevelModel.frag.glsl @@ -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; +} diff --git a/STROOP/Resources/Shaders/LevelModel.vert.glsl b/STROOP/Resources/Shaders/LevelModel.vert.glsl new file mode 100644 index 000000000..de36cda80 --- /dev/null +++ b/STROOP/Resources/Shaders/LevelModel.vert.glsl @@ -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; +} diff --git a/STROOP/Resources/Shaders/Lines.geom.glsl b/STROOP/Resources/Shaders/Lines.geom.glsl index dc7f31e50..bc4fd4523 100644 --- a/STROOP/Resources/Shaders/Lines.geom.glsl +++ b/STROOP/Resources/Shaders/Lines.geom.glsl @@ -1,4 +1,4 @@ -#version 330 core +#version 330 core uniform vec2 viewportSize; @@ -12,28 +12,49 @@ layout (triangle_strip, max_vertices = 4) out; void main() { - vec3 p1 = gl_in[1].gl_Position.xyz / gl_in[1].gl_Position.w; - vec3 p0 = gl_in[0].gl_Position.xyz / gl_in[0].gl_Position.w; + vec4 c0 = gl_in[0].gl_Position, c1 = gl_in[1].gl_Position; + vec4 color0 = gs_color[0], color1 = gs_color[1]; + float width0 = gs_width[0], width1 = gs_width[1]; + + float d0 = c0.z + c0.w, d1 = c1.z + c1.w; + if (d0 < 0 && d1 < 0) + return; + if (d0 < 0) + { + float t = d0 / (d0 - d1); + c0 = mix(c0, c1, t); + color0 = mix(color0, color1, t); + width0 = mix(width0, width1, t); + } + else if (d1 < 0) + { + float t = d1 / (d1 - d0); + c1 = mix(c1, c0, t); + color1 = mix(color1, color0, t); + width1 = mix(width1, width0, t); + } + + float w0 = max(c0.w, 1e-6); + float w1 = max(c1.w, 1e-6); + vec3 p0 = c0.xyz / w0; + vec3 p1 = c1.xyz / w1; vec2 dist = p1.xy - p0.xy; vec2 perpendicular = normalize(vec2(dist.y, -dist.x)) / viewportSize; - - float w0 = gl_in[0].gl_Position.w; - float w1 = gl_in[1].gl_Position.w; - - gl_Position = vec4(vec3(p0.xy + perpendicular * gs_width[0], p0.z) * w0, w0); - fs_color = gs_color[0]; + + gl_Position = vec4(vec3(p0.xy + perpendicular * width0, p0.z) * w0, w0); + fs_color = color0; EmitVertex(); - - gl_Position = vec4(vec3(p0.xy - perpendicular * gs_width[0], p0.z) * w0, w0); - fs_color = gs_color[0]; + + gl_Position = vec4(vec3(p0.xy - perpendicular * width0, p0.z) * w0, w0); + fs_color = color0; EmitVertex(); - - gl_Position = vec4(vec3(p1.xy + perpendicular * gs_width[1], p1.z) * w1, w1); - fs_color = gs_color[1]; + + gl_Position = vec4(vec3(p1.xy + perpendicular * width1, p1.z) * w1, w1); + fs_color = color1; EmitVertex(); - - gl_Position = vec4(vec3(p1.xy - perpendicular * gs_width[1], p1.z) * w1, w1); - fs_color = gs_color[1]; + + gl_Position = vec4(vec3(p1.xy - perpendicular * width1, p1.z) * w1, w1); + fs_color = color1; EmitVertex(); EndPrimitive(); -} \ No newline at end of file +} diff --git a/STROOP/Resources/Shaders/Skybox.frag.glsl b/STROOP/Resources/Shaders/Skybox.frag.glsl new file mode 100644 index 000000000..e38e09d66 --- /dev/null +++ b/STROOP/Resources/Shaders/Skybox.frag.glsl @@ -0,0 +1,23 @@ +#version 330 core + +in vec2 fs_screen; + +uniform sampler2D sky; +uniform bool useTexture; +uniform vec3 color; +uniform vec2 windowOrigin; + +layout(location=0) out vec4 outColor; + +void main() +{ + if (!useTexture) + { + outColor = vec4(color, 1); + return; + } + + float x = windowOrigin.x + fs_screen.x * 320.0; + float y = windowOrigin.y - fs_screen.y * 240.0; + outColor = vec4(texture(sky, vec2(x / 1600.0, (960.0 - y) / 960.0)).rgb, 1); +} diff --git a/STROOP/Resources/Shaders/Skybox.vert.glsl b/STROOP/Resources/Shaders/Skybox.vert.glsl new file mode 100644 index 000000000..c5520016d --- /dev/null +++ b/STROOP/Resources/Shaders/Skybox.vert.glsl @@ -0,0 +1,10 @@ +#version 330 core + +out vec2 fs_screen; + +void main() +{ + vec2 ndc = vec2((gl_VertexID & 1) * 4.0 - 1.0, (gl_VertexID >> 1) * 4.0 - 1.0); + gl_Position = vec4(ndc, 1, 1); + fs_screen = vec2(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5); +} diff --git a/STROOP/Tabs/GhostTab/ColoredHats.cs b/STROOP/Tabs/GhostTab/ColoredHats.cs index 14c01c9fd..f18296f8b 100644 --- a/STROOP/Tabs/GhostTab/ColoredHats.cs +++ b/STROOP/Tabs/GhostTab/ColoredHats.cs @@ -40,6 +40,21 @@ partial class GhostTab uint COLORED_HATS_LIGHTS_ADDR => ghostRegionBase + 0x8300u; + public uint ColoredHatsGeneratorAddress => ghostRegionBase + COLORED_HATS_CODE_OFFSET; + + public uint ColoredHatsLightsAddress => COLORED_HATS_LIGHTS_ADDR; + + public bool ColoredHatsInstalled => + Config.Stream.GetUInt32(ColoredHatsGeneratorAddress) == 0x27BDFFC0 + && Config.Stream.GetUInt32(ColoredHatsGeneratorAddress + 4) == 0xAFBF0014 + && Config.Stream.GetUInt32(ColoredHatsGeneratorAddress + 8) == 0x3C090401; + + public bool GhostHackInstalled => + Config.Stream.GetUInt32(ghostRegionBase + GHOST_LOOP_CODE_OFFSET) == 0x27BDFFC0 + && Config.Stream.GetUInt32(ghostRegionBase + GHOST_LOOP_CODE_OFFSET + 4) == 0x3C088036; + + public bool GhostsDrawnInGame => GhostHackInstalled && Config.Stream.GetByte(numGhostsAddr) > 0; + private void EnableColoredHats() { using (Config.Stream.Suspend()) diff --git a/STROOP/Tabs/MapTab/DataUtil/F3D.cs b/STROOP/Tabs/MapTab/DataUtil/F3D.cs new file mode 100644 index 000000000..3554f6980 --- /dev/null +++ b/STROOP/Tabs/MapTab/DataUtil/F3D.cs @@ -0,0 +1,922 @@ +using System; +using System.Collections.Generic; +using OpenTK.Mathematics; + +namespace STROOP.Tabs.MapTab.DataUtil +{ + public class Texture + { + public int width, height; + public byte[] rgba; + public int wrapS, wrapT; + } + + public enum CullMode + { + None, + Back, + Front, + } + + public class Batch + { + public int texture; + public int layer; + public bool decal; + public CullMode cull; + public int first, count; + + public int uvAnimation = -1; + public int texGen; + public ulong fog; + } + + public class Mesh + { + public readonly List batches = new List(); + public int triangleCount; + } + + public class MeshStore + { + public const int VertexSize = 36; + + public byte[] data = new byte[VertexSize * 4096]; + public int length; + + public Mesh Add(MeshBuilder builder) + { + var mesh = new Mesh { triangleCount = builder.triangleCount }; + var keys = new List<(int texture, int layer, bool decal, CullMode cull, int uvAnimation, int texGen, ulong fog)>(builder.meshes.Keys); + keys.Sort((a, b) => a.layer != b.layer ? a.layer.CompareTo(b.layer) : a.texture.CompareTo(b.texture)); + foreach (var key in keys) + { + var vertices = builder.meshes[key]; + if (length + vertices.Count > data.Length) + Array.Resize(ref data, Math.Max(length + vertices.Count, data.Length * 2)); + vertices.CopyTo(data, length); + mesh.batches.Add(new Batch + { + texture = key.texture, + layer = key.layer, + decal = key.decal, + cull = key.cull, + uvAnimation = key.uvAnimation, + texGen = key.texGen, + fog = key.fog, + first = length / VertexSize, + count = vertices.Count / VertexSize, + }); + length += vertices.Count; + } + + return mesh; + } + } + + public class MeshBuilder + { + public readonly Dictionary<(int texture, int layer, bool decal, CullMode cull, int uvAnimation, int texGen, ulong fog), List> meshes = new Dictionary<(int, int, bool, CullMode, int, int, ulong), List>(); + public int triangleCount; + } + + public class TextureSet + { + public readonly List textures = new List(); + readonly Dictionary<(uint addr, int fmt, int siz, int w, int h, uint tlut, int pal, int tlutType, int cms, int cmt), int> indices = new Dictionary<(uint, int, int, int, int, uint, int, int, int, int), int>(); + + public int Get(Rdram ram, uint addr, int fmt, int siz, int w, int h, uint? tlut, int pal, int tlutType, int cms, int cmt) + { + var key = (addr, fmt, siz, w, h, tlut ?? 0xFFFFFFFF, pal, tlutType, cms, cmt); + if (!indices.TryGetValue(key, out int index)) + { + textures.Add(new Texture + { + width = w, + height = h, + rgba = DecodeTexture(ram, addr, fmt, siz, w, h, tlut, pal, tlutType), + wrapS = (cms & 1) != 0 ? 1 : (cms & 2) != 0 ? 2 : 0, + wrapT = (cmt & 1) != 0 ? 1 : (cmt & 2) != 0 ? 2 : 0, + }); + indices[key] = index = textures.Count - 1; + } + + return index; + } + + static int C5(int v) => (v << 3) | (v >> 2); + + static void Rgba16(int v, byte[] o, int i) + { + o[i] = (byte)C5((v >> 11) & 31); + o[i + 1] = (byte)C5((v >> 6) & 31); + o[i + 2] = (byte)C5((v >> 1) & 31); + o[i + 3] = (byte)((v & 1) != 0 ? 255 : 0); + } + + public static byte[] DecodeTexture(Rdram ram, uint addr, int fmt, int siz, int w, int h, uint? tlut, int palette, int tlutType) + { + int bits = 4 << siz; + uint src = ram.Phys(addr); + var o = new byte[w * h * 4]; + uint tlutPhys = tlut.HasValue ? ram.Phys(tlut.Value) : 0; + for (int i = 0; i < w * h; i++) + { + int v; + switch (bits) + { + case 4: + int b = ram.U8Phys(src + (uint)(i >> 1)); + v = (i & 1) == 0 ? b >> 4 : b & 15; + break; + case 8: + v = ram.U8Phys(src + (uint)i); + break; + case 16: + v = ram.U16Phys(src + (uint)(2 * i)); + break; + default: + v = (int)ram.U32Phys(src + (uint)(4 * i)); + break; + } + + int k = 4 * i; + switch (fmt) + { + case 0: + if (bits == 16) + Rgba16(v, o, k); + else + { + o[k] = (byte)(v >> 24); + o[k + 1] = (byte)(v >> 16); + o[k + 2] = (byte)(v >> 8); + o[k + 3] = (byte)v; + } + + break; + case 2: + int index = v + (bits == 4 ? palette * 16 : 0); + if (!tlut.HasValue) + { + byte g = (byte)(siz == 0 ? index * 17 : index); + o[k] = o[k + 1] = o[k + 2] = g; + o[k + 3] = 255; + } + else + { + int p = ram.U16Phys(tlutPhys + (uint)(2 * index)); + if (tlutType == 3) + { + o[k] = o[k + 1] = o[k + 2] = (byte)(p >> 8); + o[k + 3] = (byte)p; + } + else + Rgba16(p, o, k); + } + + break; + case 3: + if (bits == 4) + { + int i3 = (v >> 1) & 7; + o[k] = o[k + 1] = o[k + 2] = (byte)((i3 << 5) | (i3 << 2) | (i3 >> 1)); + o[k + 3] = (byte)((v & 1) != 0 ? 255 : 0); + } + else if (bits == 8) + { + o[k] = o[k + 1] = o[k + 2] = (byte)((v >> 4) * 17); + o[k + 3] = (byte)((v & 15) * 17); + } + else + { + o[k] = o[k + 1] = o[k + 2] = (byte)(v >> 8); + o[k + 3] = (byte)v; + } + + break; + default: + o[k] = o[k + 1] = o[k + 2] = o[k + 3] = (byte)(bits == 4 ? v * 17 : v & 255); + break; + } + } + + return o; + } + } + + public enum Microcode + { + F3D, + F3DEX, + F3DEX2, + } + + public class F3D + { + const int G_MOVEMEM = 0x03, G_VTX = 0x04, G_DL = 0x06; + + const int EX_G_BRANCH_Z = 0xB0, EX_G_TRI2 = 0xB1, EX_G_RDPHALF_1 = 0xB4, EX_G_QUAD = 0xB5; + + const int EX2_G_VTX = 0x01, EX2_G_BRANCH_Z = 0x04, EX2_G_TRI1 = 0x05, EX2_G_TRI2 = 0x06, EX2_G_QUAD = 0x07; + const int EX2_G_TEXTURE = 0xD7, EX2_G_GEOMETRYMODE = 0xD9, EX2_G_MOVEWORD = 0xDB, EX2_G_MOVEMEM = 0xDC; + const int EX2_G_DL = 0xDE, EX2_G_ENDDL = 0xDF, EX2_G_RDPHALF_1 = 0xE1, EX2_G_SETOTHERMODE_H = 0xE3; + const int EX2_G_MV_LIGHT = 0x0A; + + const uint EX2_G_CULL_FRONT = 0x00000200, EX2_G_CULL_BACK = 0x00000400, EX2_G_SHADING_SMOOTH = 0x00200000; + const uint F3D_G_SHADING_SMOOTH = 0x00000200; + const uint SHARED_GEO_BITS = 0x00000001 | 0x00000004 | 0x00010000 | 0x00020000 | 0x00040000 | 0x00080000 | 0x00100000; + + public Microcode ucode = Microcode.F3D; + uint rdpHalf1; + const int G_CLEARGEOMETRYMODE = 0xB6, G_SETGEOMETRYMODE = 0xB7, G_ENDDL = 0xB8, G_SETOTHERMODE_H = 0xBA; + const int G_TEXTURE = 0xBB, G_MOVEWORD = 0xBC, G_TRI1 = 0xBF; + const int G_LOADTLUT = 0xF0, G_SETTILESIZE = 0xF2, G_LOADBLOCK = 0xF3, G_LOADTILE = 0xF4, G_SETTILE = 0xF5; + const int G_SETFOGCOLOR = 0xF8, G_SETPRIMCOLOR = 0xFA, G_SETENVCOLOR = 0xFB, G_SETCOMBINE = 0xFC, G_SETTIMG = 0xFD; + const uint G_TEXTURE_GEN = 0x00040000, G_TEXTURE_GEN_LINEAR = 0x00080000, G_FOG = 0x00010000; + const uint G_LIGHTING = 0x00020000, G_CULL_FRONT = 0x00001000, G_CULL_BACK = 0x00002000; + const int G_MW_NUMLIGHT = 0x02, G_MW_FOG = 0x08, G_MW_LIGHTCOL = 0x0A; + + public const uint GEO_INIT = 0x00000004 | 0x00000200 | G_CULL_BACK | G_LIGHTING; + + public const uint COMBINE_SHADE_W1 = 4 << 15; + + internal class Tile + { + public int fmt, siz = 2, tmem, pal, cms, cmt, masks, maskt, uls, ult, lrs, lrt; + + public Tile MemberwiseCloneTile() => (Tile)MemberwiseClone(); + } + + struct Vtx + { + public float x, y, z; + public short s, t; + public byte r, g, b, a; + + public bool lit; + public sbyte nx, ny, nz, lx, ly, lz; + public byte ar, ag, ab; + } + + readonly Rdram ram; + readonly TextureSet textureSet; + readonly MeshBuilder output; + + public uint geo = GEO_INIT; + public int uvAnimation = -1; + bool texOn; + double texScaleS = 1, texScaleT = 1; + readonly Tile[] tiles = new Tile[8]; + uint timgAddr; + readonly Dictionary tmemSrc = new Dictionary(); + uint? tlut; + int tlutType; + byte[] prim = { 255, 255, 255, 255 }, env = { 255, 255, 255, 255 }; + public uint combine0, combine1; + uint fogColor, fogFactors; + readonly Dictionary lights = new Dictionary(); + int numLights = 1; + readonly Vtx?[] vbuf = new Vtx?[64]; + readonly byte[] raw = new byte[16]; + + public F3D(Rdram ram, TextureSet textureSet, MeshBuilder output) + { + this.ram = ram; + this.textureSet = textureSet; + this.output = output; + for (int i = 0; i < tiles.Length; i++) + tiles[i] = new Tile(); + } + + public sealed class State + { + internal uint geo, timgAddr, combine0, combine1, fogColor, fogFactors; + internal bool texOn; + internal double texScaleS, texScaleT; + internal Tile[] tiles; + internal KeyValuePair[] tmemSrc; + internal uint? tlut; + internal int tlutType, numLights; + internal byte[] prim, env; + internal KeyValuePair[] lights; + internal Microcode ucode; + int hash; + + public int Hash + { + get + { + if (hash != 0) + return hash; + var h = new HashCode(); + h.Add(geo); + h.Add(timgAddr); + h.Add(combine0); + h.Add(combine1); + h.Add(fogColor); + h.Add(fogFactors); + h.Add(texOn); + h.Add(texScaleS); + h.Add(texScaleT); + foreach (var t in tiles) + h.Add(HashCode.Combine(t.fmt, t.siz, t.tmem, t.pal, t.cms, t.cmt, HashCode.Combine(t.masks, t.maskt), HashCode.Combine(t.uls, t.ult, t.lrs, t.lrt))); + foreach (var kv in tmemSrc) + h.Add(HashCode.Combine(kv.Key, kv.Value)); + h.Add(tlut); + h.Add(tlutType); + h.Add(numLights); + h.Add(BitConverter.ToInt32(prim)); + h.Add(BitConverter.ToInt32(env)); + foreach (var kv in lights) + h.Add(HashCode.Combine(kv.Key, kv.Value)); + h.Add((int)ucode); + return hash = h.ToHashCode() | 1; + } + } + } + + public State Save() + { + var t = new Tile[tiles.Length]; + for (int i = 0; i < t.Length; i++) + t[i] = (Tile)tiles[i].MemberwiseCloneTile(); + var tmem = new List>(tmemSrc); + tmem.Sort((a, b) => a.Key.CompareTo(b.Key)); + var l = new List>(lights); + l.Sort((a, b) => a.Key.CompareTo(b.Key)); + return new State + { + geo = geo, timgAddr = timgAddr, combine0 = combine0, combine1 = combine1, fogColor = fogColor, fogFactors = fogFactors, texOn = texOn, + texScaleS = texScaleS, texScaleT = texScaleT, tiles = t, tmemSrc = tmem.ToArray(), tlut = tlut, + tlutType = tlutType, numLights = numLights, prim = (byte[])prim.Clone(), env = (byte[])env.Clone(), + lights = l.ToArray(), ucode = ucode, + }; + } + + public void Load(State s) + { + geo = s.geo; + timgAddr = s.timgAddr; + combine0 = s.combine0; + combine1 = s.combine1; + fogColor = s.fogColor; + fogFactors = s.fogFactors; + texOn = s.texOn; + texScaleS = s.texScaleS; + texScaleT = s.texScaleT; + for (int i = 0; i < tiles.Length; i++) + tiles[i] = s.tiles[i].MemberwiseCloneTile(); + tmemSrc.Clear(); + foreach (var kv in s.tmemSrc) + tmemSrc[kv.Key] = kv.Value; + tlut = s.tlut; + tlutType = s.tlutType; + numLights = s.numLights; + prim = (byte[])s.prim.Clone(); + env = (byte[])s.env.Clone(); + lights.Clear(); + foreach (var kv in s.lights) + lights[kv.Key] = kv.Value; + ucode = s.ucode; + } + + void Combiner(out int a0, out int b0, out int c0, out int d0) + { + a0 = (int)(combine0 >> 20) & 15; + c0 = (int)(combine0 >> 15) & 31; + b0 = (int)(combine1 >> 28) & 15; + d0 = (int)(combine1 >> 15) & 7; + } + + int ColorSource(out bool usesTex) + { + Combiner(out int a0, out int b0, out int c0, out int d0); + usesTex = a0 == 1 || b0 == 1 || c0 == 1 || d0 == 1; + if (a0 == 4 || b0 == 4 || c0 == 4 || d0 == 4) return 1; + if (a0 == 3 || b0 == 3 || c0 == 3 || d0 == 3) return 2; + if (a0 == 5 || b0 == 5 || c0 == 5 || d0 == 5) return 3; + return 0; + } + + byte AlphaSource(byte shadeAlpha) + { + int a = (int)(combine0 >> 12) & 7, c = (int)(combine0 >> 9) & 7, b = (int)(combine1 >> 12) & 7, d = (int)(combine1 >> 9) & 7; + if (a == 5 || b == 5 || c == 5 || d == 5) return env[3]; + if (a == 3 || b == 3 || c == 3 || d == 3) return prim[3]; + if (a == 4 || b == 4 || c == 4 || d == 4) return shadeAlpha; + return 255; + } + + bool Decal() + { + Combiner(out int a0, out int b0, out int c0, out int d0); + return a0 == 1 && b0 == 4 && c0 == 8 && d0 == 4; + } + + void Shade(ref Vtx v, in Matrix4 m) + { + if ((geo & G_LIGHTING) == 0) + { + v.r = raw[12]; + v.g = raw[13]; + v.b = raw[14]; + v.a = raw[15]; + return; + } + + var n = Vector3.TransformNormal(new Vector3((sbyte)raw[12], (sbyte)raw[13], (sbyte)raw[14]), m); + if (n.LengthSquared > 0) + n = n.Normalized() * 127; + v.lit = true; + v.nx = (sbyte)Math.Round(n.X); + v.ny = (sbyte)Math.Round(n.Y); + v.nz = (sbyte)Math.Round(n.Z); + v.a = raw[15]; + + double ar = 128, ag = 128, ab = 128; + if (lights.TryGetValue(numLights + 1, out var amb)) + (ar, ag, ab) = (amb.r, amb.g, amb.b); + var light = lights.TryGetValue(1, out var first) ? first : ((byte)255, (byte)255, (byte)255, (sbyte)0x28, (sbyte)0x28, (sbyte)0x28); + v.r = light.Item1; + v.g = light.Item2; + v.b = light.Item3; + v.lx = light.Item4; + v.ly = light.Item5; + v.lz = light.Item6; + + for (int i = 2; i <= numLights; i++) + { + var l = lights.TryGetValue(i, out var x) ? x : ((byte)255, (byte)255, (byte)255, (sbyte)0x28, (sbyte)0x28, (sbyte)0x28); + double ln = Math.Sqrt(l.Item4 * l.Item4 + l.Item5 * l.Item5 + l.Item6 * l.Item6); + if (ln == 0) ln = 1; + double k = Math.Max(0, ((sbyte)raw[12] * l.Item4 + (sbyte)raw[13] * l.Item5 + (sbyte)raw[14] * l.Item6) / (127.0 * ln)); + ar += l.Item1 * k; + ag += l.Item2 * k; + ab += l.Item3 * k; + } + + v.ar = (byte)Math.Min(255, (int)ar); + v.ag = (byte)Math.Min(255, (int)ag); + v.ab = (byte)Math.Min(255, (int)ab); + } + + int GetTexture(bool usesTex, out int w, out int h, out int uls, out int ult) + { + var t = tiles[0]; + w = ((t.lrs - t.uls) >> 2) + 1; + h = ((t.lrt - t.ult) >> 2) + 1; + uls = t.uls; + ult = t.ult; + if (!(texOn && usesTex)) + return -1; + if (!tmemSrc.TryGetValue(t.tmem, out uint addr) || !(0 < w && w <= 256 && 0 < h && h <= 256)) + return -1; + return textureSet.Get(ram, addr, t.fmt, t.siz, w, h, t.fmt == 2 ? tlut : null, t.siz == 0 ? t.pal : 0, tlutType, + t.cms | (t.masks == 0 ? 2 : 0), t.cmt | (t.maskt == 0 ? 2 : 0)); + } + + public Vector2 TexCoordScale() + { + var t = tiles[0]; + int w = ((t.lrs - t.uls) >> 2) + 1, h = ((t.lrt - t.ult) >> 2) + 1; + return w > 0 && h > 0 ? new Vector2((float)(texScaleS / (32.0 * w)), (float)(texScaleT / (32.0 * h))) : Vector2.Zero; + } + + void LoadVertices(uint addr, int n, int v0, in Matrix4 m) + { + uint p = ram.Phys(addr); + for (int i = 0; i < n; i++, p += 16) + { + if (!ram.Ok(p, 16)) + return; + for (uint k = 0; k < 16; k++) + raw[k] = ram.U8Phys(p + k); + StoreVertex(v0 + i, m); + } + } + + public void LoadVertices(byte[] vertices, int n, int v0, in Matrix4 m) + { + for (int i = 0; i < n && (i + 1) * 16 <= vertices.Length; i++) + { + Buffer.BlockCopy(vertices, i * 16, raw, 0, 16); + StoreVertex(v0 + i, m); + } + } + + void StoreVertex(int index, in Matrix4 m) + { + var pos = new Vector3((short)(raw[0] << 8 | raw[1]), (short)(raw[2] << 8 | raw[3]), (short)(raw[4] << 8 | raw[5])); + var world = Vector3.TransformPosition(pos, m); + var v = new Vtx + { + x = world.X, + y = world.Y, + z = world.Z, + s = (short)(raw[8] << 8 | raw[9]), + t = (short)(raw[10] << 8 | raw[11]), + }; + Shade(ref v, m); + if (index >= 0 && index < vbuf.Length) + vbuf[index] = v; + } + + void Triangle(int i0, int i1, int i2, int layer) + { + if (i0 >= vbuf.Length || i1 >= vbuf.Length || i2 >= vbuf.Length || !vbuf[i0].HasValue || !vbuf[i1].HasValue || !vbuf[i2].HasValue) + return; + var cull = (geo & G_CULL_BACK) != 0 ? CullMode.Back : CullMode.None; + if ((geo & G_CULL_FRONT) != 0) + { + if (cull == CullMode.Back) + return; + cull = CullMode.Front; + } + + int src = ColorSource(out bool usesTex); + int texture = GetTexture(usesTex, out int w, out int h, out int uls, out int ult); + bool decal = texture >= 0 && Decal(); + int texGen = texture >= 0 && (geo & G_TEXTURE_GEN) != 0 ? (geo & G_TEXTURE_GEN_LINEAR) != 0 ? 2 : 1 : 0; + ulong fog = (geo & G_FOG) != 0 ? (ulong)fogColor << 32 | fogFactors : 0; + var key = (texture, layer, decal, cull, uvAnimation, texGen, fog); + if (!output.meshes.TryGetValue(key, out var mesh)) + output.meshes[key] = mesh = new List(MeshStore.VertexSize * 3 * 32); + + float fu = 0, fv = 0, ou = 0, ov = 0; + if (texture >= 0) + { + fu = (float)(texScaleS / (32.0 * w)); + fv = (float)(texScaleT / (32.0 * h)); + ou = uls / (4.0f * w); + ov = ult / (4.0f * h); + } + + Vertex(mesh, vbuf[i0].Value, texture, fu, fv, ou, ov, src, texGen != 0); + Vertex(mesh, vbuf[i1].Value, texture, fu, fv, ou, ov, src, texGen != 0); + Vertex(mesh, vbuf[i2].Value, texture, fu, fv, ou, ov, src, texGen != 0); + output.triangleCount++; + } + + void Vertex(List mesh, in Vtx v, int texture, float fu, float fv, float ou, float ov, int src, bool texGen) + { + AddFloat(mesh, v.x); + AddFloat(mesh, v.y); + AddFloat(mesh, v.z); + AddFloat(mesh, texture < 0 ? 0 : texGen ? 32768 * fu : v.s * fu - ou); + AddFloat(mesh, texture < 0 ? 0 : texGen ? 32768 * fv : v.t * fv - ov); + switch (src) + { + case 1: + mesh.Add(v.r); + mesh.Add(v.g); + mesh.Add(v.b); + mesh.Add(v.a); + break; + case 2: + mesh.AddRange(prim); + break; + case 3: + mesh.AddRange(env); + break; + default: + mesh.Add(255); + mesh.Add(255); + mesh.Add(255); + mesh.Add(v.a); + break; + } + + mesh[mesh.Count - 1] = AlphaSource(v.a); + + bool lit = src == 1 && v.lit, normal = lit || (texGen && v.lit); + mesh.Add(lit ? v.ar : (byte)0); + mesh.Add(lit ? v.ag : (byte)0); + mesh.Add(lit ? v.ab : (byte)0); + mesh.Add(lit ? (byte)255 : (byte)0); + mesh.Add((byte)(normal ? v.nx : 0)); + mesh.Add((byte)(normal ? v.ny : 0)); + mesh.Add((byte)(normal ? v.nz : 0)); + mesh.Add(0); + mesh.Add((byte)(lit ? v.lx : 0)); + mesh.Add((byte)(lit ? v.ly : 0)); + mesh.Add((byte)(lit ? v.lz : 0)); + mesh.Add(0); + } + + static void AddFloat(List list, float f) + { + int b = BitConverter.SingleToInt32Bits(f); + list.Add((byte)b); + list.Add((byte)(b >> 8)); + list.Add((byte)(b >> 16)); + list.Add((byte)(b >> 24)); + } + + public void SetEnvColor(byte r, byte g, byte b, byte a) => env = new[] { r, g, b, a }; + + public void MoveMemLight(int index, uint address) + { + if (index < 0x86 || index > 0x94 || index % 2 != 0) + return; + ReadLight((index - 0x86) / 2 + 1, address); + } + + void ReadLight(int n, uint address) + { + uint p = ram.Phys(address); + if (ram.Ok(p, 16)) + lights[n] = (ram.U8Phys(p), ram.U8Phys(p + 1), ram.U8Phys(p + 2), (sbyte)ram.U8Phys(p + 8), (sbyte)ram.U8Phys(p + 9), (sbyte)ram.U8Phys(p + 10)); + } + + public void Run(uint dl, int layer, in Matrix4 m, int depth = 0) + { + uint pc = dl; + for (int iteration = 0; iteration < 100000; iteration++) + { + if (!ram.Ok(ram.Phys(pc), 8)) + return; + uint w0 = ram.U32(pc), w1 = ram.U32(pc + 4); + pc += 8; + if (!Command(w0, w1, ucode, layer, m, depth)) + return; + } + } + + public bool Command(uint w0, uint w1, int layer, in Matrix4 m, int depth = 0) => CommandF3D(w0, w1, false, layer, m, depth); + + public bool Command(uint w0, uint w1, Microcode microcode, int layer, in Matrix4 m, int depth = 0) + { + switch (microcode) + { + case Microcode.F3DEX2: + return CommandF3DEX2(w0, w1, layer, m, depth); + case Microcode.F3DEX: + return CommandF3D(w0, w1, true, layer, m, depth); + default: + return CommandF3D(w0, w1, false, layer, m, depth); + } + } + + bool Branch(uint dl, uint w0, int layer, in Matrix4 m, int depth) + { + if (depth < 16) + Run(dl, layer, m, depth + 1); + return ((w0 >> 16) & 0xFF) == 0; + } + + void Triangle2x(uint w, int layer) => Triangle((int)((w >> 16) & 0xFF) / 2, (int)((w >> 8) & 0xFF) / 2, (int)(w & 0xFF) / 2, layer); + + void OtherModeH(int shift, int length, uint w1) + { + if (shift <= 14 && 14 < shift + length) + tlutType = (int)(w1 >> 14) & 3; + } + + static uint GeoEx2ToF3D(uint bits) + { + uint r = bits & SHARED_GEO_BITS; + if ((bits & EX2_G_CULL_FRONT) != 0) r |= G_CULL_FRONT; + if ((bits & EX2_G_CULL_BACK) != 0) r |= G_CULL_BACK; + if ((bits & EX2_G_SHADING_SMOOTH) != 0) r |= F3D_G_SHADING_SMOOTH; + return r; + } + + bool CommandF3DEX2(uint w0, uint w1, int layer, in Matrix4 m, int depth) + { + int op = (int)(w0 >> 24); + if (op >= 0xE4) + { + Rdp(op, w0, w1); + return true; + } + + switch (op) + { + case EX2_G_ENDDL: + return false; + case EX2_G_DL: + return Branch(w1, w0, layer, m, depth); + case EX2_G_BRANCH_Z: + if (depth < 16) + Run(rdpHalf1, layer, m, depth + 1); + return false; + case EX2_G_RDPHALF_1: + rdpHalf1 = w1; + break; + case EX2_G_VTX: + int n = (int)(w0 >> 12) & 0xFF; + LoadVertices(w1, n, (int)((w0 >> 1) & 0x7F) - n, m); + break; + case EX2_G_TRI1: + Triangle2x(w0, layer); + break; + case EX2_G_TRI2: + case EX2_G_QUAD: + Triangle2x(w0, layer); + Triangle2x(w1, layer); + break; + case EX2_G_GEOMETRYMODE: + geo = (geo & ~GeoEx2ToF3D(~w0 & 0xFFFFFF)) | GeoEx2ToF3D(w1); + break; + case EX2_G_TEXTURE: + texOn = ((w0 >> 1) & 0x7F) != 0; + SetTextureScale(w1); + break; + case EX2_G_MOVEMEM: + if ((w0 & 0xFF) == EX2_G_MV_LIGHT) + { + int offset = (int)((w0 >> 8) & 0xFF) * 8; + if (offset % 0x18 == 0 && offset >= 0x30) + ReadLight(offset / 0x18 - 1, w1); + } + + break; + case EX2_G_MOVEWORD: + int index = (int)(w0 >> 16) & 0xFF; + if (index == G_MW_NUMLIGHT) + numLights = (int)Math.Clamp(w1 / 24, 1, 7); + else if (index == G_MW_FOG) + fogFactors = w1; + else if (index == G_MW_LIGHTCOL) + { + int offset = (int)(w0 & 0xFFFF); + if (offset % 0x18 == 0) + { + int light = offset / 0x18 + 1; + var l = lights.TryGetValue(light, out var x) ? x : ((byte)255, (byte)255, (byte)255, (sbyte)0x28, (sbyte)0x28, (sbyte)0x28); + lights[light] = ((byte)(w1 >> 24), (byte)(w1 >> 16), (byte)(w1 >> 8), l.Item4, l.Item5, l.Item6); + } + } + + break; + case EX2_G_SETOTHERMODE_H: + int len = (int)(w0 & 0xFF) + 1; + OtherModeH(32 - (int)((w0 >> 8) & 0xFF) - len, len, w1); + break; + } + + return true; + } + + void SetTextureScale(uint w1) + { + uint s = (w1 >> 16) & 0xFFFF, t = w1 & 0xFFFF; + texScaleS = s == 0xFFFF ? (s + 1) / 65536.0 : s / 65536.0; + texScaleT = t == 0xFFFF ? (t + 1) / 65536.0 : t / 65536.0; + } + + bool CommandF3D(uint w0, uint w1, bool ex, int layer, in Matrix4 m, int depth) + { + switch ((int)(w0 >> 24)) + { + case G_ENDDL: + return false; + case G_DL: + return Branch(w1, w0, layer, m, depth); + case G_VTX: + if (ex) + LoadVertices(w1, (int)(w0 >> 10) & 0x3F, (int)((w0 >> 16) & 0xFF) / 2, m); + else + LoadVertices(w1, (int)((w0 >> 20) & 15) + 1, (int)(w0 >> 16) & 15, m); + break; + case G_TRI1: + if (ex) + Triangle2x(w1, layer); + else + Triangle((int)((w1 >> 16) & 0xFF) / 10, (int)((w1 >> 8) & 0xFF) / 10, (int)(w1 & 0xFF) / 10, layer); + break; + case EX_G_TRI2 when ex: + case EX_G_QUAD when ex: + Triangle2x(w0, layer); + Triangle2x(w1, layer); + break; + case EX_G_RDPHALF_1 when ex: + rdpHalf1 = w1; + break; + case EX_G_BRANCH_Z when ex: + if (depth < 16) + Run(rdpHalf1, layer, m, depth + 1); + return false; + case G_SETGEOMETRYMODE: + geo |= w1; + break; + case G_CLEARGEOMETRYMODE: + geo &= ~w1; + break; + case G_TEXTURE: + texOn = (w0 & 0xFF) != 0; + SetTextureScale(w1); + break; + case G_MOVEMEM: + MoveMemLight((int)(w0 >> 16) & 0xFF, w1); + break; + case G_MOVEWORD: + if ((w0 & 0xFF) == G_MW_NUMLIGHT) + numLights = (int)Math.Max(1, (((long)w1 - 0x80000000L) >> 5) - 1); + else if ((w0 & 0xFF) == G_MW_FOG) + fogFactors = w1; + break; + case G_SETOTHERMODE_H: + OtherModeH((int)(w0 >> 8) & 0xFF, (int)w0 & 0xFF, w1); + break; + default: + Rdp((int)(w0 >> 24), w0, w1); + break; + } + + return true; + } + + void Rdp(int op, uint w0, uint w1) + { + switch (op) + { + case G_SETFOGCOLOR: + fogColor = w1; + break; + case G_SETTIMG: + timgAddr = w1; + break; + case G_SETTILE: + var tile = tiles[(w1 >> 24) & 7]; + tile.fmt = (int)(w0 >> 21) & 7; + tile.siz = (int)(w0 >> 19) & 3; + tile.tmem = (int)w0 & 0x1FF; + tile.pal = (int)(w1 >> 20) & 15; + tile.cmt = (int)(w1 >> 18) & 3; + tile.cms = (int)(w1 >> 8) & 3; + tile.maskt = (int)(w1 >> 14) & 15; + tile.masks = (int)(w1 >> 4) & 15; + break; + case G_SETTILESIZE: + var sizedTile = tiles[(w1 >> 24) & 7]; + sizedTile.uls = (int)(w0 >> 12) & 0xFFF; + sizedTile.ult = (int)w0 & 0xFFF; + sizedTile.lrs = (int)(w1 >> 12) & 0xFFF; + sizedTile.lrt = (int)w1 & 0xFFF; + break; + case G_LOADBLOCK: + case G_LOADTILE: + tmemSrc[tiles[(w1 >> 24) & 7].tmem] = timgAddr; + break; + case G_LOADTLUT: + tlut = timgAddr; + break; + case G_SETPRIMCOLOR: + prim = new[] { (byte)(w1 >> 24), (byte)(w1 >> 16), (byte)(w1 >> 8), (byte)w1 }; + break; + case G_SETENVCOLOR: + env = new[] { (byte)(w1 >> 24), (byte)(w1 >> 16), (byte)(w1 >> 8), (byte)w1 }; + break; + case G_SETCOMBINE: + combine0 = w0 & 0xFFFFFF; + combine1 = w1; + break; + } + } + + public static Microcode Detect(Rdram ram, IEnumerable displayLists, int maxCommands = 512) + { + int f3d = 0, f3dex = 0, f3dex2 = 0; + foreach (uint dl in displayLists) + { + if (dl == 0) + continue; + bool ex1 = false; + uint pc = dl; + for (int i = 0; i < maxCommands; i++, pc += 8) + { + uint p = ram.Phys(pc); + if (!ram.Ok(p, 8)) + break; + uint w0 = ram.U32Phys(p); + int op = (int)(w0 >> 24); + if (w0 == 0xDF000000) + { + f3dex2++; + break; + } + + if (w0 == 0xB8000000) + { + if (ex1) f3dex++; + else f3d++; + break; + } + + if (op == G_VTX && (w0 & 0xF) == 0xF && ((w0 >> 10) & 0x3F) * 16 - 1 == (w0 & 0x3FF)) + ex1 = true; + else if (op == EX_G_TRI2) + ex1 = true; + } + } + + if (f3dex2 > f3d && f3dex2 > f3dex) + return Microcode.F3DEX2; + if (f3dex > f3d) + return Microcode.F3DEX; + return Microcode.F3D; + } + } +} diff --git a/STROOP/Tabs/MapTab/DataUtil/LevelModel.cs b/STROOP/Tabs/MapTab/DataUtil/LevelModel.cs new file mode 100644 index 000000000..7e846761a --- /dev/null +++ b/STROOP/Tabs/MapTab/DataUtil/LevelModel.cs @@ -0,0 +1,1610 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OpenTK.Mathematics; + +namespace STROOP.Tabs.MapTab.DataUtil +{ + public class LevelModel + { + public class GameAddresses + { + public uint marioObjectPointer, marioState; + + public uint geoSwitchAnimState; + + public uint skyboxTextures; + + public uint geoPaintingDraw, paintingGroups; + + public uint coloredHatsGenerator, coloredHatsLights; + + public bool ghostHack; + + public uint geoSwitchMarioCapEffect, geoSwitchMarioCapOnOff, geoMirrorMarioSetAlpha, bodyStates; + + public uint geoUpdateLayerTransparency; + + public uint environmentRegions, movtexIdToTexture, movtexQuadCollectionFromId; + public uint geoMovtexDrawWaterRegions, geoMovtexDrawNoColor, geoMovtexDrawColored, geoMovtexDrawColoredNoUpdate, geoMovtexDrawColored2NoUpdate; + public uint movtexNonColored, movtexColored, movtexColored2; + + public uint geoRenderMirrorMario, geoBitsBowserColoring, geoExecInsideCastleLight, hudDisplay, saveFileGetFlags; + + public uint geoEnvFxMain, envFxMode, envFxBuffer, snowParticleCount, bubbleParticleMaxCount; + + public uint geoExecFlyingCarpetCreate, flyingCarpetRippleTimer; + + public uint displayPaintingRippling, paintingRippleImage, paintingRippleEnvMapped; + + public static GameAddresses FromSymbols(Func symbol, uint marioObjectPointer, uint marioState) + { + uint S(string name) => symbol(name) ?? 0; + uint envFxBubbleConfig = S("gEnvFxBubbleConfig"); + return new GameAddresses + { + marioObjectPointer = marioObjectPointer, + marioState = marioState, + geoSwitchAnimState = S("geo_switch_anim_state"), + skyboxTextures = S("sSkyboxTextures"), + geoPaintingDraw = S("geo_painting_draw"), + paintingGroups = S("sPaintingGroups"), + geoSwitchMarioCapEffect = S("geo_switch_mario_cap_effect"), + geoSwitchMarioCapOnOff = S("geo_switch_mario_cap_on_off"), + geoMirrorMarioSetAlpha = S("geo_mirror_mario_set_alpha"), + bodyStates = S("gBodyStates"), + geoUpdateLayerTransparency = S("geo_update_layer_transparency"), + environmentRegions = S("gEnvironmentRegions"), + movtexIdToTexture = S("gMovtexIdToTexture"), + movtexQuadCollectionFromId = S("get_quad_collection_from_id"), + geoMovtexDrawWaterRegions = S("geo_movtex_draw_water_regions"), + geoMovtexDrawNoColor = S("geo_movtex_draw_nocolor"), + geoMovtexDrawColored = S("geo_movtex_draw_colored"), + geoMovtexDrawColoredNoUpdate = S("geo_movtex_draw_colored_no_update"), + geoMovtexDrawColored2NoUpdate = S("geo_movtex_draw_colored_2_no_update"), + movtexNonColored = S("gMovtexNonColored"), + movtexColored = S("gMovtexColored"), + movtexColored2 = S("gMovtexColored2"), + geoRenderMirrorMario = S("geo_render_mirror_mario"), + geoBitsBowserColoring = S("geo_bits_bowser_coloring"), + geoExecInsideCastleLight = S("geo_exec_inside_castle_light"), + hudDisplay = S("gHudDisplay"), + saveFileGetFlags = S("save_file_get_flags"), + geoEnvFxMain = S("geo_envfx_main"), + envFxMode = S("gEnvFxMode"), + envFxBuffer = S("gEnvFxBuffer"), + snowParticleCount = S("gSnowParticleCount"), + bubbleParticleMaxCount = symbol("sBubbleParticleMaxCount") ?? (envFxBubbleConfig != 0 ? envFxBubbleConfig + 0x1C : 0), + geoExecFlyingCarpetCreate = S("geo_exec_flying_carpet_create"), + flyingCarpetRippleTimer = S("sFlyingCarpetRippleTimer"), + displayPaintingRippling = S("display_painting_rippling"), + paintingRippleImage = S("painting_ripple_image"), + paintingRippleEnvMapped = S("painting_ripple_env_mapped"), + }; + } + } + + public struct Shadow + { + public Vector3 position; + public float size, opacity; + public bool square; + } + + public struct Instance + { + public Mesh mesh; + public Matrix4 matrix; + public bool billboard; + public Vector3 anchor, anchorScale; + + public bool frame; + + public bool cap; + } + + public readonly MeshStore store = new MeshStore(); + public readonly TextureSet textures = new TextureSet(); + public Mesh level = new Mesh(); + public readonly List instances = new List(); + + public readonly List marioInstances = new List(); + public Vector3 marioModelPosition; + + public int marioInstancesStart = -1; + public readonly List shadows = new List(); + + public Texture skybox; + public Vector3? backgroundColor; + + public int displayListCount; + + public Vector2 perspectivePlanes = new Vector2(100, 20000); + public uint area; + + public bool IsEmpty => level.triangleCount == 0; + + public static readonly LevelModel None = new LevelModel(); + + const int GN_ROOT = 0x001, GN_ORTHO = 0x002, GN_PERSPECTIVE = 0x103, GN_LOD = 0x00B, GN_SWITCH = 0x10C, GN_CAMERA = 0x114; + const int GN_TRANSROT = 0x015, GN_TRANS = 0x016, GN_ROT = 0x017, GN_OBJECT = 0x018, GN_ANIMPART = 0x019, GN_BILLBOARD = 0x01A, GN_DL = 0x01B, GN_SCALE = 0x01C; + const int GN_SHADOW = 0x028, GN_OBJPARENT = 0x029, GN_GENLIST = 0x12A, GN_BG = 0x12C, GN_HELD = 0x12E; + + const int GRAPH_RENDER_ACTIVE = 0x01, GRAPH_RENDER_BILLBOARD = 0x04, GRAPH_RENDER_INVISIBLE = 0x10; + + sealed class GraphScheme + { + public static readonly GraphScheme Vanilla = new GraphScheme(null, null, 0x20); + + const int HACKER_ROOT = 20, HACKER_MASTER_LIST = 2, MASTER_LIST_HEADER = 0x14; + + static readonly int[] HackerTypes = + { + GN_ORTHO, GN_PERSPECTIVE, 0x004, GN_LOD, GN_SWITCH, GN_CAMERA, GN_TRANSROT, GN_TRANS, GN_ROT, GN_OBJECT, + GN_ANIMPART, GN_BILLBOARD, GN_DL, GN_SCALE, GN_SHADOW, GN_OBJPARENT, GN_GENLIST, GN_BG, GN_HELD, 0x02F, + GN_ROOT, 0x00A, + }; + + static readonly int[] HackerLayers = { 0, 1, 3, 2, 4, 6, 5, 7 }; + static readonly int[] HackerSilhouetteLayers = { 0, 1, 3, 2, 4, 4, 1, 4, 1, 4, 6, 5, 7 }; + + readonly int[] types, layers; + public readonly uint perspectiveNear; + + GraphScheme(int[] types, int[] layers, uint perspectiveNear) + { + this.types = types; + this.layers = layers; + this.perspectiveNear = perspectiveNear; + } + + public bool EmulatorAutoLod => types != null; + + public int Type(int raw) => types == null ? raw : raw >= 0 && raw < types.Length ? types[raw] : -1; + + public int Layer(int raw) => layers == null ? raw : raw < layers.Length ? layers[raw] : LAYER_OPAQUE; + + public static GraphScheme Detect(Rdram ram, uint root) + { + if (ram.U16(root) != HACKER_ROOT) + return Vanilla; + int layerCount = 8; + foreach (uint c in Children(ram, root)) + if (ram.U16(c) == HACKER_MASTER_LIST) + { + long size = (long)ram.U32(c + 0x10) - c; + if (size == MASTER_LIST_HEADER + 8 * HackerSilhouetteLayers.Length) + layerCount = HackerSilhouetteLayers.Length; + break; + } + + return new GraphScheme(HackerTypes, layerCount == 8 ? HackerLayers : HackerSilhouetteLayers, 0x1C); + } + } + + GraphScheme scheme = GraphScheme.Vanilla; + + int NodeType(Rdram ram, uint n) => scheme.Type(ram.U16(n)); + + Microcode ucode = Microcode.F3D; + + F3D NewF3D(Rdram ram, MeshBuilder builder) => new F3D(ram, textures, builder) { ucode = ucode }; + + F3D.State DefaultState(Rdram ram) + { + var f3d = NewF3D(ram, new MeshBuilder()); + f3d.combine1 = F3D.COMBINE_SHADE_W1; + return f3d.Save(); + } + + uint rootNode, cameraNode, objectGroup, mirrorParent, envFxNode; + + readonly Dictionary<(uint dl, int layer, int state), (Mesh mesh, F3D.State state)> objectMeshes = new Dictionary<(uint, int, int), (Mesh, F3D.State)>(); + + readonly F3D.State[] layerStates = new F3D.State[8]; + + F3D.State defaultState; + + #region matrices (row vectors, v' = v . M: the game's convention) + + static Matrix4 Affine(double m00, double m01, double m02, double m10, double m11, double m12, double m20, double m21, double m22, double tx, double ty, double tz) => + new Matrix4( + (float)m00, (float)m01, (float)m02, 0, + (float)m10, (float)m11, (float)m12, 0, + (float)m20, (float)m21, (float)m22, 0, + (float)tx, (float)ty, (float)tz, 1); + + static Matrix4 RotZXYTrans(double tx, double ty, double tz, short rx, short ry, short rz) + { + double sx = Math.Sin(rx * Math.PI / 32768), cx = Math.Cos(rx * Math.PI / 32768); + double sy = Math.Sin(ry * Math.PI / 32768), cy = Math.Cos(ry * Math.PI / 32768); + double sz = Math.Sin(rz * Math.PI / 32768), cz = Math.Cos(rz * Math.PI / 32768); + return Affine( + cy * cz + sx * sy * sz, cx * sz, -sy * cz + sx * cy * sz, + -cy * sz + sx * sy * cz, cx * cz, sy * sz + sx * cy * cz, + cx * sy, -sx, cx * cy, + tx, ty, tz); + } + + static Matrix4 RotXYZTrans(double tx, double ty, double tz, short rx, short ry, short rz) + { + double sx = Math.Sin(rx * Math.PI / 32768), cx = Math.Cos(rx * Math.PI / 32768); + double sy = Math.Sin(ry * Math.PI / 32768), cy = Math.Cos(ry * Math.PI / 32768); + double sz = Math.Sin(rz * Math.PI / 32768), cz = Math.Cos(rz * Math.PI / 32768); + return Affine( + cy * cz, cy * sz, -sy, + sx * sy * cz - cx * sz, sx * sy * sz + cx * cz, sx * cy, + cx * sy * cz + sx * sz, cx * sy * sz - sx * cz, cx * cy, + tx, ty, tz); + } + + static Matrix4 ScaleRows(Matrix4 m, Vector3 s) + { + m.Row0 *= s.X; + m.Row1 *= s.Y; + m.Row2 *= s.Z; + m.Row0.W = m.Row1.W = m.Row2.W = 0; + return m; + } + + #endregion + + #region the static level + + public static LevelModel Build(Rdram ram, uint currentAreaPointer, GameAddresses addresses) + { + var model = new LevelModel(); + model.area = ram.U32(currentAreaPointer); + if (!Rdram.IsPointer(model.area)) + return model; + uint root = ram.U32(model.area + 4); + if (!Rdram.IsPointer(root)) + return model; + model.rootNode = root; + model.addresses = addresses; + model.scheme = GraphScheme.Detect(ram, root); + + var dls = new List<(uint dl, int layer, Matrix4 m)>(); + model.CollectDisplayLists(ram, root, Matrix4.Identity, 0, dls); + model.displayListCount = dls.Count; + model.ucode = F3D.Detect(ram, dls.Select(d => d.dl)); + + var builder = new MeshBuilder(); + var f3d = model.NewF3D(ram, builder); + foreach (var (dl, layer, m) in dls) + { + f3d.geo = F3D.GEO_INIT; + f3d.Run(dl, layer, m); + } + + foreach (var (node, m) in model.movtexNodes) + { + f3d.geo = F3D.GEO_INIT; + model.EmitMovtex(ram, f3d, node, m); + } + + model.level = model.store.Add(builder); + model.ReadBackground(ram, root, addresses); + return model; + } + + const float PAINTING_SIZE = 614; + + + const int PAINTING_IDLE = 0, PAINTING_ENV_MAP = 1; + + public readonly List levelInstances = new List(); + + public readonly MeshStore frameStore = new MeshStore(); + + readonly List<(uint parameter, Matrix4 m)> paintingNodes = new List<(uint, Matrix4)>(); + + public bool TryGetGameCamera(Rdram ram, out Vector3 position, out Vector3 focus) + { + position = focus = Vector3.Zero; + if (!Rdram.IsPointer(cameraNode)) + return false; + position = new Vector3(ram.F32(cameraNode + 0x1C), ram.F32(cameraNode + 0x20), ram.F32(cameraNode + 0x24)); + focus = new Vector3(ram.F32(cameraNode + 0x28), ram.F32(cameraNode + 0x2C), ram.F32(cameraNode + 0x30)); + bool Finite(Vector3 v) => float.IsFinite(v.X) && float.IsFinite(v.Y) && float.IsFinite(v.Z); + return Finite(position) && Finite(focus) && (focus - position).LengthSquared > 1e-4f; + } + + public void UpdateLevelGeometry(Rdram ram, GameAddresses addresses) + { + this.addresses = addresses; + levelInstances.Clear(); + frameStore.length = 0; + defaultState ??= DefaultState(ram); + foreach (var (parameter, m) in paintingNodes) + EmitPainting(ram, parameter, m); + } + + void EmitPainting(Rdram ram, uint parameter, Matrix4 m) + { + uint group = ram.U32(addresses.paintingGroups + 4 * ((parameter >> 8) & 0xFF)); + if (group == 0) + return; + uint painting = ram.U32(group + 4 * (parameter & 0xFF)); + if (painting == 0 || !ram.Ok(ram.Phys(painting), 0x78)) + return; + float size = ram.F32(painting + 0x74); + if (!(size > 0)) + return; + + var transform = Matrix4.CreateScale(size / PAINTING_SIZE) + * Matrix4.CreateRotationY(MathHelper.DegreesToRadians(ram.F32(painting + 0x0C))) + * Matrix4.CreateRotationX(MathHelper.DegreesToRadians(ram.F32(painting + 0x08))) + * Matrix4.CreateTranslation(ram.F32(painting + 0x10), ram.F32(painting + 0x14), ram.F32(painting + 0x18)) + * m; + int layer = ram.U8(painting + 0x6D) == 0xFF ? LAYER_OPAQUE : LAYER_TRANSPARENT; + + if ((sbyte)ram.U8(painting + 0x07) != PAINTING_IDLE) + { + EmitRipplingPainting(ram, painting, layer, transform); + return; + } + + uint dl = ram.U32(painting + 0x58); + if (dl == 0) + return; + var key = (dl, 0x500 | layer, 0); + if (!objectMeshes.TryGetValue(key, out var entry)) + { + var builder = new MeshBuilder(); + var f3d = NewF3D(ram, builder); + f3d.Load(defaultState); + f3d.Run(dl, layer, Matrix4.Identity); + objectMeshes[key] = entry = (store.Add(builder), null); + } + + if (entry.mesh.triangleCount > 0) + levelInstances.Add(new Instance { mesh = entry.mesh, matrix = transform }); + } + + void EmitRipplingPainting(Rdram ram, uint painting, int layer, Matrix4 transform) + { + bool envMapped = (sbyte)ram.U8(painting + 0x03) == PAINTING_ENV_MAP; + var meshData = FindSegmentedConstants(ram, addresses.displayPaintingRippling, 0x02); + var beginEnd = FindSegmentedConstants(ram, envMapped ? addresses.paintingRippleEnvMapped : addresses.paintingRippleImage, 0x02); + if (meshData.Count < 2 || beginEnd.Count < 2) + return; + uint mesh = meshData[0], neighborTris = meshData[1]; + + int numVtx = Math.Clamp((int)ram.S16(mesh), 0, 1024); + int numTris = Math.Clamp((int)ram.S16(mesh + 2 * (uint)(numVtx * 3 + 1)), 0, 2048); + if (numVtx == 0 || numTris == 0) + return; + var pos = new short[numVtx, 3]; + var normal = new sbyte[numVtx, 3]; + + float sizeRatio = ram.F32(painting + 0x74) / PAINTING_SIZE; + float magnitude = ram.F32(painting + 0x1C), rate = ram.F32(painting + 0x34), dispersion = ram.F32(painting + 0x40); + float timer = ram.F32(painting + 0x4C), rippleX = ram.F32(painting + 0x50), rippleY = ram.F32(painting + 0x54); + for (int i = 0; i < numVtx; i++) + { + uint v = mesh + 2 * (uint)(i * 3 + 1); + pos[i, 0] = ram.S16(v); + pos[i, 1] = ram.S16(v + 2); + if (ram.S16(v + 4) == 0) + continue; + float x = pos[i, 0] * sizeRatio, y = pos[i, 1] * sizeRatio; + float distance = MathF.Sqrt((x - rippleX) * (x - rippleX) + (y - rippleY) * (y - rippleY)) / dispersion; + if (timer < distance) + continue; + float z = magnitude * (float)Math.Cos(rate * (2 * Math.PI) * (timer - distance)); + pos[i, 2] = (short)(z >= 0 ? z + 0.5 : z - 0.5); + } + + var triNormals = new Vector3[numTris]; + for (int i = 0; i < numTris; i++) + { + uint tri = mesh + 2 * (uint)(numVtx * 3 + i * 3 + 2); + int v0 = Math.Clamp((int)ram.S16(tri), 0, numVtx - 1); + int v1 = Math.Clamp((int)ram.S16(tri + 2), 0, numVtx - 1); + int v2 = Math.Clamp((int)ram.S16(tri + 4), 0, numVtx - 1); + float x0 = pos[v0, 0], y0 = pos[v0, 1], z0 = pos[v0, 2]; + float x1 = pos[v1, 0], y1 = pos[v1, 1], z1 = pos[v1, 2]; + float x2 = pos[v2, 0], y2 = pos[v2, 1], z2 = pos[v2, 2]; + triNormals[i] = new Vector3( + (y1 - y0) * (z2 - z1) - (z1 - z0) * (y2 - y1), + (z1 - z0) * (x2 - x1) - (x1 - x0) * (z2 - z1), + (x1 - x0) * (y2 - y1) - (y1 - y0) * (x2 - x1)); + } + + static sbyte Component(float c) => c > 0 ? (sbyte)(c * 127.0 + 0.5) : c < 0 ? (sbyte)(c * 128.0 - 0.5) : (sbyte)0; + uint entry = neighborTris; + for (int i = 0; i < numVtx; i++) + { + int neighbors = Math.Clamp((int)ram.S16(entry), 0, 64); + var sum = Vector3.Zero; + for (int j = 0; j < neighbors; j++) + sum += triNormals[Math.Clamp((int)ram.S16(entry + 2 * (uint)(j + 1)), 0, numTris - 1)]; + entry += 2 * (uint)(neighbors + 1); + if (neighbors == 0 || sum.Length == 0) + continue; + var n = sum.Normalized(); + normal[i, 0] = Component(n.X); + normal[i, 1] = Component(n.Y); + normal[i, 2] = Component(n.Z); + } + + uint rippleDl = ram.U32(painting + 0x68), textureMaps = ram.U32(painting + 0x5C), textureArray = ram.U32(painting + 0x60); + if (rippleDl == 0 || textureMaps == 0 || textureArray == 0) + return; + + var builder = new MeshBuilder(); + var f3d = NewF3D(ram, builder); + f3d.Load(defaultState); + f3d.Run(beginEnd[0], layer, transform); + f3d.Run(rippleDl, layer, transform); + + int images = envMapped ? 1 : Math.Clamp((int)(sbyte)ram.U8(painting + 0x02), 0, 16); + int width = ram.S16(painting + 0x64), height = ram.S16(painting + 0x66); + byte alpha = ram.U8(painting + 0x6D); + var vertices = new byte[3 * 16]; + for (int image = 0; image < images; image++) + { + uint map = ram.U32(textureMaps + 4 * (uint)image); + if (map == 0) + continue; + f3d.Command(0xFD000000 | 2 << 19, ram.U32(textureArray + 4 * (uint)image), layer, transform); + f3d.Command(0xF5000000 | 2 << 19, 0x07000000, layer, transform); + f3d.Command(0xF3000000, 0x07000000 | (uint)Math.Clamp(width * height - 1, 0, 4095) << 12, layer, transform); + + int mapVerts = Math.Clamp((int)ram.S16(map), 0, 1024); + int mapTris = Math.Clamp((int)ram.S16(map + 2 * (uint)(mapVerts * 3 + 1)), 0, 2048); + if (mapVerts == 0) + continue; + for (int t = 0; t < mapTris; t++) + { + for (int k = 0; k < 3; k++) + { + int mapping = Math.Clamp((int)ram.S16(map + 2 * (uint)(mapVerts * 3 + 2 + t * 3 + k)), 0, mapVerts - 1); + uint mapped = map + 2 * (uint)(mapping * 3 + 1); + int vtx = Math.Clamp((int)ram.S16(mapped), 0, numVtx - 1); + WriteVertex(vertices, k, pos[vtx, 0], pos[vtx, 1], pos[vtx, 2], ram.S16(mapped + 2), ram.S16(mapped + 4), + (byte)normal[vtx, 0], (byte)normal[vtx, 1], (byte)normal[vtx, 2], alpha); + } + + f3d.LoadVertices(vertices, 3, 0, transform); + f3d.Command(0xBF000000, 10 << 8 | 20, layer, transform); + } + } + + f3d.Run(beginEnd[beginEnd.Count - 1], layer, transform); + var built = frameStore.Add(builder); + if (built.triangleCount > 0) + levelInstances.Add(new Instance { mesh = built, matrix = Matrix4.Identity, frame = true }); + } + + const uint SAVE_FLAG_HAVE_WING_CAP = 1 << 1; + + static bool CastleLightVisible(Rdram ram, GameAddresses addresses) + { + if (addresses.hudDisplay == 0 || addresses.saveFileGetFlags == 0) + return false; + uint? flags = MipsCall.Call(ram, addresses.saveFileGetFlags); + return ram.S16(addresses.hudDisplay + 4) >= 10 && flags.HasValue && (flags.Value & SAVE_FLAG_HAVE_WING_CAP) == 0; + } + + public static int StaticStateKey(Rdram ram, GameAddresses addresses) => + addresses.geoExecInsideCastleLight != 0 && CastleLightVisible(ram, addresses) ? 1 : 0; + + static uint FindSegmentedConstant(Rdram ram, uint function, int segment) + { + var constants = FindSegmentedConstants(ram, function, segment); + return constants.Count > 0 ? constants[0] : 0; + } + + const uint JR_RA = 0x03E00008; + + static int WrittenRegister(uint i) + { + int op = (int)(i >> 26); + if (op == 0x00) + return (int)(i >> 11) & 31; + if (op == 0x03) + return 31; + if ((op >= 0x08 && op <= 0x0F) || (op >= 0x20 && op <= 0x27)) + return (int)(i >> 16) & 31; + return 0; + } + + static uint FindFirstHalfwordLoad(Rdram ram, uint function) + { + if (!Rdram.IsPointer(function)) + return 0; + var upper = new uint?[32]; + uint end = function + 4 * 512; + for (uint pc = function; pc < end; pc += 4) + { + uint i = ram.U32(pc); + int op = (int)(i >> 26), rs = (int)(i >> 21) & 31, rt = (int)(i >> 16) & 31; + uint imm = i & 0xFFFF; + if ((op == 0x21 || op == 0x25) && upper[rs].HasValue) + return upper[rs].Value + (uint)(short)imm; + upper[WrittenRegister(i)] = null; + if (op == 0x0F) + upper[rt] = (imm >> 8) == 0x80 ? imm << 16 : null; + else if (i == JR_RA) + end = pc + 8; + } + + return 0; + } + + static List FindSegmentedConstants(Rdram ram, uint function, int segment) + { + var found = new List(); + if (!Rdram.IsPointer(function)) + return found; + var upper = new uint?[32]; + uint end = function + 4 * 512; + for (uint pc = function; pc < end; pc += 4) + { + uint i = ram.U32(pc); + int op = (int)(i >> 26), rs = (int)(i >> 21) & 31, rt = (int)(i >> 16) & 31; + uint imm = i & 0xFFFF; + if ((op == 0x09 || op == 0x0D) && upper[rs].HasValue) + { + uint value = op == 0x09 ? upper[rs].Value + (uint)(short)imm : upper[rs].Value | imm; + if (found.Count == 0 || found[found.Count - 1] != value) + found.Add(value); + } + + upper[WrittenRegister(i)] = null; + if (op == 0x0F) + upper[rt] = (imm >> 8) == segment ? imm << 16 : null; + else if (i == JR_RA) + end = pc + 8; + } + + return found; + } + + + const int LAYER_TRANSPARENT_INTER = 7; + const uint MOVTEX_JRB_INITIAL_MIST = 0x1205, MOVTEX_HMC_TOXIC_MAZE_MIST = 0x0702, MOVTEX_SSL_TOXBOX_QUICKSAND_MIST = 0x0851; + const int MOVTEX_TEXTURE_MIST = 1; + const int G_IM_FMT_RGBA = 0, G_IM_FMT_IA = 3; + const uint MOVTEX_OBJECT_SIZE = 0x24, MOVTEX_QUAD_SIZE = 0x1C, MOVTEX_REGION_SIZE = 12; + + readonly List<(uint node, Matrix4 m)> movtexNodes = new List<(uint, Matrix4)>(); + + public class UvAnimation + { + public uint address; + public bool rotate; + public short built; + public Vector2 scale; + public Matrix3 transform = Matrix3.Identity; + } + + public readonly List uvAnimations = new List(); + + int AddUvAnimation(Rdram ram, uint address, bool rotate, Vector2 scale) + { + uvAnimations.Add(new UvAnimation { address = address, rotate = rotate, built = ram.S16(address), scale = scale }); + return uvAnimations.Count - 1; + } + + public void UpdateTextureAnimations(Rdram ram) + { + foreach (var a in uvAnimations) + { + int delta = (short)(ram.S16(a.address) - a.built); + if (a.rotate) + { + float angle = (float)(delta / 65536.0 * 2 * Math.PI), sin = MathF.Sin(angle), cos = MathF.Cos(angle); + a.transform = new Matrix3(cos, -sin, 0, sin, cos, 0, 0, 0, 1); + } + else + a.transform = new Matrix3(1, 0, 0, 0, 1, 0, delta * a.scale.X, 0, 1); + } + } + + bool IsMovtex(uint function) => function != 0 && (function == addresses.geoMovtexDrawWaterRegions + || function == addresses.geoMovtexDrawNoColor + || function == addresses.geoMovtexDrawColored + || function == addresses.geoMovtexDrawColoredNoUpdate + || function == addresses.geoMovtexDrawColored2NoUpdate); + + void EmitMovtex(Rdram ram, F3D f3d, uint node, Matrix4 m) + { + uint function = ram.U32(node + 0x14), parameter = ram.U32(node + 0x18); + if (addresses.movtexIdToTexture == 0) + return; + if (function == addresses.geoMovtexDrawWaterRegions) + EmitWaterRegions(ram, f3d, parameter, m); + else if (function == addresses.geoMovtexDrawNoColor) + EmitMovtexObject(ram, f3d, addresses.movtexNonColored, parameter, false, m); + else if (function == addresses.geoMovtexDrawColored || function == addresses.geoMovtexDrawColoredNoUpdate) + EmitMovtexObject(ram, f3d, addresses.movtexColored, parameter, true, m); + else if (function == addresses.geoMovtexDrawColored2NoUpdate) + EmitMovtexObject(ram, f3d, addresses.movtexColored2, parameter, true, m); + } + + void EmitWaterRegions(Rdram ram, F3D f3d, uint parameter, Matrix4 m) + { + uint regions = ram.U32(addresses.environmentRegions); + if (!Rdram.IsPointer(regions) || parameter == MOVTEX_JRB_INITIAL_MIST) + return; + uint? collection = MipsCall.Call(ram, addresses.movtexQuadCollectionFromId, parameter); + if (!collection.HasValue || collection.Value == 0) + return; + + bool mist = parameter == MOVTEX_JRB_INITIAL_MIST || parameter == MOVTEX_HMC_TOXIC_MAZE_MIST || parameter == MOVTEX_SSL_TOXBOX_QUICKSAND_MIST; + byte r = 255, g = 255, b = 255; + if (parameter == MOVTEX_HMC_TOXIC_MAZE_MIST) + b = 0; + else if (parameter == MOVTEX_SSL_TOXBOX_QUICKSAND_MIST) + g = b = 0; + + const int layer = LAYER_TRANSPARENT_INTER; + f3d.Command(0xB6000000, 0x00022000, layer, m); + f3d.Command(0xFC121824, 0xFF33FFFF, layer, m); + f3d.Command(0xBB000001, 0xFFFFFFFF, layer, m); + f3d.Command(0xF5000000 | (uint)(mist ? G_IM_FMT_IA : G_IM_FMT_RGBA) << 21 | 2 << 19 | 8 << 9, 0x00014050, layer, m); + f3d.Command(0xF2000000, 0x0007C07C, layer, m); + + int count = Math.Min((int)ram.S16(regions), 64); + var vertices = new byte[4 * 16]; + for (int i = 0; i < count; i++) + { + uint region = regions + 2 + (uint)i * MOVTEX_REGION_SIZE; + short id = ram.S16(region), y = ram.S16(region + 10); + uint quads = FindQuadArray(ram, collection.Value, id); + if (quads == 0) + continue; + int quadCount = Math.Min((int)ram.S16(quads), 64); + for (int q = 0; q < quadCount; q++) + { + uint quad = quads + 2 + (uint)q * MOVTEX_QUAD_SIZE; + short rot = ram.S16(quad), scale = ram.S16(quad + 4); + byte alpha = (byte)ram.S16(quad + 24); + int textureId = ram.S16(quad + 26); + int direction = ram.S16(quad + 22) == 0 ? 1 : -1; + for (int v = 0; v < 4; v++) + { + int rotOffset = v == 0 ? 0 : v == 2 ? -32768 : direction * (v == 1 ? 16384 : -16384); + double angle = (ushort)(short)(rot + rotOffset) / 65536.0 * 2 * Math.PI; + double radius = 32.0 * (32.0 * scale - 1.0); + WriteVertex(vertices, v, ram.S16(quad + 6 + 4 * (uint)v), y, ram.S16(quad + 8 + 4 * (uint)v), + (short)(radius * Math.Sin(angle)), (short)(radius * Math.Cos(angle)), r, g, b, alpha); + } + + LoadMovtexTexture(ram, f3d, textureId, textureId == MOVTEX_TEXTURE_MIST ? G_IM_FMT_IA : G_IM_FMT_RGBA, layer, m); + f3d.LoadVertices(vertices, 4, 0, m); + f3d.uvAnimation = AddUvAnimation(ram, quad, true, Vector2.Zero); + f3d.Command(0xBF000000, 10 << 8 | 20, layer, m); + f3d.Command(0xBF000000, 20 << 8 | 30, layer, m); + f3d.uvAnimation = -1; + } + } + } + + static uint FindQuadArray(Rdram ram, uint collection, short id) + { + for (uint i = 0; i < 64; i++) + { + short entryId = ram.S16(collection + 8 * i); + if (entryId == -1) + return 0; + if (entryId == id) + return ram.U32(collection + 8 * i + 4); + } + + return 0; + } + + void EmitMovtexObject(Rdram ram, F3D f3d, uint table, uint parameter, bool colored, Matrix4 m) + { + if (table == 0) + return; + for (uint entry = table; entry < table + 64 * MOVTEX_OBJECT_SIZE; entry += MOVTEX_OBJECT_SIZE) + { + uint verts = ram.U32(entry + 0x0C); + if (verts == 0) + return; + if (ram.U32(entry) != parameter) + continue; + + int textureId = ram.S32(entry + 4), count = Math.Clamp(ram.S32(entry + 8), 0, 16); + uint beginDl = ram.U32(entry + 0x10), endDl = ram.U32(entry + 0x14), triDl = ram.U32(entry + 0x18); + byte r = ram.U8(entry + 0x1C), g = ram.U8(entry + 0x1D), b = ram.U8(entry + 0x1E), a = ram.U8(entry + 0x1F); + int layer = ram.S32(entry + 0x20); + + uint stride = colored ? 8u : 5u, st = colored ? 7u : 4u; + short baseS = ram.S16(verts + 2 * st), baseT = ram.S16(verts + 2 * (st + 1)); + var vertices = new byte[count * 16]; + for (int i = 0; i < count; i++) + { + uint v = verts + 2 * (uint)i * stride; + short s = baseS, t = baseT; + if (i > 0) + { + s = (short)(baseS + ram.S16(v + 2 * st) * 1024); + t = (short)(baseT + ram.S16(v + 2 * (st + 1)) * 1024); + } + + if (colored) + (r, g, b) = ((byte)ram.S16(v + 8), (byte)ram.S16(v + 10), (byte)ram.S16(v + 12)); + WriteVertex(vertices, i, ram.S16(v + 2), ram.S16(v + 4), ram.S16(v + 6), s, t, r, g, b, a); + } + + f3d.Run(beginDl, layer, m); + LoadMovtexTexture(ram, f3d, textureId, G_IM_FMT_RGBA, layer, m); + f3d.LoadVertices(vertices, count, 0, m); + f3d.uvAnimation = AddUvAnimation(ram, verts + 2 * st, false, f3d.TexCoordScale()); + f3d.Run(triDl, layer, m); + f3d.uvAnimation = -1; + f3d.Run(endDl, layer, m); + return; + } + } + + void LoadMovtexTexture(Rdram ram, F3D f3d, int textureId, int format, int layer, in Matrix4 m) + { + uint texture = ram.U32(addresses.movtexIdToTexture + 4 * (uint)textureId); + f3d.Command(0xFD000000 | (uint)format << 21 | 2 << 19, texture, layer, m); + f3d.Command(0xF5000000 | (uint)format << 21 | 2 << 19, 0x07000000, layer, m); + f3d.Command(0xF3000000, 0x07000000 | 1023 << 12, layer, m); + } + + static void WriteVertex(byte[] vertices, int index, short x, short y, short z, short s, short t, byte r, byte g, byte b, byte a) + { + int o = index * 16; + void S16(int at, short value) + { + vertices[o + at] = (byte)(value >> 8); + vertices[o + at + 1] = (byte)value; + } + + S16(0, x); + S16(2, y); + S16(4, z); + S16(6, 0); + S16(8, s); + S16(10, t); + vertices[o + 12] = r; + vertices[o + 13] = g; + vertices[o + 14] = b; + vertices[o + 15] = a; + } + + const int SKYBOX_COLS = 10, SKYBOX_ROWS = 8; + + void ReadBackground(Rdram ram, uint root, GameAddresses addresses) + { + var stack = new Stack(); + var seen = new HashSet(); + stack.Push(root); + while (stack.Count > 0 && seen.Count < 4096) + { + uint n = stack.Pop(); + if (!Rdram.IsPointer(n) || !seen.Add(n)) + continue; + if (NodeType(ram, n) == GN_BG) + { + uint function = ram.U32(n + 0x14), background = ram.U32(n + 0x1C); + if (function == 0) + { + int c = (int)(background & 0xFFFF); + backgroundColor = new Vector3(((c >> 11) & 31) / 31f, ((c >> 6) & 31) / 31f, ((c >> 1) & 31) / 31f); + } + else if (addresses.skyboxTextures != 0) + { + uint tiles = ram.U32(addresses.skyboxTextures + 4 * (background & 0xFF)); + var image = new byte[SKYBOX_COLS * 32 * SKYBOX_ROWS * 32 * 4]; + for (int t = 0; t < SKYBOX_COLS * SKYBOX_ROWS; t++) + { + byte[] tile = TextureSet.DecodeTexture(ram, ram.U32(tiles + (uint)(4 * t)), 0, 2, 32, 32, null, 0, 0); + int x0 = t % SKYBOX_COLS * 32, y0 = t / SKYBOX_COLS * 32; + for (int y = 0; y < 32; y++) + Buffer.BlockCopy(tile, y * 128, image, ((y0 + y) * SKYBOX_COLS * 32 + x0) * 4, 128); + } + + skybox = new Texture { width = SKYBOX_COLS * 32, height = SKYBOX_ROWS * 32, rgba = image, wrapS = 0, wrapT = 2 }; + } + + return; + } + + foreach (uint c in Children(ram, n)) + stack.Push(c); + } + } + + static IEnumerable Children(Rdram ram, uint n) + { + uint first = ram.U32(n + 0x10), c = first; + for (int i = 0; i < 1024 && Rdram.IsPointer(c); i++) + { + yield return c; + c = ram.U32(c + 0x08); + if (c == first) + break; + } + } + + void CollectDisplayLists(Rdram ram, uint n, Matrix4 m, int depth, List<(uint, int, Matrix4)> output) + { + if (depth > 40) + return; + int t = NodeType(ram, n); + ushort flags = ram.U16(n + 2); + if ((flags & GRAPH_RENDER_ACTIVE) == 0) + return; + if (t == GN_CAMERA) + cameraNode = n; + if (t == GN_PERSPECTIVE) + perspectivePlanes = new Vector2(ram.U16(n + scheme.perspectiveNear), ram.U16(n + scheme.perspectiveNear + 2)); + if (t == GN_OBJPARENT) + objectGroup = ram.U32(n + 0x14); + if (t == GN_GENLIST && addresses.geoPaintingDraw != 0 && ram.U32(n + 0x14) == addresses.geoPaintingDraw) + paintingNodes.Add((ram.U32(n + 0x18), m)); + if (t == GN_GENLIST && IsMovtex(ram.U32(n + 0x14))) + movtexNodes.Add((n, m)); + if (t == GN_GENLIST && addresses.geoRenderMirrorMario != 0 && ram.U32(n + 0x14) == addresses.geoRenderMirrorMario) + mirrorParent = n; + if (t == GN_GENLIST && addresses.geoEnvFxMain != 0 && ram.U32(n + 0x14) == addresses.geoEnvFxMain) + envFxNode = n; + if (t == GN_GENLIST && addresses.geoExecInsideCastleLight != 0 && ram.U32(n + 0x14) == addresses.geoExecInsideCastleLight + && CastleLightVisible(ram, addresses)) + { + uint lightDl = FindSegmentedConstant(ram, addresses.geoExecInsideCastleLight, 0x07); + if (lightDl != 0) + output.Add((lightDl, LAYER_TRANSPARENT, m)); + } + if (t == GN_OBJPARENT || t == GN_GENLIST || t == GN_BG || t == GN_HELD || t == GN_OBJECT || t == GN_SHADOW || t == GN_ORTHO) + return; + int layer = scheme.Layer((flags >> 8) & 0xFF); + uint dl = 0; + switch (t) + { + case GN_DL: + dl = ram.U32(n + 0x14); + break; + case GN_TRANSROT: + m = RotZXYTrans(ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C), ram.S16(n + 0x1E), ram.S16(n + 0x20), ram.S16(n + 0x22)) * m; + dl = ram.U32(n + 0x14); + break; + case GN_TRANS: + case GN_ANIMPART: + case GN_BILLBOARD: + m = RotZXYTrans(ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C), 0, 0, 0) * m; + dl = ram.U32(n + 0x14); + break; + case GN_ROT: + m = RotZXYTrans(0, 0, 0, ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C)) * m; + dl = ram.U32(n + 0x14); + break; + case GN_SCALE: + m = Matrix4.CreateScale(ram.F32(n + 0x18)) * m; + dl = ram.U32(n + 0x14); + break; + } + + if (dl != 0) + output.Add((dl, layer, m)); + if (t == GN_LOD && !(ram.S16(n + 0x14) <= 0 && 0 < ram.S16(n + 0x16))) + return; + + foreach (uint c in Children(ram, n)) + CollectDisplayLists(ram, c, m, depth + 1, output); + } + + #endregion + + #region the objects (Mario included), as rendered on the last frame + + const int ANIM_NONE = 0, ANIM_TRANSLATION = 1, ANIM_VERTICAL_TRANSLATION = 2, ANIM_LATERAL_TRANSLATION = 3, ANIM_NO_TRANSLATION = 4, ANIM_ROTATION = 5; + + struct AnimState + { + public int type, frame; + public uint attributes, values; + public float translationMultiplier; + } + + AnimState anim; + Vector3 gameCamera, gameCameraForward; + + float LodDistance(Vector3 position) => + scheme.EmulatorAutoLod ? 50 + : gameCameraForward != Vector3.Zero ? Vector3.Dot(position - gameCamera, gameCameraForward) + : (position - gameCamera).Length; + byte areaIndex; + int nodeBudget; + GameAddresses addresses; + uint currentObject, marioObject, mirrorMario; + bool inHeldObject; + ISet roomCulledObjects; + + const int ACTIVE_FLAG_ACTIVE = 0x01, ACTIVE_FLAG_IN_DIFFERENT_ROOM = 0x08; + + public void UpdateObjects(Rdram ram, GameAddresses addresses, ISet roomCulledObjects = null) + { + instances.Clear(); + shadows.Clear(); + if (!Rdram.IsPointer(objectGroup)) + return; + this.addresses = addresses; + this.roomCulledObjects = roomCulledObjects; + defaultState ??= DefaultState(ram); + for (int i = 0; i < layerStates.Length; i++) + layerStates[i] = defaultState; + marioObject = ram.U32(addresses.marioObjectPointer); + areaIndex = ram.U8(rootNode + 0x14); + gameCamera = Rdram.IsPointer(cameraNode) + ? new Vector3(ram.F32(cameraNode + 0x1C), ram.F32(cameraNode + 0x20), ram.F32(cameraNode + 0x24)) + : Vector3.Zero; + var focus = Rdram.IsPointer(cameraNode) + ? new Vector3(ram.F32(cameraNode + 0x28), ram.F32(cameraNode + 0x2C), ram.F32(cameraNode + 0x30)) + : Vector3.Zero; + gameCameraForward = (focus - gameCamera).LengthSquared > 0 ? (focus - gameCamera).Normalized() : Vector3.Zero; + marioInstances.Clear(); + marioInstancesStart = -1; + foreach (uint obj in Children(ram, objectGroup)) + { + nodeBudget = 4096; + int first = instances.Count; + DrawObject(ram, obj); + if (obj == marioObject) + { + marioInstances.AddRange(instances.GetRange(first, instances.Count - first)); + marioInstancesStart = first; + marioModelPosition = new Vector3(ram.F32(obj + 0x20), ram.F32(obj + 0x24), ram.F32(obj + 0x28)); + } + } + + if (envFxNode != 0) + EmitEnvFx(ram); + + if (Rdram.IsPointer(mirrorParent)) + foreach (uint obj in Children(ram, mirrorParent)) + { + mirrorMario = obj; + nodeBudget = 4096; + DrawObject(ram, obj); + } + } + + void DrawObject(Rdram ram, uint n) + { + if (NodeType(ram, n) != GN_OBJECT) + return; + ushort flags = ram.U16(n + 2); + if ((flags & GRAPH_RENDER_INVISIBLE) != 0) + return; + if ((flags & GRAPH_RENDER_ACTIVE) == 0) + { + if (n == mirrorMario) + return; + int activeFlags = ram.U16(n + 0x74); + bool roomCulled = (activeFlags & ACTIVE_FLAG_IN_DIFFERENT_ROOM) != 0 || (roomCulledObjects?.Contains(n) ?? false); + if ((activeFlags & ACTIVE_FLAG_ACTIVE) == 0 || !roomCulled) + return; + } + + if (ram.U8(n + 0x18) != areaIndex) + return; + uint sharedChild = ram.U32(n + 0x14); + if (!Rdram.IsPointer(sharedChild)) + return; + + var pos = new Vector3(ram.F32(n + 0x20), ram.F32(n + 0x24), ram.F32(n + 0x28)); + var scale = new Vector3(ram.F32(n + 0x2C), ram.F32(n + 0x30), ram.F32(n + 0x34)); + var frame = new Frame(); + uint throwMatrix = ram.U32(n + 0x50); + if (Rdram.IsPointer(throwMatrix)) + frame.matrix = ScaleRows(ReadMatrix(ram, throwMatrix), scale); + else if ((flags & GRAPH_RENDER_BILLBOARD) != 0) + { + frame.matrix = Matrix4.Identity; + frame.billboard = true; + frame.anchor = pos; + frame.anchorScale = scale; + } + else + { + var angle = (ram.S16(n + 0x1A), ram.S16(n + 0x1C), ram.S16(n + 0x1E)); + frame.matrix = ScaleRows(RotZXYTrans(pos.X, pos.Y, pos.Z, angle.Item1, angle.Item2, angle.Item3), scale); + } + + frame.objectMatrix = frame.matrix; + currentObject = n; + SetAnimation(ram, n); + DrawNodeAndSiblings(ram, sharedChild, frame, 0); + anim.type = ANIM_NONE; + } + + void AddShadow(Rdram ram, uint n, bool held, in Frame frame) + { + float size = ram.S16(n + 0x14); + byte solidity = ram.U8(n + 0x16), type = ram.U8(n + 0x17); + Vector3 position; + if (held) + position = frame.WorldPosition; + else + { + position = new Vector3(ram.F32(currentObject + 0x20), ram.F32(currentObject + 0x24), ram.F32(currentObject + 0x28)); + size *= ram.F32(currentObject + 0x2C); + } + + float floor = IsMario && addresses.marioState != 0 + ? ram.F32(addresses.marioState + 0x70) + : ram.F32(currentObject + 0xE8); + float height = position.Y - floor; + if (float.IsNaN(floor) || floor < -10000 || height < -50 || height > 2000) + return; + + float opacity = solidity / 255f; + if (height > 0) + { + size *= height >= 600 ? 0.5f : 1 - 0.5f * height / 600; + if (height > 600) + opacity *= Math.Max(0, 1 - (height - 600) / 600); + } + + if (size > 0 && opacity > 0) + shadows.Add(new Shadow + { + position = new Vector3(position.X, floor, position.Z), + size = size, + opacity = opacity, + square = type >= 10 && type <= 12, + }); + } + + static Matrix4 ReadMatrix(Rdram ram, uint a) + { + var m = new Matrix4(); + for (int r = 0; r < 4; r++) + for (int c = 0; c < 4; c++) + m[r, c] = ram.F32(a + (uint)(16 * r + 4 * c)); + return m; + } + + void SetAnimation(Rdram ram, uint objectNode) + { + anim = default; + uint curAnim = ram.U32(objectNode + 0x3C); + if (!Rdram.IsPointer(curAnim) && (curAnim >> 24) == 0) + return; + int animFlags = ram.U16(curAnim); + short divisor = ram.S16(curAnim + 0x02); + if ((animFlags & (1 << 3)) != 0) + anim.type = ANIM_VERTICAL_TRANSLATION; + else if ((animFlags & (1 << 4)) != 0) + anim.type = ANIM_LATERAL_TRANSLATION; + else if ((animFlags & (1 << 6)) != 0) + anim.type = ANIM_NO_TRANSLATION; + else + anim.type = ANIM_TRANSLATION; + anim.frame = ram.S16(objectNode + 0x40); + anim.values = ram.U32(curAnim + 0x0C); + anim.attributes = ram.U32(curAnim + 0x10); + anim.translationMultiplier = divisor == 0 ? 1 : ram.S16(objectNode + 0x3A) / (float)divisor; + } + + short NextAnimationValue(Rdram ram) + { + int count = ram.U16(anim.attributes), start = ram.U16(anim.attributes + 2); + int index = anim.frame < count ? start + anim.frame : start + count - 1; + anim.attributes += 4; + return ram.S16(anim.values + (uint)(2 * index)); + } + + struct Frame + { + public Matrix4 matrix, objectMatrix; + public bool billboard; + public Vector3 anchor, anchorScale; + + public Vector3 WorldPosition => billboard ? anchor + matrix.Row3.Xyz : matrix.Row3.Xyz; + } + + void DrawNodeAndSiblings(Rdram ram, uint first, Frame frame, int depth) + { + uint c = first; + for (int i = 0; i < 256 && Rdram.IsPointer(c); i++) + { + DrawNode(ram, c, frame, depth); + c = ram.U32(c + 0x08); + if (c == first) + break; + } + } + + void Emit(uint dl, int layer, in Frame frame, Rdram ram) + { + if (!Rdram.IsPointer(dl) && (dl >> 24) == 0) + return; + int slot = layer & 7; + var incoming = layerStates[slot]; + var key = (dl, layer, incoming.Hash); + if (!objectMeshes.TryGetValue(key, out var entry)) + { + var builder = new MeshBuilder(); + var f3d = NewF3D(ram, builder); + f3d.Load(incoming); + f3d.Run(dl, layer, Matrix4.Identity); + objectMeshes[key] = entry = (store.Add(builder), f3d.Save()); + } + + layerStates[slot] = entry.state; + if (entry.mesh.triangleCount > 0) + instances.Add(new Instance { mesh = entry.mesh, matrix = frame.matrix, billboard = frame.billboard, anchor = frame.anchor, anchorScale = frame.anchorScale, cap = inCap }); + } + + bool inCap; + + void DrawNode(Rdram ram, uint n, Frame frame, int depth) + { + if (depth > 64 || --nodeBudget < 0) + return; + int t = NodeType(ram, n); + ushort flags = ram.U16(n + 2); + bool active = IsMario && !IsGhost && capWings.Contains(n) ? wingsShown : (flags & GRAPH_RENDER_ACTIVE) != 0; + if (!active) + return; + int layer = scheme.Layer((flags >> 8) & 0xFF); + uint dl = 0; + switch (t) + { + case GN_DL: + dl = ram.U32(n + 0x14); + break; + case GN_TRANSROT: + frame.matrix = RotZXYTrans(ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C), ram.S16(n + 0x1E), ram.S16(n + 0x20), ram.S16(n + 0x22)) * frame.matrix; + dl = ram.U32(n + 0x14); + break; + case GN_TRANS: + frame.matrix = RotZXYTrans(ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C), 0, 0, 0) * frame.matrix; + dl = ram.U32(n + 0x14); + break; + case GN_ROT: + frame.matrix = RotZXYTrans(0, 0, 0, ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C)) * frame.matrix; + dl = ram.U32(n + 0x14); + break; + case GN_SCALE: + frame.matrix = Matrix4.CreateScale(ram.F32(n + 0x18)) * frame.matrix; + dl = ram.U32(n + 0x14); + break; + case GN_ANIMPART: + frame.matrix = AnimatedPart(ram, n) * frame.matrix; + dl = ram.U32(n + 0x14); + break; + case GN_BILLBOARD: + var position = Vector3.TransformPosition(new Vector3(ram.S16(n + 0x18), ram.S16(n + 0x1A), ram.S16(n + 0x1C)), frame.matrix); + frame.anchor = frame.billboard ? frame.anchor + position : position; + frame.anchorScale = Vector3.One; + frame.billboard = true; + frame.matrix = Matrix4.Identity; + dl = ram.U32(n + 0x14); + break; + case GN_SHADOW: + AddShadow(ram, n, inHeldObject, frame); + break; + case GN_SWITCH: + int selected = ram.S16(n + 0x1E), i = 0; + uint switchFunction = ram.U32(n + 0x14); + bool capSwitch = false; + if (addresses.geoSwitchAnimState != 0 && switchFunction == addresses.geoSwitchAnimState) + { + selected = ram.S32(currentObject + 0xF0); + if (selected >= ram.S16(n + 0x1C) || selected < 0) + selected = 0; + } + else if (addresses.geoSwitchMarioCapEffect != 0 && switchFunction == addresses.geoSwitchMarioCapEffect) + selected = MarioCapEffect(ram); + else if (IsCapSwitch(ram, n, switchFunction)) + { + selected = CapSwitchCase(ram, n); + capSwitch = true; + } + + uint selectedChild = ram.U32(n + 0x10); + for (i = 0; Rdram.IsPointer(selectedChild) && selected > i && i < 256; i++) + selectedChild = ram.U32(selectedChild + 0x08); + bool wasInCap = inCap; + inCap |= capSwitch; + if (Rdram.IsPointer(selectedChild)) + DrawNode(ram, selectedChild, frame, depth + 1); + inCap = wasInCap; + return; + case GN_LOD: + float distance = LodDistance(frame.WorldPosition); + if (!(ram.S16(n + 0x14) <= distance && distance < ram.S16(n + 0x16))) + return; + break; + case GN_HELD: + if (IsMario && !inHeldObject) + DrawHeldObject(ram, n, frame, depth); + return; + case GN_GENLIST: + uint generator = ram.U32(n + 0x14); + if (addresses.coloredHatsGenerator != 0 && generator == addresses.coloredHatsGenerator) + EmitColoredHat(ram, layer, frame); + else if (addresses.geoMirrorMarioSetAlpha != 0 && generator == addresses.geoMirrorMarioSetAlpha) + SetMarioAlpha(ram); + else if (addresses.geoUpdateLayerTransparency != 0 && generator == addresses.geoUpdateLayerTransparency) + { + int opacity = ram.S32(currentObject + 0x17C) & 0xFF; + int target = ram.U32(n + 0x18) == 20 ? 6 : opacity == 0xFF ? LAYER_OPAQUE : LAYER_TRANSPARENT; + SetEnvAlpha(ram, target, opacity); + } + else if (IsMovtex(generator)) + EmitObjectMovtex(ram, n, generator, layer, frame); + else if (addresses.geoExecFlyingCarpetCreate != 0 && generator == addresses.geoExecFlyingCarpetCreate) + EmitFlyingCarpet(ram, frame); + else if (addresses.geoBitsBowserColoring != 0 && generator == addresses.geoBitsBowserColoring) + { + int slot = (ram.S32(currentObject + 0x17C) & 0xFF) == 0xFF ? LAYER_OPAQUE : LAYER_TRANSPARENT; + if (ram.S16(currentObject + 0x1B2) != 0) + ClearLighting(ram, slot); + } + + break; + case GN_OBJECT: + case GN_OBJPARENT: + case GN_BG: + return; + } + + if (dl != 0) + Emit(dl, layer, frame, ram); + uint children = ram.U32(n + 0x10); + if (Rdram.IsPointer(children)) + DrawNodeAndSiblings(ram, children, frame, depth + 1); + } + + const int LAYER_OPAQUE = 1, LAYER_TRANSPARENT = 5; + + bool IsMario => currentObject == marioObject || (currentObject == mirrorMario && mirrorMario != 0); + + bool IsGhost => addresses.ghostHack && !IsMario; + + uint BodyState(Rdram ram) => + addresses.bodyStates != 0 ? addresses.bodyStates + : addresses.marioState != 0 ? ram.U32(addresses.marioState + 0x98) + : 0; + + int MarioCapEffect(Rdram ram) => + IsGhost ? (sbyte)ram.U8(currentObject + 0x61) : ram.U16(BodyState(ram) + 8) >> 8; + + const int MARIO_HAS_DEFAULT_CAP_OFF = 1, MARIO_HAS_WING_CAP_ON = 2; + + readonly HashSet capWings = new HashSet(); + bool wingsShown; + + bool IsCapSwitch(Rdram ram, uint n, uint function) + { + if (!IsMario || IsGhost) + return false; + if (addresses.geoSwitchMarioCapOnOff != 0) + return function == addresses.geoSwitchMarioCapOnOff; + for (uint s = ram.U32(n + 0x08), i = 0; s != n && Rdram.IsPointer(s) && i < 64; s = ram.U32(s + 0x08), i++) + if (NodeType(ram, s) == GN_TRANSROT) + return true; + return false; + } + + int CapSwitchCase(Rdram ram, uint n) + { + uint bodyState = BodyState(ram); + int capState = Rdram.IsPointer(bodyState) ? (sbyte)ram.U8(bodyState + 4) : 0; + wingsShown = (capState & MARIO_HAS_WING_CAP_ON) != 0; + for (uint s = ram.U32(n + 0x08), i = 0; s != n && Rdram.IsPointer(s) && i < 64; s = ram.U32(s + 0x08), i++) + if (NodeType(ram, s) == GN_TRANSROT) + capWings.Add(s); + return capState & MARIO_HAS_DEFAULT_CAP_OFF; + } + + readonly Dictionary<(int state, int alpha), F3D.State> alphaStates = new Dictionary<(int, int), F3D.State>(); + + void SetMarioAlpha(Rdram ram) + { + int alpha; + if (IsGhost) + alpha = (sbyte)ram.U8(currentObject + 0x61) != 0 ? 0x7F : 0xFF; + else + { + int modelState = ram.U16(BodyState(ram) + 8); + alpha = (modelState & 0x100) != 0 ? modelState & 0xFF : 0xFF; + } + + SetEnvAlpha(ram, alpha == 0xFF ? LAYER_OPAQUE : LAYER_TRANSPARENT, alpha); + } + + void SetEnvAlpha(Rdram ram, int slot, int alpha) + { + slot &= 7; + var key = (layerStates[slot].Hash, alpha); + if (!alphaStates.TryGetValue(key, out var state)) + { + var f3d = NewF3D(ram, new MeshBuilder()); + f3d.Load(layerStates[slot]); + f3d.SetEnvColor(255, 255, 255, (byte)alpha); + alphaStates[key] = state = f3d.Save(); + } + + layerStates[slot] = state; + } + + + const int ENVFX_SNOW_NORMAL = 1, ENVFX_SNOW_WATER = 2, ENVFX_SNOW_BLIZZARD = 3, ENVFX_BUBBLE_START = 10; + const int ENVFX_FLOWERS = 11, ENVFX_LAVA_BUBBLES = 12, ENVFX_WHIRLPOOL_BUBBLES = 13, ENVFX_JETSTREAM_BUBBLES = 14; + const uint ENVFX_PARTICLE_SIZE = 0x38; + const uint DL_SNOW_GRAY_BEGIN = 0x0B006A50, DL_SNOW_BLUE_BEGIN = 0x0B006CD8, DL_PARTICLES_END = 0x0B006AB0; + const uint DL_BUBBLES_BEGIN = 0x0B006D38, DL_BUBBLE_TEXTURE = 0x0B006D68; + const uint FLOWER_TEXTURES = 0x0B002008, LAVA_BUBBLE_TEXTURES = 0x0B006020, BUBBLE_TEXTURES = 0x0B006848; + + void EmitEnvFx(Rdram ram) + { + if (addresses.envFxMode == 0 || addresses.envFxBuffer == 0) + return; + int mode = (sbyte)ram.U8(addresses.envFxMode); + uint buffer = ram.U32(addresses.envFxBuffer); + if (mode == 0 || !Rdram.IsPointer(buffer) || (ram.U16(envFxNode + 2) & GRAPH_RENDER_ACTIVE) == 0) + return; + + bool bubbles = mode >= ENVFX_BUBBLE_START; + uint countAddress = bubbles ? addresses.bubbleParticleMaxCount : addresses.snowParticleCount; + if (countAddress == 0) + return; + int count = bubbles ? ram.S32(countAddress) : ram.S16(countAddress); + count = Math.Clamp((count + 4) / 5 * 5, 0, 200); + for (int i = 0; i < count; i++) + { + uint particle = buffer + (uint)i * ENVFX_PARTICLE_SIZE; + var mesh = EnvFxMesh(ram, mode, bubbles ? ram.S16(buffer + (uint)(i - i % 5) * ENVFX_PARTICLE_SIZE + 2) : 0); + if (mesh == null) + return; + var position = new Vector3(ram.S32(particle + 4), ram.S32(particle + 8), ram.S32(particle + 12)); + instances.Add(new Instance { mesh = mesh, matrix = Matrix4.Identity, billboard = true, anchor = position, anchorScale = Vector3.One }); + } + } + + Mesh EnvFxMesh(Rdram ram, int mode, int frame) + { + const int layer = 4; + var key = ((uint)frame, 0x300 | layer, mode); + if (objectMeshes.TryGetValue(key, out var entry)) + return entry.mesh; + + uint begin, texture = 0; + short spread, height; + (short s, short t)[] st; + byte shade; + switch (mode) + { + case ENVFX_SNOW_NORMAL: + case ENVFX_SNOW_BLIZZARD: + case ENVFX_SNOW_WATER: + begin = mode == ENVFX_SNOW_WATER ? DL_SNOW_BLUE_BEGIN : DL_SNOW_GRAY_BEGIN; + break; + case ENVFX_FLOWERS: + case ENVFX_LAVA_BUBBLES: + case ENVFX_WHIRLPOOL_BUBBLES: + case ENVFX_JETSTREAM_BUBBLES: + begin = DL_BUBBLES_BEGIN; + uint textures = mode == ENVFX_FLOWERS ? FLOWER_TEXTURES : mode == ENVFX_LAVA_BUBBLES ? LAVA_BUBBLE_TEXTURES : BUBBLE_TEXTURES; + texture = ram.U32(textures + 4 * (uint)(mode == ENVFX_FLOWERS || mode == ENVFX_LAVA_BUBBLES ? Math.Clamp(frame, 0, 63) : 0)); + break; + default: + return null; + } + + var vertices = new byte[3 * 16]; + if (mode < ENVFX_BUBBLE_START) + { + WriteVertex(vertices, 0, -5, 5, 0, 0, 0, 0x7F, 0x7F, 0x7F, 0xFF); + WriteVertex(vertices, 1, -5, -5, 0, 0, 960, 0x7F, 0x7F, 0x7F, 0xFF); + WriteVertex(vertices, 2, 5, 5, 0, 960, 0, 0x7F, 0x7F, 0x7F, 0xFF); + } + else + { + (spread, height) = mode == ENVFX_FLOWERS ? ((short)50, (short)75) : mode == ENVFX_LAVA_BUBBLES ? ((short)100, (short)150) : ((short)40, (short)60); + st = new[] { ((short)1544, (short)964), ((short)522, (short)-568), ((short)-498, (short)964) }; + shade = 0xFF; + WriteVertex(vertices, 0, spread, 0, 0, st[0].s, st[0].t, shade, shade, shade, 0xFF); + WriteVertex(vertices, 1, 0, height, 0, st[1].s, st[1].t, shade, shade, shade, 0xFF); + WriteVertex(vertices, 2, (short)-spread, 0, 0, st[2].s, st[2].t, shade, shade, shade, 0xFF); + } + + var builder = new MeshBuilder(); + var f3d = NewF3D(ram, builder); + f3d.Load(defaultState); + f3d.Run(begin, layer, Matrix4.Identity); + if (texture != 0) + { + f3d.Command(0xFD000000 | 2 << 19, texture, layer, Matrix4.Identity); + f3d.Run(DL_BUBBLE_TEXTURE, layer, Matrix4.Identity); + } + + f3d.LoadVertices(vertices, 3, 0, Matrix4.Identity); + f3d.Command(0xBF000000, 10 << 8 | 20, layer, Matrix4.Identity); + f3d.Run(DL_PARTICLES_END, layer, Matrix4.Identity); + var mesh = store.Add(builder); + objectMeshes[key] = (mesh, null); + return mesh; + } + + const int FLYING_CARPET_VERTICES = 21; + + void EmitFlyingCarpet(Rdram ram, in Frame frame) + { + if (addresses.flyingCarpetRippleTimer == 0) + addresses.flyingCarpetRippleTimer = FindFirstHalfwordLoad(ram, addresses.geoExecFlyingCarpetCreate); + if (addresses.flyingCarpetRippleTimer == 0) + return; + ushort timer = ram.U16(addresses.flyingCarpetRippleTimer); + var key = ((uint)timer, 0x400 | LAYER_OPAQUE, (int)addresses.geoExecFlyingCarpetCreate); + if (!objectMeshes.TryGetValue(key, out var entry)) + { + var constants = FindSegmentedConstants(ram, addresses.geoExecFlyingCarpetCreate, 0x07); + if (constants.Count < 4) + return; + uint data = constants[0], begin = constants[1], half = constants[2], end = constants[constants.Count - 1]; + + var vertices = new byte[FLYING_CARPET_VERTICES * 16]; + for (int n = 0; n < FLYING_CARPET_VERTICES; n++) + { + int row = n / 3, col = n % 3; + double sin = Math.Sin(((ushort)(timer + (row << 12) + (col << 14)) >> 4) * (2 * Math.PI / 4096)); + double y = sin * 20.0; + uint v = data + 8 * (uint)n; + WriteVertex(vertices, n, ram.S16(v), (short)(y >= 0 ? y + 0.5 : y - 0.5), ram.S16(v + 2), ram.S16(v + 4), ram.S16(v + 6), 0, 127, 0, 255); + } + + var builder = new MeshBuilder(); + var f3d = NewF3D(ram, builder); + f3d.Load(defaultState); + f3d.Run(begin, LAYER_OPAQUE, Matrix4.Identity); + f3d.LoadVertices(vertices, 12, 0, Matrix4.Identity); + f3d.Run(half, LAYER_OPAQUE, Matrix4.Identity); + f3d.LoadVertices(vertices.AsSpan(9 * 16).ToArray(), 12, 0, Matrix4.Identity); + f3d.Run(half, LAYER_OPAQUE, Matrix4.Identity); + f3d.Run(end, LAYER_OPAQUE, Matrix4.Identity); + objectMeshes[key] = entry = (store.Add(builder), null); + } + + if (entry.mesh.triangleCount > 0) + instances.Add(new Instance { mesh = entry.mesh, matrix = frame.matrix, billboard = frame.billboard, anchor = frame.anchor, anchorScale = frame.anchorScale }); + } + + void EmitObjectMovtex(Rdram ram, uint n, uint function, int layer, in Frame frame) + { + uint parameter = ram.U32(n + 0x18); + var key = (parameter, layer | 0x200, (int)function); + if (!objectMeshes.TryGetValue(key, out var entry)) + { + var builder = new MeshBuilder(); + var f3d = NewF3D(ram, builder); + f3d.Load(defaultState); + EmitMovtex(ram, f3d, n, Matrix4.Identity); + objectMeshes[key] = entry = (store.Add(builder), null); + } + + if (entry.mesh.triangleCount > 0) + instances.Add(new Instance { mesh = entry.mesh, matrix = frame.matrix, billboard = frame.billboard, anchor = frame.anchor, anchorScale = frame.anchorScale }); + } + + void ClearLighting(Rdram ram, int slot) + { + slot &= 7; + var key = (layerStates[slot].Hash, -1); + if (!alphaStates.TryGetValue(key, out var state)) + { + var f3d = NewF3D(ram, new MeshBuilder()); + f3d.Load(layerStates[slot]); + f3d.Command(0xB6000000, 0x00020000, slot, Matrix4.Identity); + alphaStates[key] = state = f3d.Save(); + } + + layerStates[slot] = state; + } + + const uint COLORED_HAT_DL_1 = 0x040119A0, COLORED_HAT_DL_2 = 0x04011978; + + void EmitColoredHat(Rdram ram, int layer, in Frame frame) + { + uint index = IsMario ? 0u : ram.U8(currentObject + 0x60); + uint lights = addresses.coloredHatsLights + 0x20 * index; + int slot = layer & 7; + var incoming = layerStates[slot]; + var key = (lights, layer | 0x100, HashCode.Combine(incoming.Hash, ram.U32(lights), ram.U32(lights + 8), ram.U32(lights + 12))); + if (!objectMeshes.TryGetValue(key, out var entry)) + { + var builder = new MeshBuilder(); + var f3d = NewF3D(ram, builder); + f3d.Load(incoming); + f3d.MoveMemLight(0x88, lights); + f3d.MoveMemLight(0x86, lights + 8); + f3d.Run(COLORED_HAT_DL_1, layer, Matrix4.Identity); + f3d.MoveMemLight(0x88, lights); + f3d.MoveMemLight(0x86, lights + 8); + f3d.Run(COLORED_HAT_DL_2, layer, Matrix4.Identity); + objectMeshes[key] = entry = (store.Add(builder), f3d.Save()); + } + + layerStates[slot] = entry.state; + if (entry.mesh.triangleCount > 0) + instances.Add(new Instance { mesh = entry.mesh, matrix = frame.matrix, billboard = frame.billboard, anchor = frame.anchor, anchorScale = frame.anchorScale }); + } + + Matrix4 AnimatedPart(Rdram ram, uint n) + { + double tx = ram.S16(n + 0x18), ty = ram.S16(n + 0x1A), tz = ram.S16(n + 0x1C); + short rx = 0, ry = 0, rz = 0; + switch (anim.type) + { + case ANIM_TRANSLATION: + tx += NextAnimationValue(ram) * anim.translationMultiplier; + ty += NextAnimationValue(ram) * anim.translationMultiplier; + tz += NextAnimationValue(ram) * anim.translationMultiplier; + anim.type = ANIM_ROTATION; + break; + case ANIM_LATERAL_TRANSLATION: + tx += NextAnimationValue(ram) * anim.translationMultiplier; + anim.attributes += 4; + tz += NextAnimationValue(ram) * anim.translationMultiplier; + anim.type = ANIM_ROTATION; + break; + case ANIM_VERTICAL_TRANSLATION: + anim.attributes += 4; + ty += NextAnimationValue(ram) * anim.translationMultiplier; + anim.attributes += 4; + anim.type = ANIM_ROTATION; + break; + case ANIM_NO_TRANSLATION: + anim.attributes += 12; + anim.type = ANIM_ROTATION; + break; + } + + if (anim.type == ANIM_ROTATION) + { + rx = NextAnimationValue(ram); + ry = NextAnimationValue(ram); + rz = NextAnimationValue(ram); + } + + return RotXYZTrans(tx, ty, tz, rx, ry, rz); + } + + void DrawHeldObject(Rdram ram, uint n, Frame frame, int depth) + { + uint obj = ram.U32(n + 0x1C); + if (!Rdram.IsPointer(obj)) + return; + uint sharedChild = ram.U32(obj + 0x14); + if (!Rdram.IsPointer(sharedChild)) + return; + + var hand = frame.WorldPosition; + var holder = frame.objectMatrix; + holder.Row3 = new Vector4(hand, 1); + var translation = Matrix4.CreateTranslation(ram.S16(n + 0x20) / 4f, ram.S16(n + 0x22) / 4f, ram.S16(n + 0x24) / 4f); + var scale = new Vector3(ram.F32(obj + 0x2C), ram.F32(obj + 0x30), ram.F32(obj + 0x34)); + var held = new Frame { matrix = ScaleRows(translation * holder, scale) }; + held.objectMatrix = held.matrix; + + var holderAnimation = anim; + uint holderObject = currentObject; + bool wasInHeldObject = inHeldObject; + currentObject = obj; + inHeldObject = true; + SetAnimation(ram, obj); + DrawNodeAndSiblings(ram, sharedChild, held, depth + 1); + anim = holderAnimation; + currentObject = holderObject; + inHeldObject = wasInHeldObject; + } + + #endregion + } +} diff --git a/STROOP/Tabs/MapTab/DataUtil/MipsCall.cs b/STROOP/Tabs/MapTab/DataUtil/MipsCall.cs new file mode 100644 index 000000000..67c4661f9 --- /dev/null +++ b/STROOP/Tabs/MapTab/DataUtil/MipsCall.cs @@ -0,0 +1,142 @@ +using System.Collections.Generic; + +namespace STROOP.Tabs.MapTab.DataUtil +{ + public static class MipsCall + { + const uint RETURN_ADDRESS = 0xFFFFFFF0, STACK = 0x807FFFF0; + const int MAX_STEPS = 4096; + + public static uint? Call(Rdram ram, uint function, uint a0 = 0, uint a1 = 0, uint a2 = 0, uint a3 = 0) + { + var r = new uint[32]; + r[4] = a0; + r[5] = a1; + r[6] = a2; + r[7] = a3; + r[29] = STACK; + r[31] = RETURN_ADDRESS; + var stores = new Dictionary(); + + uint Load8(uint a) => stores.TryGetValue(a, out byte b) ? b : ram.U8(a); + uint Load16(uint a) => Load8(a) << 8 | Load8(a + 1); + uint Load32(uint a) => Load16(a) << 16 | Load16(a + 2); + + uint pc = function; + uint? branchTarget = null; + for (int step = 0; step < MAX_STEPS; step++) + { + if (pc == RETURN_ADDRESS) + return r[2]; + if (!Rdram.IsPointer(pc)) + return null; + + uint i = ram.U32(pc); + uint next = pc + 4; + uint? target = null; + bool skipDelaySlot = false; + + int op = (int)(i >> 26), rs = (int)(i >> 21) & 31, rt = (int)(i >> 16) & 31, rd = (int)(i >> 11) & 31, sa = (int)(i >> 6) & 31; + uint imm = i & 0xFFFF, simm = (uint)(short)imm; + uint branch = next + (simm << 2); + uint value = 0; + int dest = 0; + + switch (op) + { + case 0x00: + dest = rd; + switch (i & 63) + { + case 0x00: value = r[rt] << sa; break; + case 0x02: value = r[rt] >> sa; break; + case 0x03: value = (uint)((int)r[rt] >> sa); break; + case 0x04: value = r[rt] << (int)(r[rs] & 31); break; + case 0x06: value = r[rt] >> (int)(r[rs] & 31); break; + case 0x07: value = (uint)((int)r[rt] >> (int)(r[rs] & 31)); break; + case 0x08: target = r[rs]; dest = 0; break; + case 0x21: case 0x20: value = r[rs] + r[rt]; break; + case 0x23: case 0x22: value = r[rs] - r[rt]; break; + case 0x24: value = r[rs] & r[rt]; break; + case 0x25: value = r[rs] | r[rt]; break; + case 0x26: value = r[rs] ^ r[rt]; break; + case 0x27: value = ~(r[rs] | r[rt]); break; + case 0x2A: value = (int)r[rs] < (int)r[rt] ? 1u : 0; break; + case 0x2B: value = r[rs] < r[rt] ? 1u : 0; break; + default: return null; + } + + break; + case 0x01: + bool taken = rt switch { 0 or 2 => (int)r[rs] < 0, 1 or 3 => (int)r[rs] >= 0, _ => false }; + if (rt > 3) + return null; + if (taken) + target = branch; + else if (rt >= 2) + skipDelaySlot = true; + break; + case 0x02: + target = (next & 0xF0000000) | ((i & 0x3FFFFFF) << 2); + break; + case 0x04: case 0x14: + case 0x05: case 0x15: + case 0x06: case 0x16: + case 0x07: case 0x17: + bool condition = (op & 3) switch + { + 0 => r[rs] == r[rt], + 1 => r[rs] != r[rt], + 2 => (int)r[rs] <= 0, + _ => (int)r[rs] > 0, + }; + if (condition) + target = branch; + else if (op >= 0x14) + skipDelaySlot = true; + break; + case 0x08: case 0x09: dest = rt; value = r[rs] + simm; break; + case 0x0A: dest = rt; value = (int)r[rs] < (int)simm ? 1u : 0; break; + case 0x0B: dest = rt; value = r[rs] < simm ? 1u : 0; break; + case 0x0C: dest = rt; value = r[rs] & imm; break; + case 0x0D: dest = rt; value = r[rs] | imm; break; + case 0x0E: dest = rt; value = r[rs] ^ imm; break; + case 0x0F: dest = rt; value = imm << 16; break; + case 0x20: dest = rt; value = (uint)(sbyte)Load8(r[rs] + simm); break; + case 0x21: dest = rt; value = (uint)(short)Load16(r[rs] + simm); break; + case 0x23: dest = rt; value = Load32(r[rs] + simm); break; + case 0x24: dest = rt; value = Load8(r[rs] + simm); break; + case 0x25: dest = rt; value = Load16(r[rs] + simm); break; + case 0x28: case 0x29: case 0x2B: + int size = op == 0x28 ? 1 : op == 0x29 ? 2 : 4; + uint address = r[rs] + simm; + for (int b = 0; b < size; b++) + stores[address + (uint)b] = (byte)(r[rt] >> (8 * (size - 1 - b))); + break; + default: + return null; + } + + if (dest != 0) + r[dest] = value; + + if (branchTarget.HasValue) + { + pc = branchTarget.Value; + branchTarget = null; + if (target.HasValue) + return null; + } + else if (target.HasValue) + { + branchTarget = target; + pc = next; + } + else + pc = skipDelaySlot ? next + 4 : next; + } + + return null; + } + } +} diff --git a/STROOP/Tabs/MapTab/DataUtil/Rdram.cs b/STROOP/Tabs/MapTab/DataUtil/Rdram.cs new file mode 100644 index 000000000..b17c2fcd0 --- /dev/null +++ b/STROOP/Tabs/MapTab/DataUtil/Rdram.cs @@ -0,0 +1,45 @@ +using System; + +namespace STROOP.Tabs.MapTab.DataUtil +{ + public class Rdram + { + public readonly byte[] data; + readonly uint[] segments = new uint[32]; + + public Rdram(byte[] littleEndianWords) + { + data = littleEndianWords; + } + + public void LoadSegmentTable(uint segmentTable) + { + for (uint i = 0; i < 32; i++) + segments[i] = U32((segmentTable | 0x80000000) + 4 * i); + } + + public uint Phys(uint a) => (a & 0x80000000) != 0 ? a & 0x7FFFFF : (segments[(a >> 24) & 0x1F] + (a & 0xFFFFFF)) & 0x7FFFFF; + + public bool Ok(uint physical, int length) => physical + length <= data.Length; + + public static bool IsPointer(uint a) => a >= 0x80000000 && a < 0x80800000; + + public byte U8Phys(uint p) => p < data.Length ? data[p ^ 3] : (byte)0; + + public ushort U16Phys(uint p) => (ushort)(U8Phys(p) << 8 | U8Phys(p + 1)); + + public uint U32Phys(uint p) + { + if ((p & 3) == 0) + return p + 4 <= data.Length ? BitConverter.ToUInt32(data, (int)p) : 0; + return (uint)(U8Phys(p) << 24 | U8Phys(p + 1) << 16 | U8Phys(p + 2) << 8 | U8Phys(p + 3)); + } + + public byte U8(uint a) => U8Phys(Phys(a)); + public ushort U16(uint a) => U16Phys(Phys(a)); + public short S16(uint a) => (short)U16(a); + public uint U32(uint a) => U32Phys(Phys(a)); + public int S32(uint a) => (int)U32(a); + public float F32(uint a) => BitConverter.Int32BitsToSingle((int)U32(a)); + } +} diff --git a/STROOP/Tabs/MapTab/MapGraphics.cs b/STROOP/Tabs/MapTab/MapGraphics.cs index 362734ab0..191a06979 100644 --- a/STROOP/Tabs/MapTab/MapGraphics.cs +++ b/STROOP/Tabs/MapTab/MapGraphics.cs @@ -63,6 +63,14 @@ public bool HoverOrthogonal(Vector3 position, float radius) public readonly List[] drawLayers; + public readonly List marioModelCopies = new List(); + + public bool marioModelAvailable { get; private set; } + + bool marioModelDrawn; + + public void ReportMarioModelDrawn() => marioModelDrawn = true; + public Renderers.RendererCollection rendererCollection { get; private set; } public Renderers.SpriteRenderer objectRenderer => rendererCollection.objectRenderer; public Renderers.TriangleRenderer triangleRenderer => rendererCollection.triangleRenderer; @@ -269,6 +277,8 @@ public MapGraphics(MapTab mapTab, GLControl glControl, Func ge List levelTrianglesFor3DMap; + public bool Draw3DCollisionGeometry => view3D.display3DLevelGeometry || !mapTab.IsLevelModelShown; + Control previouslyActiveControl; Control GetActiveLeafControl(ContainerControl root) @@ -442,6 +452,9 @@ private void OnPaint() for (int i = 0; i < drawLayers.Length; i++) drawLayers[i].Clear(); + marioModelAvailable = marioModelDrawn; + marioModelDrawn = false; + marioModelCopies.Clear(); foreach (var renderer in rendererCollection.renderers) renderer.SetDrawCalls(this); @@ -457,7 +470,7 @@ private void OnPaint() GL.ClearDepth(1); GL.Clear(ClearBufferMask.DepthBufferBit); - if (view3D.display3DLevelGeometry) + if (Draw3DCollisionGeometry) drawLayers[(int)DrawLayers.FillBuffers].Insert(0, () => { foreach (var t in levelTrianglesFor3DMap) @@ -551,9 +564,21 @@ private void UpdateMapView() switch (view3D.camera3DMode) { case View3D.Camera3DMode.InGame: - view3D.position = new Vector3(Models.DataModels.Camera.X, Models.DataModels.Camera.Y, Models.DataModels.Camera.Z); - view3D.yaw = (float)MoreMath.AngleUnitsToRadians(Models.DataModels.Camera.FacingYaw); - view3D.pitch = (float)MoreMath.AngleUnitsToRadians(-Models.DataModels.Camera.FacingPitch); + if (MapObjects.MapLevelModelObject.GameCamera is (Vector3 cameraPosition, Vector3 cameraFocus)) + { + view3D.position = cameraPosition; + var look = cameraFocus - cameraPosition; + view3D.yaw = (float)(Math.PI / 2 - Math.Atan2(look.Z, look.X)); + view3D.pitch = (float)-Math.Atan2(look.Y, Math.Sqrt(look.X * look.X + look.Z * look.Z)); + } + else + { + view3D.position = new Vector3(Models.DataModels.Camera.X, Models.DataModels.Camera.Y, Models.DataModels.Camera.Z); + view3D.yaw = (float)MoreMath.AngleUnitsToRadians(Models.DataModels.Camera.FacingYaw); + view3D.pitch = (float)MoreMath.AngleUnitsToRadians(-Models.DataModels.Camera.FacingPitch); + } + + viewDirection = currentView.ComputeViewDirection(); target = currentView.position + viewDirection; break; case View3D.Camera3DMode.FocusOnPositionAngle: @@ -565,7 +590,13 @@ private void UpdateMapView() } nearClip = Math.Max(1, Math.Min(50, (target - currentView.position).Length / 100)); - farClip = nearClip * 5000; + farClip = Math.Max(nearClip * 5000, SceneFarDistance(currentView.position)); + nearClip = Math.Max(nearClip, farClip / MAX_DEPTH_RATIO); + if (!float.IsFinite(nearClip) || !float.IsFinite(farClip) || nearClip >= farClip) + { + nearClip = 1; + farClip = 5000; + } ViewMatrix = Matrix4.LookAt(currentView.position, target, new Vector3(0, 1, 0)); var mat = Matrix4.Invert(ViewMatrix); @@ -580,22 +611,109 @@ private void UpdateMapView() UpdateCursor(); } + const float LEVEL_HALF_EXTENT = 32768, PU_SIZE = 65536, MAX_DEPTH_RATIO = 100000, MAX_FAR_DISTANCE = 1e8f; + + float SceneFarDistance(Vector3 camera) + { + float FarthestCorner(Vector3 center) + { + float x = Math.Abs(camera.X - center.X) + LEVEL_HALF_EXTENT; + float y = Math.Abs(camera.Y - center.Y) + LEVEL_HALF_EXTENT; + float z = Math.Abs(camera.Z - center.Z) + LEVEL_HALF_EXTENT; + return (float)Math.Sqrt(x * x + y * y + z * z); + } + + float farthest = FarthestCorner(Vector3.Zero); + if (MapViewEnablePuView) + { + float puX = (float)Math.Round(camera.X / PU_SIZE), puZ = (float)Math.Round(camera.Z / PU_SIZE); + for (int dx = -1; dx <= 1; dx++) + for (int dz = -1; dz <= 1; dz++) + farthest = Math.Max(farthest, FarthestCorner(new Vector3((puX + dx) * PU_SIZE, 0, (puZ + dz) * PU_SIZE))); + } + + var mario = new Vector3(Models.DataModels.Mario.X, Models.DataModels.Mario.Y, Models.DataModels.Mario.Z); + if (float.IsFinite(mario.X) && float.IsFinite(mario.Y) && float.IsFinite(mario.Z)) + farthest = Math.Max(farthest, (mario - camera).Length + 2000); + return float.IsFinite(farthest) ? Math.Min(farthest * 1.05f, MAX_FAR_DISTANCE) : MAX_FAR_DISTANCE; + } + bool FindClosestIntersection(Vector3 rayOrigin, Vector3 rayDirection, out Vector3 intersection, out Models.TriangleDataModel triangle) { intersection = default(Vector3); triangle = default(Models.TriangleDataModel); Vector3 viewDirection = Vector3.Normalize(rayDirection); float closestDistance = float.PositiveInfinity, newDistance; + UpdateLevelTriangleBounds(); + foreach (var offset in CursorPuOffsets()) + { + if (offset != Vector3.Zero && !RayHitsBox(rayOrigin - offset, viewDirection, levelTrianglesMin, levelTrianglesMax)) + continue; + foreach (var t in levelTrianglesFor3DMap) + if (t.Intersect(rayOrigin - offset, viewDirection, out Vector3 newIntersection, out Vector3 newNormal) + && (newDistance = (newIntersection + offset - currentView.position).LengthSquared) < closestDistance) + { + closestDistance = newDistance; + intersection = newIntersection + offset; + triangle = t; + } + } + + return (closestDistance < float.PositiveInfinity); + } + + List levelTrianglesBoundsSource; + Vector3 levelTrianglesMin, levelTrianglesMax; + + void UpdateLevelTriangleBounds() + { + if (ReferenceEquals(levelTrianglesBoundsSource, levelTrianglesFor3DMap)) + return; + levelTrianglesBoundsSource = levelTrianglesFor3DMap; + levelTrianglesMin = new Vector3(float.PositiveInfinity); + levelTrianglesMax = new Vector3(float.NegativeInfinity); foreach (var t in levelTrianglesFor3DMap) - if (t.Intersect(rayOrigin, viewDirection, out Vector3 newIntersection, out Vector3 newNormal) - && (newDistance = (newIntersection - currentView.position).LengthSquared) < closestDistance) + foreach (var p in new[] { t.p1, t.p2, t.p3 }) { - closestDistance = newDistance; - intersection = newIntersection; - triangle = t; + levelTrianglesMin = Vector3.ComponentMin(levelTrianglesMin, p); + levelTrianglesMax = Vector3.ComponentMax(levelTrianglesMax, p); } + } - return (closestDistance < float.PositiveInfinity); + static bool RayHitsBox(Vector3 origin, Vector3 direction, Vector3 min, Vector3 max) + { + float enter = 0, exit = float.PositiveInfinity; + for (int axis = 0; axis < 3; axis++) + { + if (Math.Abs(direction[axis]) < 1e-9f) + { + if (origin[axis] < min[axis] || origin[axis] > max[axis]) + return false; + continue; + } + + float t0 = (min[axis] - origin[axis]) / direction[axis], t1 = (max[axis] - origin[axis]) / direction[axis]; + enter = Math.Max(enter, Math.Min(t0, t1)); + exit = Math.Min(exit, Math.Max(t0, t1)); + if (enter > exit) + return false; + } + + return true; + } + + IEnumerable CursorPuOffsets() + { + if (!MapViewEnablePuView || viewMode != ViewMode.ThreeDimensional) + { + yield return Vector3.Zero; + yield break; + } + + float puX = (float)Math.Round(currentView.position.X / PU_SIZE), puZ = (float)Math.Round(currentView.position.Z / PU_SIZE); + for (int dx = -1; dx <= 1; dx++) + for (int dz = -1; dz <= 1; dz++) + yield return new Vector3((puX + dx) * PU_SIZE, 0, (puZ + dz) * PU_SIZE); } public void UpdateCursor() @@ -640,6 +758,7 @@ public bool Hover3D(Vector3 position, float radius) private int _dragStartMouseX = 0; private int _dragStartMouseY = 0; private Vector3 _translateStartCenter = new Vector3(0); + private float _panUnitsPerPixel = 1; private Vector2 _translateStartOrthoOffset = new Vector2(2); private Vector3 _rotatePivot = new Vector3(0); private Vector3 _rotateDiff = new Vector3(0); @@ -675,6 +794,9 @@ private void OnMouseDown(object sender, MouseEventArgs e) _dragStartMouseY = e.Y; _dragStartYaw = currentView.yaw; _dragStartPitch = currentView.pitch; + _translateStartCenter = currentView.position; + float depth = Vector3.Dot(mapCursorPosition - currentView.position, -BillboardMatrix.Row2.Xyz); + _panUnitsPerPixel = 2 * (float)Math.Tan(0.5) * (depth > 1 ? depth : Math.Max(currentView.movementSpeed, 100)) / glControl.Height; break; } @@ -763,23 +885,10 @@ private void OnMouseMove(object sender, MouseEventArgs e) { if (viewMode == ViewMode.ThreeDimensional) { - int pixelDiffX = e.X - _dragStartMouseX; - int pixelDiffY = e.Y - _dragStartMouseY; - float mul = 10.0f / (float)Math.Log((currentView.position - _rotatePivot).Length); - float diffX = pixelDiffX / (float)glControl.Width * 2 * mul; - float diffY = pixelDiffY / (float)glControl.Height * 2 * mul; - if (float.IsNaN(diffX) || float.IsNaN(diffY)) - throw null; - if (view3D.camera3DMode == View3D.Camera3DMode.Free) - { - view3D.yaw = _dragStartYaw - diffX; - view3D.pitch = Math.Max(-(float)Math.PI * 0.499f, Math.Min((float)Math.PI * 0.499f, _dragStartPitch + diffY)); - } - else if (view3D.camera3DMode == View3D.Camera3DMode.FocusOnPositionAngle) - { - view3D.yaw = _dragStartYaw + diffX; - view3D.pitch = Math.Max(-(float)Math.PI * 0.499f, Math.Min((float)Math.PI * 0.499f, _dragStartPitch - diffY)); - } + view3D.camera3DMode = View3D.Camera3DMode.Free; + float dx = -(e.X - _dragStartMouseX) * _panUnitsPerPixel; + float dy = (e.Y - _dragStartMouseY) * _panUnitsPerPixel; + currentView.position = _translateStartCenter + BillboardMatrix.Row0.Xyz * dx + BillboardMatrix.Row1.Xyz * dy; } } @@ -876,14 +985,20 @@ private void OnScroll(object sender, MouseEventArgs e) int delta = e.Delta > 0 ? 1 : -1; if (viewMode == ViewMode.ThreeDimensional) { + bool fine = keyboardControls.IsCtrlDown(); if (view3D.camera3DMode == View3D.Camera3DMode.FocusOnPositionAngle) - view3D.camera3DDistanceController = Math.Max(0.0f, Math.Min(100, view3D.camera3DDistanceController - delta)); + { + float steps = fine ? 0.3f : 1; + view3D.camera3DDistanceController = Math.Max(0.0f, Math.Min(100, view3D.camera3DDistanceController - delta * steps)); + } else if (view3D.camera3DMode == View3D.Camera3DMode.Free) { var diff = mapCursorPosition - currentView.position; - if (Vector3.Dot(diff, normalAtCursor) < 0) - currentView.movementSpeed = diff.Length * 0.5f; - currentView.position += Vector3.Normalize(mapCursorPosition - currentView.position) * delta * currentView.movementSpeed / 5; + float distance = Math.Max(diff.Length, 50); + float share = fine ? 0.03f : 0.1f; + float move = delta > 0 ? distance * share : -distance * share / (1 - share); + if (diff.LengthSquared > 0) + currentView.position += Vector3.Normalize(diff) * move; } } else diff --git a/STROOP/Tabs/MapTab/MapObjects/MapArrowObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapArrowObject.cs index 4bb83fe36..f2e328847 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapArrowObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapArrowObject.cs @@ -18,9 +18,13 @@ public class MapArrowObject : MapLineObject public delegate double GetRecommendedSize(PositionAngle obj); + public delegate double GetVerticalSpeed(PositionAngle obj); + public GetYaw getYaw; public GetRecommendedSize getRecommendedSize; + public GetVerticalSpeed getVerticalSpeed; + private float _arrowHeadSideLength; double _yawOffset; @@ -38,6 +42,11 @@ public MapArrowObject(PositionAngleProvider positionAngleProvider, GetYaw getYaw this.name = name; _arrowHeadSideLength = 100; + if (getYaw == ArrowSource.MarioMovingYaw || getYaw == ArrowSource.MarioSlidingYaw) + getVerticalSpeed = ArrowSource.MarioYSpeed; + else if (getYaw == ArrowSource.ObjectMovingYaw) + getVerticalSpeed = ArrowSource.ObjectYSpeed; + Size = 300; OutlineWidth = 3; OutlineColor = Color.Yellow; @@ -59,6 +68,48 @@ protected override void DrawTopDown(MapGraphics graphics) }); } + protected override void Draw3D(MapGraphics graphics) + { + graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => + { + Vector4 color = GetColor(graphics); + foreach (var posAngle in positionAngleProvider()) + { + Vector3 from = new Vector3((float)posAngle.X, (float)posAngle.Y, (float)posAngle.Z); + float horizontalLength = useRecommendedArrowLength ? (float)getRecommendedSize(posAngle) : Size; + float verticalLength = 0; + if (getVerticalSpeed != null) + { + float ySpeed = (float)getVerticalSpeed(posAngle); + if (useRecommendedArrowLength) + verticalLength = ySpeed; + else + { + float hSpeed = Math.Abs((float)getRecommendedSize(posAngle)); + float speed = (float)Math.Sqrt(hSpeed * hSpeed + ySpeed * ySpeed); + if (speed > 0) + { + horizontalLength = Size * hSpeed / speed; + verticalLength = Size * ySpeed / speed; + } + } + + if (_yawOffset == 0x8000) + verticalLength = -verticalLength; + } + + (double toX, double toZ) = MoreMath.AddVectorToPoint(horizontalLength, getYaw(posAngle) + _yawOffset, from.X, from.Z); + graphics.lineRenderer.AddArrow3D( + from, + new Vector3((float)toX, from.Y + verticalLength, (float)toZ), + posAngle.GetArrowColor(color), + OutlineWidth, + _arrowHeadSideLength, + graphics.currentView.position); + } + }); + } + protected override List GetVertices(MapGraphics graphics) => new List(); public override Lazy GetInternalImage() => Config.ObjectAssociations.ArrowImage; @@ -156,6 +207,10 @@ public class ArrowSource public static GetRecommendedSize Constant(double length) => _ => length; + + + public static GetVerticalSpeed ObjectYSpeed = posAngle => Config.Stream.GetSingle(PositionAngle.GetObjectAddress(posAngle) + ObjectConfig.YSpeedOffset); + public static GetVerticalSpeed MarioYSpeed = posAngle => Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YSpeedOffset); } } } diff --git a/STROOP/Tabs/MapTab/MapObjects/MapCameraObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapCameraObject.cs index 8abae9201..e98fa8fa8 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapCameraObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapCameraObject.cs @@ -2,6 +2,7 @@ using System.Drawing; using STROOP.Utilities; using STROOP.Structs.Configurations; +using OpenTK.Mathematics; namespace STROOP.Tabs.MapTab.MapObjects { @@ -16,5 +17,47 @@ public MapCameraObject() public override Lazy GetInternalImage() => Config.ObjectAssociations.CameraMapImage; public override string GetName() => "Camera"; + + protected override void Draw3D(MapGraphics graphics) + { + base.Draw3D(graphics); + graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => + { + var camera = PositionAngle.Camera; + var focus = PositionAngle.CameraFocus; + Vector3 cameraPos = new Vector3((float)camera.X, (float)camera.Y, (float)camera.Z); + Vector3 focusPos = new Vector3((float)focus.X, (float)focus.Y, (float)focus.Z); + Vector3 viewDir = focusPos - cameraPos; + if (viewDir.LengthSquared < 1e-4f) + return; + Vector4 color = Get3DAccentColor(Color.White); + + graphics.lineRenderer.Add(cameraPos, focusPos, color, 1); + + viewDir.Normalize(); + Vector3 right = Vector3.Cross(viewDir, Vector3.UnitY); + if (right.LengthSquared < 1e-6f) + right = Vector3.Cross(viewDir, Vector3.UnitZ); + right.Normalize(); + Vector3 up = Vector3.Cross(right, viewDir); + + float depth = Size * Get3DIconScale(graphics, cameraPos.X, cameraPos.Y, cameraPos.Z) * 3; + float halfHeight = depth * (float)Math.Tan(MathHelper.DegreesToRadians(22.5)); + float halfWidth = halfHeight * 4 / 3; + Vector3 center = cameraPos + viewDir * depth; + Vector3[] corners = + { + center + right * halfWidth + up * halfHeight, + center - right * halfWidth + up * halfHeight, + center - right * halfWidth - up * halfHeight, + center + right * halfWidth - up * halfHeight, + }; + for (int i = 0; i < corners.Length; i++) + { + graphics.lineRenderer.Add(cameraPos, corners[i], color, 2); + graphics.lineRenderer.Add(corners[i], corners[(i + 1) % corners.Length], color, 2); + } + }); + } } } diff --git a/STROOP/Tabs/MapTab/MapObjects/MapCellGridlinesObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapCellGridlinesObject.cs index fee56e692..7b74a32dc 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapCellGridlinesObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapCellGridlinesObject.cs @@ -19,11 +19,27 @@ public MapCellGridlinesObject() OutlineColor = Color.Black; } + const int CELL_EXPANSE_3D = 3; + const float COLUMN_HALF_HEIGHT_3D = 2000; + protected override List GetVertices(MapGraphics graphics) { float marioY = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset); List vertices = new List(); + if (graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional) + { + float marioX = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.XOffset); + float marioZ = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.ZOffset); + if (!IsUsableCoordinate(marioX) || !IsUsableCoordinate(marioY) || !IsUsableCoordinate(marioZ)) + return vertices; + float xMin = ((float)Math.Floor(marioX / 1024) - CELL_EXPANSE_3D) * 1024; + float zMin = ((float)Math.Floor(marioZ / 1024) - CELL_EXPANSE_3D) * 1024; + float extent = (2 * CELL_EXPANSE_3D + 1) * 1024; + AddColumnGrid3D(vertices, xMin, xMin + extent, zMin, zMin + extent, 1024, marioY, marioY - COLUMN_HALF_HEIGHT_3D, marioY + COLUMN_HALF_HEIGHT_3D); + return vertices; + } + for (int x = -8192; x <= 8192; x += 1024) { vertices.Add(new Vector3(x, marioY, -8192)); @@ -39,6 +55,46 @@ protected override List GetVertices(MapGraphics graphics) return vertices; } + const int MAX_COLUMN_LINES_3D = 64; + + static bool IsUsableCoordinate(float value) => float.IsFinite(value) && Math.Abs(value) < 1e7f; + + static int LineCount(float min, float max, float spacing) + { + if (!(spacing > 0) || !float.IsFinite(min) || !float.IsFinite(max) || max < min) + return 0; + return (int)Math.Min(MAX_COLUMN_LINES_3D, Math.Round((max - min) / spacing) + 1); + } + + public static void AddColumnGrid3D(List vertices, float xMin, float xMax, float zMin, float zMax, float spacing, float yRef, float yMin, float yMax) + { + int xCount = LineCount(xMin, xMax, spacing), zCount = LineCount(zMin, zMax, spacing); + foreach (float y in new[] { yRef, yMin, yMax }) + { + for (int i = 0; i < xCount; i++) + { + float x = xMin + i * spacing; + vertices.Add(new Vector3(x, y, zMin)); + vertices.Add(new Vector3(x, y, zMax)); + } + + for (int i = 0; i < zCount; i++) + { + float z = zMin + i * spacing; + vertices.Add(new Vector3(xMin, y, z)); + vertices.Add(new Vector3(xMax, y, z)); + } + } + + for (int i = 0; i < xCount; i++) + for (int j = 0; j < zCount; j++) + { + float x = xMin + i * spacing, z = zMin + j * spacing; + vertices.Add(new Vector3(x, yMin, z)); + vertices.Add(new Vector3(x, yMax, z)); + } + } + public override string GetName() { return "Cell Gridlines"; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapCurrentCellObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapCurrentCellObject.cs index 291d3b756..7cb4414de 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapCurrentCellObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapCurrentCellObject.cs @@ -11,6 +11,8 @@ namespace STROOP.Tabs.MapTab.MapObjects [ObjectDescription("Current Cell", "Current")] public class MapCurrentCellObject : MapQuadObject { + const float COLUMN_HALF_HEIGHT_3D = 500; + public MapCurrentCellObject() : base() { @@ -34,6 +36,14 @@ public MapCurrentCellObject() }); } + protected override List<(float xMin, float xMax, float zMin, float zMax, float yMin, float yMax)> GetBoxList3D(MapGraphics graphics) + { + var boxes = new List<(float, float, float, float, float, float)>(); + foreach (var quad in GetQuadList()) + boxes.Add((quad.xMin, quad.xMax, quad.zMin, quad.zMax, quad.y - COLUMN_HALF_HEIGHT_3D, quad.y + COLUMN_HALF_HEIGHT_3D)); + return boxes; + } + public override string GetName() { return "Current Cell"; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapCurrentUnitObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapCurrentUnitObject.cs index 66df3f19e..d32bf2c1d 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapCurrentUnitObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapCurrentUnitObject.cs @@ -23,24 +23,46 @@ public MapCurrentUnitsObject(PositionAngleProvider positionAngleProvider) Color = Color.Purple; } + static (float xMin, float xMax, float zMin, float zMax) GetUnit(PositionAngle obj) + { + var posAngleX = obj.X; + var posAngleZ = obj.Z; + int xMin = (short)posAngleX; + int xMax = xMin + (posAngleX >= 0 ? 1 : -1); + int zMin = (short)posAngleZ; + int zMax = zMin + (posAngleZ >= 0 ? 1 : -1); + return (Math.Min(xMin, xMax), Math.Max(xMin, xMax), Math.Min(zMin, zMax), Math.Max(zMin, zMax)); + } + protected override List<(float xMin, float xMax, float zMin, float zMax, float y)> GetQuadList() { var quads = new List<(float, float, float, float, float)>(); foreach (var obj in positionAngleProvider()) { - var posAngleX = obj.X; - var posAngleZ = obj.Z; - int xMin = (short)posAngleX; - int xMax = xMin + (posAngleX >= 0 ? 1 : -1); - int zMin = (short)posAngleZ; - int zMax = zMin + (posAngleZ >= 0 ? 1 : -1); - quads.Add((Math.Min(xMin, xMax), Math.Max(xMin, xMax), Math.Min(zMin, zMax), Math.Max(zMin, zMax), (float)obj.Y)); + var unit = GetUnit(obj); + quads.Add((unit.xMin, unit.xMax, unit.zMin, unit.zMax, (float)obj.Y)); } return quads; } + protected override List<(float xMin, float xMax, float zMin, float zMax, float yMin, float yMax)> GetBoxList3D(MapGraphics graphics) + { + var boxes = new List<(float, float, float, float, float, float)>(); + var floors = graphics.floors.GetTriangles(); + + foreach (var obj in positionAngleProvider()) + { + var unit = GetUnit(obj); + float floorY = floors.FindFloorAndY((float)obj.X, (float)obj.Y, (float)obj.Z).floorY; + float y = float.IsNaN(floorY) ? (float)obj.Y : floorY; + boxes.Add((unit.xMin, unit.xMax, unit.zMin, unit.zMax, y, y)); + } + + return boxes; + } + protected override ContextMenuStrip GetContextMenuStrip(MapTracker targetTracker) => new ContextMenuStrip(); public override string GetName() => $"Current Unit for {PositionAngle.NameOfMultiple(positionAngleProvider())}"; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapDrawingObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapDrawingObject.cs index fd7466df3..c053003bf 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapDrawingObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapDrawingObject.cs @@ -5,6 +5,7 @@ using System.Windows.Forms; using OpenTK; using OpenTK.Mathematics; +using STROOP.Variables.SM64MemoryLayout; namespace STROOP.Tabs.MapTab.MapObjects { @@ -14,10 +15,19 @@ public class MapDrawingObject : MapLineObject class Drawing : IHoverData { MapDrawingObject parent; + readonly bool cursorHasHeight; - public Drawing(MapDrawingObject target) + public Drawing(MapDrawingObject target, MapGraphics graphics) { this.parent = target; + cursorHasHeight = graphics.viewMode != MapGraphics.ViewMode.TopDown; + } + + static Vector3 WithHeight(Vector3 position, bool hasHeight) + { + if (!hasHeight) + position.Y = Config.Stream?.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset) ?? 0; + return position; } public void AddContextMenuItems(MapTab tab, ContextMenuStrip menu) @@ -37,7 +47,7 @@ public void AddContextMenuItems(MapTab tab, ContextMenuStrip menu) public void DragTo(Vector3 newPosition, bool setY) { - Vector3 currentVertex = newPosition; + Vector3 currentVertex = WithHeight(newPosition, setY); if (currentVertex != parent._lastVertex) { parent._vertices.Add(parent._lastVertex); @@ -53,7 +63,7 @@ public void SetLookAt(Vector3 lookAt) public void LeftClick(Vector3 position) { - parent._lastVertex = position; + parent._lastVertex = WithHeight(position, cursorHasHeight); } public void RightClick(Vector3 position) @@ -105,6 +115,6 @@ protected override ContextMenuStrip GetContextMenuStrip(MapTracker targetTracker public void ClearDrawing() => _vertices.Clear(); - public override IHoverData GetHoverData(MapGraphics graphics, ref Vector3 position) => drawingEnabled ? new Drawing(this) : null; + public override IHoverData GetHoverData(MapGraphics graphics, ref Vector3 position) => drawingEnabled ? new Drawing(this, graphics) : null; } } diff --git a/STROOP/Tabs/MapTab/MapObjects/MapFacingDividerObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapFacingDividerObject.cs index a83a24725..7f154f0ac 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapFacingDividerObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapFacingDividerObject.cs @@ -36,10 +36,29 @@ protected override List GetVertices(MapGraphics graphics) lst.Add(new Vector3(x2, y, z2)); } - ; return lst; } + protected override void Draw3D(MapGraphics graphics) + { + base.Draw3D(graphics); + + graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => + { + var outlineColor = OpenTKUtilities.ColorToVec4(OutlineColor, OpacityByte); + var fillColor = new Vector4(outlineColor.Xyz, outlineColor.W * 0.25f); + var vertices = GetVertices(graphics); + for (int i = 0; i + 1 < vertices.Count; i += 2) + { + Vector3 a = vertices[i], b = vertices[i + 1]; + Vector3 up = new Vector3(0, Size, 0); + Vector3 p0 = a - up, p1 = b - up, p2 = b + up, p3 = a + up; + graphics.triangleRenderer.Add(p0, p1, p2, false, fillColor, outlineColor, new Vector3(1, 0, 1), true); + graphics.triangleRenderer.Add(p0, p2, p3, false, fillColor, outlineColor, new Vector3(1, 1, 0), true); + } + }); + } + public override Lazy GetInternalImage() => Config.ObjectAssociations.ArrowImage; public override string GetName() => $"Facing Divider for {PositionAngle.NameOfMultiple(positionAngleProvider())}"; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapGhostObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapGhostObject.cs index 952f15da5..a1bcf71f2 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapGhostObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapGhostObject.cs @@ -21,10 +21,12 @@ public MapGhostObject() protected override void DrawTopDown(MapGraphics graphics) { + bool onlyHovered = currentMapTab.AreObjectModelsShown(graphics) + && Config.StroopMainForm.GetTab().GhostsDrawnInGame; graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => { foreach (var pa in positionAngleProvider()) - if (pa is GhostTab.Ghost.GhostPositionAngle a) + if (pa is GhostTab.Ghost.GhostPositionAngle a && (!onlyHovered || hoverData.currentPositionAngle == a)) { var transparent = graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional; var alpha = hoverData.currentPositionAngle == a ? ObjectUtilities.HoverAlpha() : 1; @@ -43,6 +45,8 @@ protected override void DrawTopDown(MapGraphics graphics) angle, Config.ObjectAssociations.MarioMapHatOnlyImage.Value, new Vector4(a.color.X, a.color.Y, a.color.Z, alpha)); + + DrawFacingLine3D(graphics, (float)a.X, (float)a.Y, (float)a.Z, a.Angle); } }); } diff --git a/STROOP/Tabs/MapTab/MapObjects/MapGridlinesObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapGridlinesObject.cs index b2818b25c..7e7a3ef55 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapGridlinesObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapGridlinesObject.cs @@ -10,7 +10,7 @@ namespace STROOP.Tabs.MapTab.MapObjects { - [ObjectDescription("Unit Gridlines", "Grid", nameof(CreateCustom))] + [ObjectDescription("Unit Gridlines", "Grid", nameof(CreateUnits))] [ObjectDescription("Custom Gridlines", "Grid", nameof(CreateCustom))] public class MapGridlinesObject : MapLineObject { @@ -131,16 +131,16 @@ protected void AddVerticesToPositionAngle( float minY, maxY; bool is3DGrid = !float.IsNaN(verticalMultiplier); + float verticalIncrement = is3DGrid ? increment * verticalMultiplier : float.PositiveInfinity; if (!is3DGrid) minY = maxY = positionAngle.Y; else { - float verticalIncrement = increment * verticalMultiplier; minY = (int)((positionAngle.Y - vExpanse) / verticalIncrement - 1) * verticalIncrement; maxY = (int)((positionAngle.Y + vExpanse) / verticalIncrement + 1) * verticalIncrement; } - for (float y = minY; y <= maxY; y += verticalMultiplier) + for (float y = minY; y <= maxY; y += verticalIncrement) { for (var x = viewXMin; x <= viewXMax; x += increment) { diff --git a/STROOP/Tabs/MapTab/MapObjects/MapIconPointObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapIconPointObject.cs index 8b34d10e3..df5d5ab88 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapIconPointObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapIconPointObject.cs @@ -11,17 +11,24 @@ protected MapIconPointObject(ObjectCreateParams creationParameters) { } + protected virtual bool IsGameObject => false; + protected override void DrawTopDown(MapGraphics graphics) { + bool onlyHovered = IsGameObject && currentMapTab.AreObjectModelsShown(graphics); graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => { foreach (var a in positionAngleProvider()) - DrawIcon(graphics, + if (!onlyHovered || hoverData.currentPositionAngle == a) + { + DrawIcon(graphics, graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional, (float)a.X, (float)a.Y, (float)a.Z, Rotates ? (float)a.Angle : 0x8000 - graphics.MapViewAngleValue, GetInternalImage()?.Value, new Vector4(1, 1, 1, hoverData.currentPositionAngle == a ? ObjectUtilities.HoverAlpha() : 1)); + DrawFacingLine3D(graphics, (float)a.X, (float)a.Y, (float)a.Z, a.Angle); + } }); } diff --git a/STROOP/Tabs/MapTab/MapObjects/MapLevelModelObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapLevelModelObject.cs new file mode 100644 index 000000000..b82a524d7 --- /dev/null +++ b/STROOP/Tabs/MapTab/MapObjects/MapLevelModelObject.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; +using STROOP.Core; +using STROOP.Structs; +using STROOP.Structs.Configurations; +using STROOP.Tabs.MapTab.DataUtil; +using STROOP.Variables.SM64MemoryLayout; + +namespace STROOP.Tabs.MapTab.MapObjects +{ + public class MapLevelModelObject : MapObject + { + const int STABLE_UPDATES_BEFORE_REBUILD = 3; + + readonly MapCurrentMapObject fallback = new MapCurrentMapObject(); + readonly Renderers.LevelModelRenderer renderer = new Renderers.LevelModelRenderer(); + readonly Rdram ram = new Rdram(new byte[Config.RamSize]); + + LevelModel model; + int levelKey; + int stableUpdates = -1; + List<(int y, int xMin, int xMax, int zMin, int zMax)> waters; + + public bool showModel = true; + public bool showObjects = true; + public bool showWaterBoxes = false; + public bool showSky = true; + public bool showFog = false; + public bool keepMarioVisible = true; + + public static (Vector3 position, Vector3 focus)? GameCamera { get; private set; } + + const float MARIO_MIN_PIXELS = 40; + public bool cullFaces = true; + public bool hideAboveMario = false; + public float hideAboveMarioOffset = 200; + + public LevelModel Model => model; + + public MapLevelModelObject() : base() + { + } + + public override string GetName() => "Level Model"; + + public override Lazy GetInternalImage() => fallback.GetInternalImage(); + + int ComputeLevelKey(in LevelModel.GameAddresses addresses) + { + static bool Known(uint us, uint jp, uint sh, uint eu) => MappingConfig.IsMapped(RomVersionConfig.SwitchOnly(us, jp, sh, eu)); + + uint area = ram.U32(AreaConfig.CurrentAreaPointerAddress); + var hash = new HashCode(); + if (Known(MiscConfig.LevelIndexAddressUS, MiscConfig.LevelIndexAddressJP, MiscConfig.LevelIndexAddressSH, MiscConfig.LevelIndexAddressEU)) + hash.Add(ram.U8(MiscConfig.LevelIndexAddress)); + hash.Add(area); + hash.Add(Rdram.IsPointer(area) ? ram.U8(area) : 0); + hash.Add(Rdram.IsPointer(area) ? ram.U32(area + 4) : 0); + if (Known(TriangleConfig.LevelTriangleCountAddressUS, TriangleConfig.LevelTriangleCountAddressJP, TriangleConfig.LevelTriangleCountAddressSH, 0)) + hash.Add(ram.S32(TriangleConfig.LevelTriangleCountAddress)); + hash.Add(ram.U32(Config.ObjectAssociations.SegmentTable + 4 * 7)); + uint waters = Known(MiscConfig.WaterPointerAddressUS, MiscConfig.WaterPointerAddressJP, 0, MiscConfig.WaterPointerAddressEU) + ? ram.U32(MiscConfig.WaterPointerAddress) + : 0; + if (Rdram.IsPointer(waters)) + for (uint i = 0, count = (uint)Math.Clamp((int)ram.S16(waters), 0, 64); i < count; i++) + hash.Add(ram.S16(waters + 12 + 12 * i)); + hash.Add(LevelModel.StaticStateKey(ram, addresses)); + return hash.ToHashCode(); + } + + public override void Update() + { + base.Update(); + fallback.Update(); + if (Config.Stream == null || !currentMapTab.IsMapVisible) + return; + + Config.Stream.CopyCachedRam(ram.data); + ram.LoadSegmentTable(Config.ObjectAssociations.SegmentTable); + + var addresses = GetGameAddresses(); + int key = ComputeLevelKey(addresses); + if (key != levelKey) + { + levelKey = key; + stableUpdates = 0; + } + else if (stableUpdates >= 0) + stableUpdates++; + else if (model == null) + stableUpdates = 0; + + if (currentMapTab.NeedsGeometryRefresh()) + stableUpdates = STABLE_UPDATES_BEFORE_REBUILD; + + if (stableUpdates >= STABLE_UPDATES_BEFORE_REBUILD) + { + stableUpdates = -1; + Rebuild(); + } + + if (model != null) + try + { + model.UpdateLevelGeometry(ram, addresses); + } + catch (Exception e) + { + System.Diagnostics.Debug.WriteLine($"Paintings could not be drawn: {e}"); + model.levelInstances.Clear(); + } + + if (model != null && showObjects) + try + { + model.UpdateObjects(ram, addresses, GetDoors()); + } + catch (Exception e) + { + System.Diagnostics.Debug.WriteLine($"Objects could not be placed: {e}"); + model.instances.Clear(); + } + + model?.UpdateTextureAnimations(ram); + GameCamera = model != null && model.TryGetGameCamera(ram, out var cameraPosition, out var cameraFocus) + ? (cameraPosition, cameraFocus) + : null; + waters = showWaterBoxes ? WaterUtilities.GetWaterLevels() : null; + } + + readonly HashSet doors = new HashSet(); + + static readonly uint[] DoorBehaviors = { 0x13000000, 0x13000AFC, 0x13000B0C }; + + HashSet GetDoors() + { + doors.Clear(); + foreach (var obj in Config.StroopMainForm.ObjectSlotsManager.GetLoadedObjectsWithPredicate( + o => Array.IndexOf(DoorBehaviors, o.BehaviorCriteria.BehaviorAddress) >= 0)) + doors.Add(obj.Address); + return doors; + } + + static LevelModel.GameAddresses GetGameAddresses() + { + var addresses = LevelModel.GameAddresses.FromSymbols(MappingConfig.GetSymbolAddress, MarioObjectConfig.PointerAddress, MarioConfig.StructAddress); + if (RomVersionConfig.Version == RomVersion.US) + { + var ghostTab = Config.StroopMainForm.GetTab(); + addresses.ghostHack = ghostTab.GhostHackInstalled; + if (ghostTab.ColoredHatsInstalled) + { + addresses.coloredHatsGenerator = ghostTab.ColoredHatsGeneratorAddress; + addresses.coloredHatsLights = ghostTab.ColoredHatsLightsAddress; + } + } + + return addresses; + } + + public void Rebuild() + { + try + { + model = LevelModel.Build(ram, AreaConfig.CurrentAreaPointerAddress, GetGameAddresses()); + } + catch (Exception e) + { + System.Diagnostics.Debug.WriteLine($"Level model could not be built: {e}"); + model = LevelModel.None; + } + } + + bool UseFallback => !currentMapTab.IsLevelModelShown; + + float GetMaxY() + { + if (!hideAboveMario) + return float.PositiveInfinity; + return (Config.Stream?.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset) ?? 0) + hideAboveMarioOffset; + } + + void DrawModel(MapGraphics graphics) + { + var drawnModel = model; + var drawnWaters = waters; + float maxY = GetMaxY(); + var levelCopies = graphics.MapViewEnablePuView ? GetPuOffsets(graphics) : null; + graphics.drawLayers[(int)MapGraphics.DrawLayers.Background].Add(() => + { + bool marioShown = graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional && showObjects + && drawnModel != null && drawnModel.marioInstances.Count > 0; + renderer.Draw(graphics, drawnModel, maxY, cullFaces, showObjects, drawnWaters, + showSky && graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional, + showFog && graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional, + marioShown ? graphics.marioModelCopies : null, levelCopies, keepMarioVisible ? MARIO_MIN_PIXELS : 0); + if (marioShown) + graphics.ReportMarioModelDrawn(); + if (graphics.viewMode != MapGraphics.ViewMode.ThreeDimensional) + { + GL.Clear(ClearBufferMask.DepthBufferBit); + GL.Disable(EnableCap.DepthTest); + } + }); + } + + const int MAX_PU_COPIES = 81; + + List GetPuOffsets(MapGraphics graphics) + { + var center = graphics.currentView.position; + return currentMapTab.GetPuCenters(graphics) + .Select(c => new Vector3(c.x, 0, c.z)) + .OrderBy(c => (c.Xz - center.Xz).LengthSquared) + .Take(MAX_PU_COPIES) + .ToList(); + } + + const float PU_SIZE = 65536, PU_HALF = 32768, BEACON_HEIGHT = 20000; + + static int PuIndex(float coordinate) => (int)Math.Floor((coordinate + PU_HALF) / PU_SIZE); + + void DrawMarioPuMarker(MapGraphics graphics) + { + if (!graphics.MapViewEnablePuView || Config.Stream == null) + return; + graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => + { + var mario = new Vector3(Models.DataModels.Mario.X, Models.DataModels.Mario.Y, Models.DataModels.Mario.Z); + if (float.IsNaN(mario.X) || float.IsNaN(mario.Z)) + return; + int puX = PuIndex(mario.X), puZ = PuIndex(mario.Z); + var color = new Vector4(1, 0.2f, 1, 1); + + if (graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional) + graphics.lineRenderer.Add(mario, mario + new Vector3(0, BEACON_HEIGHT, 0), color, 3); + + float cx = puX * PU_SIZE, cz = puZ * PU_SIZE; + var corners = new[] + { + new Vector3(cx - PU_HALF, mario.Y, cz - PU_HALF), new Vector3(cx + PU_HALF, mario.Y, cz - PU_HALF), + new Vector3(cx + PU_HALF, mario.Y, cz + PU_HALF), new Vector3(cx - PU_HALF, mario.Y, cz + PU_HALF), + }; + for (int i = 0; i < corners.Length; i++) + graphics.lineRenderer.Add(corners[i], corners[(i + 1) % corners.Length], color, 2); + + graphics.textRenderer.AddText($"Mario PU ({puX}, {puZ})", mario + new Vector3(0, 200, 0), Color.Magenta, + StringAlignment.Center, Renderers.TextRenderer.Fonts.large, StringAlignment.Far); + }); + } + + protected override void DrawTopDown(MapGraphics graphics) + { + if (UseFallback) + fallback.Draw(graphics); + else + DrawModel(graphics); + DrawMarioPuMarker(graphics); + } + + protected override void DrawOrthogonal(MapGraphics graphics) + { + if (!UseFallback) + DrawModel(graphics); + } + + protected override void Draw3D(MapGraphics graphics) + { + if (UseFallback) + fallback.Draw(graphics); + else + DrawModel(graphics); + DrawMarioPuMarker(graphics); + } + } +} diff --git a/STROOP/Tabs/MapTab/MapObjects/MapLineSegmentObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapLineSegmentObject.cs index e47a266b3..a9a3f0e1c 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapLineSegmentObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapLineSegmentObject.cs @@ -51,9 +51,11 @@ protected override List GetVertices(MapGraphics graphics) (double startX, double startZ) = MoreMath.ExtrapolateLine2D(x2, z2, x1, z1, dist + _backwardsSize); (double endX, double endZ) = MoreMath.ExtrapolateLine2D(x1, z1, x2, z2, (_useFixedSize ? 0 : dist) + Size); + double startT = dist == 0 ? 0 : -_backwardsSize / dist; + double endT = dist == 0 ? 0 : ((_useFixedSize ? 0 : dist) + Size) / dist; var vertices = new List(); - vertices.Add(new Vector3((float)startX, 0, (float)startZ)); - vertices.Add(new Vector3((float)endX, 0, (float)endZ)); + vertices.Add(new Vector3((float)startX, (float)(y1 + (y2 - y1) * startT), (float)startZ)); + vertices.Add(new Vector3((float)endX, (float)(y1 + (y2 - y1) * endT), (float)endZ)); return vertices; } diff --git a/STROOP/Tabs/MapTab/MapObjects/MapMapObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapMapObject.cs index 896c7b562..77042954d 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapMapObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapMapObject.cs @@ -2,6 +2,10 @@ using System.Drawing; using OpenTK; using OpenTK.Mathematics; +using STROOP.Structs; +using STROOP.Structs.Configurations; +using STROOP.Utilities; +using STROOP.Variables.SM64MemoryLayout; namespace STROOP.Tabs.MapTab.MapObjects { @@ -43,6 +47,58 @@ protected override void DrawOrthogonal(MapGraphics graphics) { } + protected override void Draw3D(MapGraphics graphics) + { + renderer.SetDrawCalls(graphics); + var img = GetInternalImage(); + if (img == null) + return; + var flat = new Matrix4(1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1); + float y = GetPlaneY3D(); + graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => + { + renderer.texture = GraphicsUtil.TextureFromImage(img.Value); + foreach (var dim in GetDimensions(graphics)) + renderer.AddInstance( + flat + * Matrix4.CreateScale(dim.size.Width, 1, dim.size.Height) + * Matrix4.CreateTranslation(dim.loc.X, y, dim.loc.Y), + graphics.rendererCollection.GetObjectTextureLayer(GetInternalImage().Value), + new Vector4(1) + ); + }); + } + + MapLayout planeYLayout; + int planeYTriangleCount = -1; + float? planeY; + + float GetPlaneY3D() + { + if (Config.Stream == null) + return 0; + MapLayout layout = GetMapLayout(); + if (layout != null && layout.Y != float.MinValue && layout.Y != 0) + return layout.Y; + + if (!MappingConfig.IsMapped(RomVersionConfig.SwitchOnly(TriangleConfig.LevelTriangleCountAddressUS, TriangleConfig.LevelTriangleCountAddressJP, TriangleConfig.LevelTriangleCountAddressSH)) + || !MappingConfig.IsMapped(RomVersionConfig.SwitchOnly(TriangleConfig.TriangleListPointerAddressUS, TriangleConfig.TriangleListPointerAddressJP))) + return Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset); + + int triangleCount = Config.Stream.GetInt32(TriangleConfig.LevelTriangleCountAddress); + if (!Equals(layout, planeYLayout) || triangleCount != planeYTriangleCount) + { + planeYLayout = layout; + planeYTriangleCount = triangleCount; + planeY = null; + foreach (var tri in TriangleUtilities.GetLevelTriangles()) + if (tri.Classification == TriangleClassification.Floor && (!planeY.HasValue || tri.GetMinY() < planeY.Value)) + planeY = tri.GetMinY(); + } + + return planeY ?? Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset); + } + protected List<(PointF loc, SizeF size)> GetDimensions(MapGraphics graphics) { RectangleF rectangle = GetMapLayout().Coordinates; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapMarioObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapMarioObject.cs index ab4cc4186..1b82b281a 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapMarioObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapMarioObject.cs @@ -17,6 +17,8 @@ public MapMarioObject() : base(null) public override Lazy GetInternalImage() => Config.ObjectAssociations.MarioMapImage; + protected override bool IsGameObject => true; + public override void InitSubTrackerContextMenuStrip(MapTab mapTab, ContextMenuStrip targetStrip) { base.InitSubTrackerContextMenuStrip(mapTab, targetStrip); diff --git a/STROOP/Tabs/MapTab/MapObjects/MapMultipleObjectsObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapMultipleObjectsObject.cs index f6d20e0f0..1d1e8e19d 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapMultipleObjectsObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapMultipleObjectsObject.cs @@ -76,14 +76,21 @@ public override Lazy GetInternalImage() public override string GetName() => PositionAngle.NameOfMultiple(positionAngleProvider(), $"All {_objName}"); + protected override bool IsGameObject => true; + protected override void DrawTopDown(MapGraphics graphics) { + bool onlyHovered = currentMapTab.AreObjectModelsShown(graphics); graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => { List<(float x, float y, float z, float angle, Lazy tex, float alpha)> data = GetData(); data.Reverse(); foreach (var d in data) - DrawIcon(graphics, graphics.viewMode != MapGraphics.ViewMode.TopDown, d.x, d.y, d.z, d.angle, d.tex.Value, new Vector4(1, 1, 1, d.alpha)); + if (!onlyHovered || d.alpha != 1) + { + DrawIcon(graphics, graphics.viewMode != MapGraphics.ViewMode.TopDown, d.x, d.y, d.z, d.angle, d.tex.Value, new Vector4(1, 1, 1, d.alpha)); + DrawFacingLine3D(graphics, d.x, d.y, d.z, d.angle); + } }); } diff --git a/STROOP/Tabs/MapTab/MapObjects/MapNextPositionsObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapNextPositionsObject.cs index 29f1c1a72..c1d28816d 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapNextPositionsObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapNextPositionsObject.cs @@ -45,47 +45,137 @@ protected override void DrawTopDown(MapGraphics graphics) protected override void DrawOrthogonal(MapGraphics graphics) => DrawTopDown(graphics); - public List<(float x, float y, float z, float angle, Lazy tex)> GetData() + protected override void Draw3D(MapGraphics graphics) + { + graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => + { + List path = Positions3D(graphics, out ushort marioAngle); + + Vector4 lineColor = Get3DAccentColor(Color.Yellow); + for (int i = 0; i < path.Count - 1; i++) + graphics.lineRenderer.Add(path[i], path[i + 1], lineColor, 2); + + List<(float x, float y, float z, float angle, Lazy tex)> data = ToData(path.Skip(1).ToList(), marioAngle); + if (graphics.marioModelAvailable) + { + foreach (var dataPoint in data) + graphics.marioModelCopies.Add(new Vector3(dataPoint.x, dataPoint.y, dataPoint.z)); + return; + } + + data.Reverse(); + foreach (var dataPoint in data) + { + DrawIcon(graphics, true, dataPoint.x, dataPoint.y, dataPoint.z, dataPoint.angle, dataPoint.tex?.Value, new Vector4(1)); + DrawFacingLine3D(graphics, dataPoint.x, dataPoint.y, dataPoint.z, dataPoint.angle); + } + }); + } + + private List Positions3D(MapGraphics graphics, out ushort marioAngle) { float marioX = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.XOffset); float marioY = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset); float marioZ = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.ZOffset); - float marioYSpeed = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YSpeedOffset); + float ySpeed = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YSpeedOffset); float marioHSpeed = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.HSpeedOffset); - ushort marioAngle = Config.Stream.GetUInt16(MarioConfig.StructAddress + MarioConfig.FacingYawOffset); + marioAngle = Config.Stream.GetUInt16(MarioConfig.StructAddress + MarioConfig.FacingYawOffset); float floorY = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.FloorYOffset); + bool onGround = marioY == floorY; float multiplier = 1; - if (marioY == floorY) // on the ground + if (onGround) { uint floorTri = Config.Stream.GetUInt32(MarioConfig.StructAddress + MarioConfig.FloorTriangleOffset); - float yNorm = Config.Stream.GetSingle(floorTri + TriangleOffsetsConfig.NormY); - multiplier = yNorm; + multiplier = Config.Stream.GetSingle(floorTri + TriangleOffsetsConfig.NormY); } float effectiveSpeed = marioHSpeed * multiplier; + var floors = graphics.floors.GetTriangles(); - List<(float x, float z)> points2D = Enumerable.Range(0, (int)(_numFrames * 4)).ToList() - .ConvertAll(index => 0.25 + index / 4.0) - .ConvertAll(frameStep => ((float x, float z))MoreMath.AddVectorToPoint( - frameStep * effectiveSpeed, marioAngle, marioX, marioZ)); + List path = new List { new Vector3(marioX, marioY, marioZ) }; + float y = marioY; + int numQuarterSteps = (int)(_numFrames * 4); + for (int i = 0; i < numQuarterSteps; i++) + { + (double px, double pz) = MoreMath.AddVectorToPoint((0.25 + i / 4.0) * effectiveSpeed, marioAngle, marioX, marioZ); + float x = (float)px, z = (float)pz; + var (floor, newFloorY) = floors.FindFloorAndY(x, y, z); + bool hasFloor = floor != null && !float.IsNaN(newFloorY); + if (onGround) + { + if (hasFloor && y > newFloorY + 100) + { + onGround = false; + ySpeed = 0; + } + else if (hasFloor) + y = newFloorY; + } + else + { + y += ySpeed / 4; + if (hasFloor && y <= newFloorY) + { + y = newFloorY; + onGround = true; + } + } + + if (i % 4 == 3 && !onGround) + ySpeed = Math.Max(ySpeed - 4, -75); + + path.Add(new Vector3(x, y, z)); + } + + return path; + } + private List<(float x, float y, float z, float angle, Lazy tex)> ToData(List quarterSteps, ushort marioAngle) + { var fullStepTex = _useColoredMarios ? Config.ObjectAssociations.BlueMarioMapImage : Config.ObjectAssociations.MarioMapImage; var quarterStepTex = _useColoredMarios ? Config.ObjectAssociations.OrangeMarioMapImage : Config.ObjectAssociations.MarioMapImage; List<(float x, float y, float z, float angle, Lazy tex)> data = new List<(float x, float y, float z, float angle, Lazy tex)>(); - for (int i = 0; i < points2D.Count; i++) + for (int i = 0; i < quarterSteps.Count; i++) { bool isFullStep = i % 4 == 3; if (!isFullStep && !_showQuarterSteps) continue; - (float x, float z) = points2D[i]; var tex = isFullStep ? fullStepTex : quarterStepTex; - data.Add((x, marioY, z, marioAngle, tex)); + data.Add((quarterSteps[i].X, quarterSteps[i].Y, quarterSteps[i].Z, marioAngle, tex)); } return data; } + public List<(float x, float y, float z, float angle, Lazy tex)> GetData() + { + float marioX = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.XOffset); + float marioY = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset); + float marioZ = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.ZOffset); + float marioYSpeed = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YSpeedOffset); + float marioHSpeed = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.HSpeedOffset); + ushort marioAngle = Config.Stream.GetUInt16(MarioConfig.StructAddress + MarioConfig.FacingYawOffset); + + float floorY = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.FloorYOffset); + float multiplier = 1; + if (marioY == floorY) // on the ground + { + uint floorTri = Config.Stream.GetUInt32(MarioConfig.StructAddress + MarioConfig.FloorTriangleOffset); + float yNorm = Config.Stream.GetSingle(floorTri + TriangleOffsetsConfig.NormY); + multiplier = yNorm; + } + + float effectiveSpeed = marioHSpeed * multiplier; + + List<(float x, float z)> points2D = Enumerable.Range(0, (int)(_numFrames * 4)).ToList() + .ConvertAll(index => 0.25 + index / 4.0) + .ConvertAll(frameStep => ((float x, float z))MoreMath.AddVectorToPoint( + frameStep * effectiveSpeed, marioAngle, marioX, marioZ)); + + return ToData(points2D.ConvertAll(p => new Vector3(p.x, marioY, p.z)), marioAngle); + } + protected override ContextMenuStrip GetContextMenuStrip(MapTracker targetTracker) { ToolStripMenuItem itemUseColoredMarios = new ToolStripMenuItem("Use Colored Marios"); diff --git a/STROOP/Tabs/MapTab/MapObjects/MapObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapObject.cs index 64ff05e98..2c31e51da 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapObject.cs @@ -232,6 +232,21 @@ public static void DrawIcon( graphics.objectRenderer.AddInstance(transform, textureIndex, color); } + protected Vector4 Get3DAccentColor(Color fallback) + => OpenTKUtilities.ColorToVec4(OutlineColor.ToArgb() == Color.Black.ToArgb() ? fallback : OutlineColor, OpacityByte); + + protected void DrawFacingLine3D(MapGraphics graphics, float x, float y, float z, double angle) + { + if (graphics.viewMode != MapGraphics.ViewMode.ThreeDimensional || !Rotates || double.IsNaN(angle)) + return; + float length = Size * Get3DIconScale(graphics, x, y, z) * 2; + (double endX, double endZ) = MoreMath.AddVectorToPoint(length, angle, x, z); + Vector3 from = new Vector3(x, y, z); + Vector3 to = new Vector3((float)endX, y, (float)endZ); + graphics.lineRenderer.Add(from, to, new Vector4(0, 0, 0, OpacityByte / 255f), 4); + graphics.lineRenderer.Add(from, to, Get3DAccentColor(Color.White), 2); + } + public void Draw(MapGraphics graphics) { switch (graphics.viewMode) diff --git a/STROOP/Tabs/MapTab/MapObjects/MapObjectObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapObjectObject.cs index 94d1dc38e..4ec36f60c 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapObjectObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapObjectObject.cs @@ -241,6 +241,8 @@ protected MapObjectObject(ObjectProvider objectProvider) this.objectProvider = objectProvider; } + protected override bool IsGameObject => true; + public override Lazy GetInternalImage() { Lazy result = null; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapPreviousPositionsObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapPreviousPositionsObject.cs index a60717a66..2ba9c6b2d 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapPreviousPositionsObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapPreviousPositionsObject.cs @@ -82,6 +82,7 @@ protected override void DrawTopDown(MapGraphics graphics) dataPoint.x, dataPoint.y, dataPoint.z, dataPoint.angle, dataPoint.tex.Value, new Vector4(1)); + DrawFacingLine3D(graphics, dataPoint.x, dataPoint.y, dataPoint.z, dataPoint.angle); if (showTruncatedQsIndicators && dataPoint.earlyQs != null) graphics.textRenderer.AddText( $"{dataPoint.earlyQs.Value}/4", diff --git a/STROOP/Tabs/MapTab/MapObjects/MapPuGridlinesObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapPuGridlinesObject.cs index df28b3d27..7f700fff8 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapPuGridlinesObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapPuGridlinesObject.cs @@ -40,12 +40,15 @@ protected override List GetVertices(MapGraphics graphics) { float marioY = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset); - int xMin = ((((int)graphics.MapViewXMin) / 65536) - 1) * 65536; - int xMax = ((((int)graphics.MapViewXMax) / 65536) + 1) * 65536; - int zMin = ((((int)graphics.MapViewZMin) / 65536) - 1) * 65536; - int zMax = ((((int)graphics.MapViewZMax) / 65536) + 1) * 65536; + (int xMin, int xMax, int zMin, int zMax) = GetPuBounds(graphics); var vertices = new List(); + if (graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional) + { + MapCellGridlinesObject.AddColumnGrid3D(vertices, xMin, xMax, zMin, zMax, 65536, marioY, marioY - WALL_HALF_HEIGHT_3D, marioY + WALL_HALF_HEIGHT_3D); + return vertices; + } + for (int x = xMin; x <= xMax; x += 65536) { vertices.Add(new Vector3(x, marioY, zMin)); @@ -64,12 +67,19 @@ protected override List GetVertices(MapGraphics graphics) { float marioY = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset); - int xMin = ((((int)graphics.MapViewXMin) / 65536) - 1) * 65536 - 32768; - int xMax = ((((int)graphics.MapViewXMax) / 65536) + 1) * 65536 + 32768; - int zMin = ((((int)graphics.MapViewZMin) / 65536) - 1) * 65536 - 32768; - int zMax = ((((int)graphics.MapViewZMax) / 65536) + 1) * 65536 + 32768; + (int xMin, int xMax, int zMin, int zMax) = GetPuBounds(graphics); + xMin -= 32768; + xMax += 32768; + zMin -= 32768; + zMax += 32768; var vertices = new List(); + if (graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional) + { + MapCellGridlinesObject.AddColumnGrid3D(vertices, xMin, xMax, zMin, zMax, 65536, marioY, marioY - WALL_HALF_HEIGHT_3D, marioY + WALL_HALF_HEIGHT_3D); + return vertices; + } + for (int x = xMin; x <= xMax; x += 65536) { vertices.Add(new Vector3(x, marioY, zMin)); @@ -88,10 +98,7 @@ protected override List GetVertices(MapGraphics graphics) { float marioY = Config.Stream.GetSingle(MarioConfig.StructAddress + MarioConfig.YOffset); - int xMin = ((((int)graphics.MapViewXMin) / 65536) - 1) * 65536; - int xMax = ((((int)graphics.MapViewXMax) / 65536) + 1) * 65536; - int zMin = ((((int)graphics.MapViewZMin) / 65536) - 1) * 65536; - int zMax = ((((int)graphics.MapViewZMax) / 65536) + 1) * 65536; + (int xMin, int xMax, int zMin, int zMax) = GetPuBounds(graphics); List vertices = new List(); for (int x = xMin; x <= xMax; x += 65536) @@ -103,6 +110,12 @@ protected override List GetVertices(MapGraphics graphics) float z1 = z - 8192; float z2 = z + 8192; + if (graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional) + { + MapCellGridlinesObject.AddColumnGrid3D(vertices, x1, x2, z1, z2, x2 - x1, marioY, marioY - WALL_HALF_HEIGHT_3D, marioY + WALL_HALF_HEIGHT_3D); + continue; + } + vertices.Add(new Vector3(x1, marioY, z1)); vertices.Add(new Vector3(x1, marioY, z2)); @@ -124,6 +137,26 @@ protected override List GetVertices(MapGraphics graphics) } } + const int PU_EXPANSE_3D = 3; + const float WALL_HALF_HEIGHT_3D = 2000; + + static (int xMin, int xMax, int zMin, int zMax) GetPuBounds(MapGraphics graphics) + { + if (graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional) + { + Vector3 center = graphics.currentView.position; + int centerX = (int)Math.Round(center.X / 65536); + int centerZ = (int)Math.Round(center.Z / 65536); + return ((centerX - PU_EXPANSE_3D) * 65536, (centerX + PU_EXPANSE_3D) * 65536, + (centerZ - PU_EXPANSE_3D) * 65536, (centerZ + PU_EXPANSE_3D) * 65536); + } + + return (((((int)graphics.MapViewXMin) / 65536) - 1) * 65536, + ((((int)graphics.MapViewXMax) / 65536) + 1) * 65536, + ((((int)graphics.MapViewZMin) / 65536) - 1) * 65536, + ((((int)graphics.MapViewZMax) / 65536) + 1) * 65536); + } + public override string GetName() { return "PU Gridlines"; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapQuadObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapQuadObject.cs index 58ac398d5..85daa481e 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapQuadObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapQuadObject.cs @@ -12,30 +12,78 @@ public MapQuadObject() : base() } protected override void DrawTopDown(MapGraphics graphics) + { + graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => AddQuads(graphics)); + } + + protected override void DrawOrthogonal(MapGraphics graphics) => DrawTopDown(graphics); + + protected override void Draw3D(MapGraphics graphics) { graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => { - var color = Utilities.OpenTKUtilities.ColorToVec4(Color, OpacityByte); - var outlineColor = Utilities.OpenTKUtilities.ColorToVec4(OutlineColor); - - foreach (var quad in GetQuadList()) + var boxes = GetBoxList3D(graphics); + if (boxes == null) { - Matrix4 transform = Matrix4.CreateRotationX((float)Math.PI / 2) - * Matrix4.CreateScale((quad.xMax - quad.xMin) * 0.5f, 1, (quad.zMax - quad.zMin) * 0.5f) - * Matrix4.CreateTranslation((quad.xMin + quad.xMax) * 0.5f, quad.y, (quad.zMin + quad.zMax) * 0.5f); - graphics.circleRenderer.AddInstance( - graphics.viewMode != MapGraphics.ViewMode.TopDown, - transform, - OutlineWidth, - color, - outlineColor, - Renderers.ShapeRenderer.Shapes.Quad); + AddQuads(graphics); + return; } + + var color = Utilities.OpenTKUtilities.ColorToVec4(Color, OpacityByte); + var outlineColor = Utilities.OpenTKUtilities.ColorToVec4(OutlineColor); + foreach (var box in boxes) + AddBox(graphics, box, color, outlineColor); }); } - protected override void DrawOrthogonal(MapGraphics graphics) => DrawTopDown(graphics); + void AddQuads(MapGraphics graphics) + { + var color = Utilities.OpenTKUtilities.ColorToVec4(Color, OpacityByte); + var outlineColor = Utilities.OpenTKUtilities.ColorToVec4(OutlineColor); + + foreach (var quad in GetQuadList()) + { + Matrix4 transform = Matrix4.CreateRotationX((float)Math.PI / 2) + * Matrix4.CreateScale((quad.xMax - quad.xMin) * 0.5f, 1, (quad.zMax - quad.zMin) * 0.5f) + * Matrix4.CreateTranslation((quad.xMin + quad.xMax) * 0.5f, quad.y, (quad.zMin + quad.zMax) * 0.5f); + graphics.circleRenderer.AddInstance( + graphics.viewMode != MapGraphics.ViewMode.TopDown, + transform, + OutlineWidth, + color, + outlineColor, + Renderers.ShapeRenderer.Shapes.Quad); + } + } + + void AddBox(MapGraphics graphics, (float xMin, float xMax, float zMin, float zMax, float yMin, float yMax) box, Vector4 color, Vector4 outlineColor) + { + Vector3 Corner(float x, float y, float z) => new Vector3(x, y, z); + float x0 = box.xMin, x1 = box.xMax, z0 = box.zMin, z1 = box.zMax; + float y0 = Math.Min(box.yMin, box.yMax), y1 = Math.Max(box.yMin, box.yMax); + + if (y1 - y0 < 1e-3f) + { + AddFace(graphics, Corner(x0, y0, z0), Corner(x1, y0, z0), Corner(x1, y0, z1), Corner(x0, y0, z1), color, outlineColor); + return; + } + + AddFace(graphics, Corner(x0, y0, z0), Corner(x1, y0, z0), Corner(x1, y0, z1), Corner(x0, y0, z1), color, outlineColor); + AddFace(graphics, Corner(x0, y1, z0), Corner(x1, y1, z0), Corner(x1, y1, z1), Corner(x0, y1, z1), color, outlineColor); + AddFace(graphics, Corner(x0, y0, z0), Corner(x1, y0, z0), Corner(x1, y1, z0), Corner(x0, y1, z0), color, outlineColor); + AddFace(graphics, Corner(x0, y0, z1), Corner(x1, y0, z1), Corner(x1, y1, z1), Corner(x0, y1, z1), color, outlineColor); + AddFace(graphics, Corner(x0, y0, z0), Corner(x0, y0, z1), Corner(x0, y1, z1), Corner(x0, y1, z0), color, outlineColor); + AddFace(graphics, Corner(x1, y0, z0), Corner(x1, y0, z1), Corner(x1, y1, z1), Corner(x1, y1, z0), color, outlineColor); + } + + void AddFace(MapGraphics graphics, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, Vector4 color, Vector4 outlineColor) + { + graphics.triangleRenderer.Add(p0, p1, p2, false, color, outlineColor, new Vector3(OutlineWidth, 0, OutlineWidth), true); + graphics.triangleRenderer.Add(p0, p2, p3, false, color, outlineColor, new Vector3(OutlineWidth, OutlineWidth, 0), true); + } protected abstract List<(float xMin, float xMax, float zMin, float zMax, float y)> GetQuadList(); + + protected virtual List<(float xMin, float xMax, float zMin, float zMax, float yMin, float yMax)> GetBoxList3D(MapGraphics graphics) => null; } } diff --git a/STROOP/Tabs/MapTab/MapObjects/MapSectorObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapSectorObject.cs index fdbe86ef8..6782171cd 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapSectorObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapSectorObject.cs @@ -31,20 +31,20 @@ protected override void DrawTopDown(MapGraphics graphics) { graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => { - List<(float centerX, float centerZ, float radius, float angle, float angleRadius)> dimenstionList = GetDimensions(); - foreach ((float centerX, float centerZ, float radius, float angle, float angleRadius) in dimenstionList) + var dimenstionList = GetDimensions(); + foreach ((float centerX, float centerY, float centerZ, float radius, float angle, float angleRadius) in dimenstionList) { var pfft = MoreMath.AngleUnitsToRadians(angle - angleRadius); var pfft2 = MoreMath.AngleUnitsToRadians(angle + angleRadius); graphics.lineRenderer.Add( - new Vector3(centerX, 0, centerZ), - new Vector3(centerX + (float)Math.Sin(pfft) * radius, 0, centerZ + (float)Math.Cos(pfft) * radius), + new Vector3(centerX, centerY, centerZ), + new Vector3(centerX + (float)Math.Sin(pfft) * radius, centerY, centerZ + (float)Math.Cos(pfft) * radius), OpenTKUtilities.ColorToVec4(OutlineColor), OutlineWidth); graphics.lineRenderer.Add( - new Vector3(centerX, 0, centerZ), - new Vector3(centerX + (float)Math.Sin(pfft2) * radius, 0, centerZ + (float)Math.Cos(pfft2) * radius), + new Vector3(centerX, centerY, centerZ), + new Vector3(centerX + (float)Math.Sin(pfft2) * radius, centerY, centerZ + (float)Math.Cos(pfft2) * radius), OpenTKUtilities.ColorToVec4(OutlineColor), OutlineWidth); } @@ -53,13 +53,48 @@ protected override void DrawTopDown(MapGraphics graphics) protected override void DrawOrthogonal(MapGraphics graphics) => DrawTopDown(graphics); - protected List<(float centerX, float centerZ, float radius, float angle, float angleRadius)> GetDimensions() + protected override void Draw3D(MapGraphics graphics) { - var lst = new List<(float centerX, float centerZ, float radius, float angle, float angleRadius)>(); + graphics.drawLayers[(int)MapGraphics.DrawLayers.FillBuffers].Add(() => + { + var fillColor = OpenTKUtilities.ColorToVec4(Color, OpacityByte); + var outlineColor = OpenTKUtilities.ColorToVec4(OutlineColor); + foreach ((float centerX, float centerY, float centerZ, float radius, float angle, float angleRadius) in GetDimensions()) + { + float angleSpan = Math.Min(2 * Math.Abs(angleRadius), 65536); + int numSegments = Math.Max(2, (int)Math.Ceiling((NUM_POINTS_2D - 1) * angleSpan / 65536)); + var center = new Vector3(centerX, centerY, centerZ); + + Vector3 PointAt(int i) + { + double radians = MoreMath.AngleUnitsToRadians(angle - angleSpan / 2 + angleSpan * i / numSegments); + return new Vector3(centerX + (float)Math.Sin(radians) * radius, centerY, centerZ + (float)Math.Cos(radians) * radius); + } + + Vector3 previous = PointAt(0); + if (angleSpan < 65536) + graphics.lineRenderer.Add(center, previous, outlineColor, OutlineWidth); + for (int i = 1; i <= numSegments; i++) + { + Vector3 current = PointAt(i); + graphics.triangleRenderer.Add(center, previous, current, false, fillColor, outlineColor, Vector3.Zero, true); + graphics.lineRenderer.Add(previous, current, outlineColor, OutlineWidth); + previous = current; + } + + if (angleSpan < 65536) + graphics.lineRenderer.Add(center, previous, outlineColor, OutlineWidth); + } + }); + } + + protected List<(float centerX, float centerY, float centerZ, float radius, float angle, float angleRadius)> GetDimensions() + { + var lst = new List<(float centerX, float centerY, float centerZ, float radius, float angle, float angleRadius)>(); foreach (var _posAngle in positionAngleProvider()) { (double x, double y, double z, double angle) = _posAngle.GetValues(); - lst.Add(((float)x, (float)z, Size, (float)angle, _angleRadius)); + lst.Add(((float)x, (float)y, (float)z, Size, (float)angle, _angleRadius)); } return lst; diff --git a/STROOP/Tabs/MapTab/MapObjects/MapTriangleObject.cs b/STROOP/Tabs/MapTab/MapObjects/MapTriangleObject.cs index 790a71484..209ca6b6a 100644 --- a/STROOP/Tabs/MapTab/MapObjects/MapTriangleObject.cs +++ b/STROOP/Tabs/MapTab/MapObjects/MapTriangleObject.cs @@ -265,7 +265,7 @@ protected override void Draw3D(MapGraphics graphics) baseColor.W = OpacityByte / 255f; var projectionColor = new Vector4(baseColor.Xyz, _projectionAlphaMultiplier * baseColor.W); - if (!graphics.view3D.display3DLevelGeometry) + if (!graphics.Draw3DCollisionGeometry || tri.AssociatedObject != 0) graphics.triangleRenderer.Add( tri.p1, tri.p2, diff --git a/STROOP/Tabs/MapTab/MapTab.cs b/STROOP/Tabs/MapTab/MapTab.cs index 939ef5865..55f4665db 100644 --- a/STROOP/Tabs/MapTab/MapTab.cs +++ b/STROOP/Tabs/MapTab/MapTab.cs @@ -157,7 +157,7 @@ public void Load2D() graphics.Load(() => new Renderers.RendererCollection()); InitializeControls(); - comboBoxViewMode.SelectedIndex = 0; + comboBoxViewMode.SelectedIndex = (int)MapGraphics.ViewMode.ThreeDimensional; if (!System.IO.File.Exists(DEFAULT_TRACKER_FILE)) flowLayoutPanelMapTrackers.Controls.Clear(); else @@ -175,6 +175,18 @@ public MapLayout GetMapLayout(object mapLayoutChoice = null) => bool displayingExtendedBoundaries = false; bool needsGeometryRefresh, _needsGeometryRefreshInternal; public bool NeedsGeometryRefresh() => needsGeometryRefresh; + + MapLevelModelObject levelModelObject; + + public bool IsMapVisible { get; private set; } = true; + + public bool IsLevelModelShown => levelModelObject != null + && levelModelObject.showModel + && levelModelObject.Model is { IsEmpty: false } + && !(comboBoxMapOptionsLevel.SelectedItem is MapLayout); + + public bool AreObjectModelsShown(MapGraphics graphics) => + graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional && IsLevelModelShown && levelModelObject.showObjects; public void RequireGeometryUpdate() => _needsGeometryRefreshInternal = true; @@ -187,6 +199,17 @@ public BackgroundImage GetBackgroundImage(object backgroundChoice = null) public List<(float x, float z)> GetPuCenters(MapGraphics graphics) { + if (graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional) + { + var camera = graphics.currentView.position; + int puX = (int)Math.Round(camera.X / 65536), puZ = (int)Math.Round(camera.Z / 65536); + var around = new List<(float x, float z)>(); + for (int x = puX - 1; x <= puX + 1; x++) + for (int z = puZ - 1; z <= puZ + 1; z++) + around.Add((x * 65536f, z * 65536f)); + return around; + } + int xMin = ((((int)graphics.MapViewXMin) / 65536) - 1) * 65536; int xMax = ((((int)graphics.MapViewXMax) / 65536) + 1) * 65536; int zMin = ((((int)graphics.MapViewZMin) / 65536) - 1) * 65536; @@ -360,7 +383,7 @@ private void InitializeControls() checkBoxMapOptionsTrackMario.Checked = true; // FlowLayoutPanel - flowLayoutPanelMapTrackers.Initialize(new MapCurrentMapObject(), new MapCurrentBackgroundObject()); + flowLayoutPanelMapTrackers.Initialize(levelModelObject = new MapLevelModelObject(), new MapCurrentBackgroundObject()); // ComboBox for Level List mapLayouts = MapAssociations.GetAllMaps(); @@ -474,6 +497,7 @@ private void InitializeControls() textBoxMapControllersAngleCustom.AddEnterAction(() => radioButtonMapControllersAngleCustom.Checked = true); checkBoxMapOptionsEnablePuView.Click += (sender, e) => graphics.MapViewEnablePuView = checkBoxMapOptionsEnablePuView.Checked; + InitGameCameraCheckBox(); checkBoxMapOptionsScaleIconSizes.Click += (sender, e) => graphics.MapViewScaleIconSizes = checkBoxMapOptionsScaleIconSizes.Checked; checkBoxMapControllersCenterChangeByPixels.Click += (sender, e) => graphics.MapViewCenterChangeByPixels = checkBoxMapControllersCenterChangeByPixels.Checked; @@ -552,11 +576,45 @@ void AddViewModeContextMenu() itemRefreshLevelGeometry.Click += (__, ___) => RequireGeometryUpdate(); ctx.Items.Add(itemRefreshLevelGeometry); + var itemLevelModel = new ToolStripMenuItem("Level Model"); + var itemShowLevelModel = new ToolStripMenuItem("Show Textured Level Model") { Checked = levelModelObject.showModel }; + itemShowLevelModel.Click += (__, ___) => itemShowLevelModel.Checked = levelModelObject.showModel = !levelModelObject.showModel; + var itemShowObjects = new ToolStripMenuItem("Show Mario and Objects") { Checked = levelModelObject.showObjects }; + itemShowObjects.Click += (__, ___) => itemShowObjects.Checked = levelModelObject.showObjects = !levelModelObject.showObjects; + var itemShowWater = new ToolStripMenuItem("Show Water Boxes (collision)") { Checked = levelModelObject.showWaterBoxes }; + itemShowWater.Click += (__, ___) => itemShowWater.Checked = levelModelObject.showWaterBoxes = !levelModelObject.showWaterBoxes; + var itemShowSky = new ToolStripMenuItem("Show Sky (3D)") { Checked = levelModelObject.showSky }; + itemShowSky.Click += (__, ___) => itemShowSky.Checked = levelModelObject.showSky = !levelModelObject.showSky; + var itemShowFog = new ToolStripMenuItem("Show Fog (3D)") { Checked = levelModelObject.showFog }; + itemShowFog.Click += (__, ___) => itemShowFog.Checked = levelModelObject.showFog = !levelModelObject.showFog; + var itemKeepMarioVisible = new ToolStripMenuItem("Keep Mario Visible From Afar (3D)") { Checked = levelModelObject.keepMarioVisible }; + itemKeepMarioVisible.Click += (__, ___) => itemKeepMarioVisible.Checked = levelModelObject.keepMarioVisible = !levelModelObject.keepMarioVisible; + var itemCullFaces = new ToolStripMenuItem("Cull Back Faces (like the game)") { Checked = levelModelObject.cullFaces }; + itemCullFaces.Click += (__, ___) => itemCullFaces.Checked = levelModelObject.cullFaces = !levelModelObject.cullFaces; + var itemHideAboveMario = new ToolStripMenuItem("Hide Geometry Above Mario") { Checked = levelModelObject.hideAboveMario }; + itemHideAboveMario.Click += (__, ___) => itemHideAboveMario.Checked = levelModelObject.hideAboveMario = !levelModelObject.hideAboveMario; + var itemHideAboveMarioOffset = new ToolStripMenuItem("Set Height Above Mario..."); + itemHideAboveMarioOffset.Click += (__, ___) => + levelModelObject.hideAboveMarioOffset = (float)DialogUtilities.GetDoubleFromDialog( + levelModelObject.hideAboveMarioOffset, + levelModelObject.hideAboveMarioOffset.ToString(), + "Hide the level model above Mario's height plus:"); + itemLevelModel.DropDownItems.Add(itemShowLevelModel); + itemLevelModel.DropDownItems.Add(itemShowObjects); + itemLevelModel.DropDownItems.Add(itemShowWater); + itemLevelModel.DropDownItems.Add(itemShowSky); + itemLevelModel.DropDownItems.Add(itemShowFog); + itemLevelModel.DropDownItems.Add(itemKeepMarioVisible); + itemLevelModel.DropDownItems.Add(itemCullFaces); + itemLevelModel.DropDownItems.Add(itemHideAboveMario); + itemLevelModel.DropDownItems.Add(itemHideAboveMarioOffset); + ctx.Items.Add(itemLevelModel); + if (graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional) { ctx.Items.Add(new ToolStripSeparator()); - var itemDisplayLevelGeometry = new ToolStripMenuItem("Display Level Geometry"); + var itemDisplayLevelGeometry = new ToolStripMenuItem("Display Collision Geometry"); itemDisplayLevelGeometry.Checked = graphics.view3D.display3DLevelGeometry; itemDisplayLevelGeometry.Click += (__, ___) => itemDisplayLevelGeometry.Checked = graphics.view3D.display3DLevelGeometry = !graphics.view3D.display3DLevelGeometry; ctx.Items.Add(itemDisplayLevelGeometry); @@ -588,7 +646,7 @@ void AddViewModeContextMenu() itemCameraModeFree.Click += (__, ___) => { graphics.view3D.camera3DMode = View3D.Camera3DMode.Free; - itemCameraModeInGame.Checked = itemCameraModePivot.Checked = !(itemCameraModeInGame.Checked = true); + itemCameraModeInGame.Checked = itemCameraModePivot.Checked = !(itemCameraModeFree.Checked = true); }; ctx.Items.Add(itemCameraModeInGame); ctx.Items.Add(itemCameraModePivot); @@ -635,8 +693,40 @@ void AddViewModeContextMenu() }; } + System.Windows.Forms.CheckBox checkBoxMapOptionsGameCamera; + + void InitGameCameraCheckBox() + { + checkBoxMapOptionsGameCamera = new System.Windows.Forms.CheckBox + { + Text = "Game Cam (3D)", + AutoSize = true, + Location = new System.Drawing.Point(checkBoxMapOptionsEnablePuView.Right + 4, checkBoxMapOptionsEnablePuView.Top), + UseVisualStyleBackColor = true, + }; + checkBoxMapOptionsGameCamera.Click += (sender, e) => + { + if (checkBoxMapOptionsGameCamera.Checked) + { + comboBoxViewMode.SelectedIndex = (int)MapGraphics.ViewMode.ThreeDimensional; + graphics.viewMode = MapGraphics.ViewMode.ThreeDimensional; + graphics.view3D.camera3DMode = View3D.Camera3DMode.InGame; + } + else + graphics.view3D.camera3DMode = View3D.Camera3DMode.FocusOnPositionAngle; + }; + tabPageMapOptions.Controls.Add(checkBoxMapOptionsGameCamera); + } + public override void Update(bool active) { + if (checkBoxMapOptionsGameCamera != null && graphics?.view3D != null) + { + bool onGameCamera = graphics.viewMode == MapGraphics.ViewMode.ThreeDimensional && graphics.view3D.camera3DMode == View3D.Camera3DMode.InGame; + if (checkBoxMapOptionsGameCamera.Checked != onGameCamera) + checkBoxMapOptionsGameCamera.Checked = onGameCamera; + } + if (!_isLoaded2D) return; semaphoreTrackers.Clear(); @@ -654,7 +744,11 @@ public override void Update(bool active) } } - graphics.UpdateCursor(); + active |= popouts.Count > 0; + IsMapVisible = active; + + if (active) + graphics.UpdateCursor(); if (displayingExtendedBoundaries != SavedSettingsConfig.UseExtendedLevelBoundaries) { displayingExtendedBoundaries = SavedSettingsConfig.UseExtendedLevelBoundaries; @@ -673,7 +767,6 @@ public override void Update(bool active) { flowLayoutPanelMapTrackers.UpdateControl(); - active |= popouts.Count > 0; if (active) { base.Update(active); @@ -701,7 +794,9 @@ public override void Update(bool active) Config.Stream.SetValue(target.Z, CamHackConfig.StructAddress + CamHackConfig.FocusZOffset); } - int numLevelTriangles = Config.Stream.GetInt32(TriangleConfig.LevelTriangleCountAddress); + bool triangleCountKnown = MappingConfig.IsMapped(RomVersionConfig.SwitchOnly( + TriangleConfig.LevelTriangleCountAddressUS, TriangleConfig.LevelTriangleCountAddressJP, TriangleConfig.LevelTriangleCountAddressSH)); + int numLevelTriangles = triangleCountKnown ? Config.Stream.GetInt32(TriangleConfig.LevelTriangleCountAddress) : _numLevelTriangles; if (numLevelTriangles != _numLevelTriangles) { RequireGeometryUpdate(); diff --git a/STROOP/Tabs/MapTab/Renderers/InstanceRenderer.cs b/STROOP/Tabs/MapTab/Renderers/InstanceRenderer.cs index cea003722..75c3465d1 100644 --- a/STROOP/Tabs/MapTab/Renderers/InstanceRenderer.cs +++ b/STROOP/Tabs/MapTab/Renderers/InstanceRenderer.cs @@ -1,7 +1,8 @@ -using OpenTK.Graphics.OpenGL; +using OpenTK.Graphics.OpenGL; using OpenTK; using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using OpenTK.Mathematics; using STROOP.Core; @@ -14,7 +15,6 @@ public abstract class InstanceRenderer : Renderer where InstanceDa protected List instances = new List(); protected readonly int instanceSize; protected int maxInstances { get; private set; } - protected IntPtr dataPtr { get; private set; } public int uniform_viewProjection { get; private set; } @@ -25,16 +25,14 @@ public abstract class InstanceRenderer : Renderer where InstanceDa public InstanceRenderer() { instanceSize = Marshal.SizeOf(typeof(InstanceData)); + if (instanceSize != Unsafe.SizeOf()) + throw new InvalidOperationException($"{typeof(InstanceData)} is not blittable"); } - protected void WriteDataToBuffer() + void WriteDataToBuffer() { - IntPtr ptr = dataPtr; - foreach (var instance in instances) - { - Marshal.StructureToPtr(instance, ptr, false); - ptr = IntPtr.Add(ptr, instanceSize); - } + if (instances.Count > 0) + GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, instanceSize * instances.Count, ref CollectionsMarshal.AsSpan(instances)[0]); } protected void Init(int maxExpectedInstances) @@ -55,19 +53,12 @@ protected void UpdateBuffer(int maxInstances, bool writeData = true) if (maxInstances > this.maxInstances) { - if (dataPtr != IntPtr.Zero) - Marshal.FreeHGlobal(dataPtr); - dataPtr = Marshal.AllocHGlobal(bufferSize); this.maxInstances = maxInstances; - if (writeData) - WriteDataToBuffer(); - GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)bufferSize, writeData ? dataPtr : IntPtr.Zero, BufferUsageHint.StreamDraw); + GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)bufferSize, IntPtr.Zero, BufferUsageHint.StreamDraw); } - else - { + + if (writeData) WriteDataToBuffer(); - GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, (IntPtr)(instanceSize * instances.Count), dataPtr); - } } protected void BeginDraw(MapGraphics graphics, bool updateBuffer = true) diff --git a/STROOP/Tabs/MapTab/Renderers/LevelModelRenderer.cs b/STROOP/Tabs/MapTab/Renderers/LevelModelRenderer.cs new file mode 100644 index 000000000..c66e9ec83 --- /dev/null +++ b/STROOP/Tabs/MapTab/Renderers/LevelModelRenderer.cs @@ -0,0 +1,492 @@ +using System; +using System.Collections.Generic; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; +using STROOP.Tabs.MapTab.DataUtil; + +namespace STROOP.Tabs.MapTab.Renderers +{ + public class LevelModelRenderer + { + const int LAYER_ALPHA = 4, LAYER_TRANSPARENT = 5; + + const uint WaterColor = 0x66D57B3A; + const int ShadowSegments = 16; + + int shader, vao, buffer, dynamicVao, dynamicBuffer, frameVao, frameBuffer; + int bufferCapacity, uploadedLength; + readonly List textures = new List(); + LevelModel uploaded; + byte[] dynamicVertices = new byte[MeshStore.VertexSize * 1024]; + int dynamicLength; + + int uniform_recolorRed, uniform_viewRotation, uniform_uvTransform, uniform_texGen, uniform_fogEnabled, uniform_fogFactors, uniform_fogPlanes, uniform_fogColor; + int uniform_viewProjection, uniform_model, uniform_tex, uniform_useTexture, uniform_decal, uniform_alphaCutoff, uniform_opaque, uniform_maxY; + + int skyShader, skyVao, skyTexture; + int uniform_sky_useTexture, uniform_sky_color, uniform_sky_windowOrigin; + + static int CreateVertexArray(out int vertexBuffer) + { + int vertexArray = GL.GenVertexArray(); + vertexBuffer = GL.GenBuffer(); + GL.BindVertexArray(vertexArray); + GL.BindBuffer(BufferTarget.ArrayBuffer, vertexBuffer); + for (int i = 0; i < 6; i++) + GL.EnableVertexAttribArray(i); + GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, MeshStore.VertexSize, 0); + GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, MeshStore.VertexSize, 12); + GL.VertexAttribPointer(2, 4, VertexAttribPointerType.UnsignedByte, true, MeshStore.VertexSize, 20); + GL.VertexAttribPointer(3, 4, VertexAttribPointerType.UnsignedByte, true, MeshStore.VertexSize, 24); + GL.VertexAttribPointer(4, 3, VertexAttribPointerType.Byte, false, MeshStore.VertexSize, 28); + GL.VertexAttribPointer(5, 3, VertexAttribPointerType.Byte, false, MeshStore.VertexSize, 32); + GL.BindVertexArray(0); + return vertexArray; + } + + void Init() + { + shader = GraphicsUtil.GetShaderProgram("Resources/Shaders/LevelModel.vert.glsl", "Resources/Shaders/LevelModel.frag.glsl"); + uniform_viewProjection = GL.GetUniformLocation(shader, "viewProjection"); + uniform_model = GL.GetUniformLocation(shader, "model"); + uniform_viewRotation = GL.GetUniformLocation(shader, "viewRotation"); + uniform_tex = GL.GetUniformLocation(shader, "tex"); + uniform_useTexture = GL.GetUniformLocation(shader, "useTexture"); + uniform_decal = GL.GetUniformLocation(shader, "decal"); + uniform_alphaCutoff = GL.GetUniformLocation(shader, "alphaCutoff"); + uniform_opaque = GL.GetUniformLocation(shader, "opaque"); + uniform_maxY = GL.GetUniformLocation(shader, "maxY"); + uniform_uvTransform = GL.GetUniformLocation(shader, "uvTransform"); + uniform_recolorRed = GL.GetUniformLocation(shader, "recolorRed"); + uniform_texGen = GL.GetUniformLocation(shader, "texGen"); + uniform_fogEnabled = GL.GetUniformLocation(shader, "fogEnabled"); + uniform_fogFactors = GL.GetUniformLocation(shader, "fogFactors"); + uniform_fogPlanes = GL.GetUniformLocation(shader, "fogPlanes"); + uniform_fogColor = GL.GetUniformLocation(shader, "fogColor"); + vao = CreateVertexArray(out buffer); + dynamicVao = CreateVertexArray(out dynamicBuffer); + frameVao = CreateVertexArray(out frameBuffer); + + skyShader = GraphicsUtil.GetShaderProgram("Resources/Shaders/Skybox.vert.glsl", "Resources/Shaders/Skybox.frag.glsl"); + uniform_sky_useTexture = GL.GetUniformLocation(skyShader, "useTexture"); + uniform_sky_color = GL.GetUniformLocation(skyShader, "color"); + uniform_sky_windowOrigin = GL.GetUniformLocation(skyShader, "windowOrigin"); + skyVao = GL.GenVertexArray(); + } + + void UploadSkybox(Texture sky) + { + if (skyTexture != 0) + GL.DeleteTexture(skyTexture); + skyTexture = 0; + if (sky == null) + return; + skyTexture = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2D, skyTexture); + GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba8, sky.width, sky.height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, sky.rgba); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); + GL.BindTexture(TextureTarget.Texture2D, 0); + } + + void DrawSky(MapGraphics graphics, LevelModel model) + { + if (skyTexture == 0 && !model.backgroundColor.HasValue) + return; + var forward = -graphics.BillboardMatrix.Row2.Xyz; + double yaw = (Math.Atan2(forward.X, forward.Z) * 180 / Math.PI + 360) % 360; + double pitch = Math.Atan2(forward.Y, Math.Sqrt(forward.X * forward.X + forward.Z * forward.Z)) * 180 / Math.PI; + double x = Math.Round(320 * yaw / 90) % 1280; + double y = Math.Min(960, Math.Max(240, Math.Round(4 * pitch) + 600)); + + GL.UseProgram(skyShader); + GL.Uniform1(uniform_sky_useTexture, skyTexture != 0 ? 1 : 0); + var color = model.backgroundColor ?? Vector3.Zero; + GL.Uniform3(uniform_sky_color, ref color); + GL.Uniform2(uniform_sky_windowOrigin, (float)(1280 - x), (float)y); + GL.ActiveTexture(TextureUnit.Texture0); + GL.BindTexture(TextureTarget.Texture2D, skyTexture); + GL.Disable(EnableCap.DepthTest); + GL.DepthMask(false); + GL.Disable(EnableCap.Blend); + GL.BindVertexArray(skyVao); + GL.DrawArrays(PrimitiveType.Triangles, 0, 3); + GL.BindVertexArray(0); + GL.DepthMask(true); + } + + static TextureWrapMode WrapMode(int wrap) => wrap switch + { + 1 => TextureWrapMode.MirroredRepeat, + 2 => TextureWrapMode.ClampToEdge, + _ => TextureWrapMode.Repeat, + }; + + void Upload(LevelModel model) + { + if (model != uploaded) + { + if (textures.Count > 0) + GL.DeleteTextures(textures.Count, textures.ToArray()); + textures.Clear(); + uploadedLength = 0; + UploadSkybox(model.skybox); + uploaded = model; + } + + var modelTextures = model.textures.textures; + for (int i = textures.Count; i < modelTextures.Count; i++) + { + var t = modelTextures[i]; + int texture = GL.GenTexture(); + GL.BindTexture(TextureTarget.Texture2D, texture); + GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba8, t.width, t.height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, t.rgba); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)WrapMode(t.wrapS)); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)WrapMode(t.wrapT)); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear); + GL.GenerateMipmap(GenerateMipmapTarget.Texture2D); + textures.Add(texture); + } + + GL.BindTexture(TextureTarget.Texture2D, 0); + + var store = model.store; + if (store.length > uploadedLength) + { + GL.BindBuffer(BufferTarget.ArrayBuffer, buffer); + if (store.length > bufferCapacity || uploadedLength == 0) + { + bufferCapacity = store.data.Length; + GL.BufferData(BufferTarget.ArrayBuffer, bufferCapacity, store.data, BufferUsageHint.StaticDraw); + } + else + GL.BufferSubData(BufferTarget.ArrayBuffer, (IntPtr)uploadedLength, store.length - uploadedLength, ref store.data[uploadedLength]); + + uploadedLength = store.length; + } + } + + int UploadDynamic(IReadOnlyList shadows, IReadOnlyList<(int y, int xMin, int xMax, int zMin, int zMax)> waters) + { + dynamicLength = 0; + if (shadows != null) + foreach (var s in shadows) + { + var c = s.position + new Vector3(0, 4, 0); + uint color = (uint)(255 * Math.Min(1, s.opacity)) << 24; + float r = s.size / 2; + if (s.square) + { + Quad(c.X - r, c.X + r, c.Z - r, c.Z + r, c.Y, color); + continue; + } + + for (int i = 0; i < ShadowSegments; i++) + { + DynamicVertex(c.X, c.Y, c.Z, color); + DynamicVertex(c.X + r * CircleSin[i], c.Y, c.Z + r * CircleCos[i], color); + DynamicVertex(c.X + r * CircleSin[i + 1], c.Y, c.Z + r * CircleCos[i + 1], color); + } + } + + if (waters != null) + foreach (var w in waters) + Quad(w.xMin, w.xMax, w.zMin, w.zMax, w.y, WaterColor); + + if (dynamicLength > 0) + { + GL.BindBuffer(BufferTarget.ArrayBuffer, dynamicBuffer); + GL.BufferData(BufferTarget.ArrayBuffer, dynamicLength, dynamicVertices, BufferUsageHint.StreamDraw); + } + + return dynamicLength / MeshStore.VertexSize; + } + + static readonly float[] CircleSin = new float[ShadowSegments + 1], CircleCos = new float[ShadowSegments + 1]; + + static LevelModelRenderer() + { + for (int i = 0; i <= ShadowSegments; i++) + { + CircleSin[i] = (float)Math.Sin(2 * Math.PI * i / ShadowSegments); + CircleCos[i] = (float)Math.Cos(2 * Math.PI * i / ShadowSegments); + } + } + + void Quad(float xMin, float xMax, float zMin, float zMax, float y, uint color) + { + DynamicVertex(xMin, y, zMin, color); + DynamicVertex(xMax, y, zMin, color); + DynamicVertex(xMax, y, zMax, color); + DynamicVertex(xMin, y, zMin, color); + DynamicVertex(xMax, y, zMax, color); + DynamicVertex(xMin, y, zMax, color); + } + + void DynamicVertex(float x, float y, float z, uint color) + { + if (dynamicLength + MeshStore.VertexSize > dynamicVertices.Length) + Array.Resize(ref dynamicVertices, dynamicVertices.Length * 2); + var span = dynamicVertices.AsSpan(dynamicLength, MeshStore.VertexSize); + BitConverter.TryWriteBytes(span, x); + BitConverter.TryWriteBytes(span.Slice(4), y); + BitConverter.TryWriteBytes(span.Slice(8), z); + BitConverter.TryWriteBytes(span.Slice(12), 0f); + BitConverter.TryWriteBytes(span.Slice(16), 0f); + BitConverter.TryWriteBytes(span.Slice(20), color); + span.Slice(24).Clear(); + dynamicLength += MeshStore.VertexSize; + } + + static float Determinant3(in Matrix4 m) => new Matrix3(m).Determinant; + + FrontFaceDirection frontFace; + bool viewMirrors; + + void SetModel(in Matrix4 model) + { + var m = model; + GL.UniformMatrix4(uniform_model, false, ref m); + var face = (Determinant3(model) < 0) != viewMirrors ? FrontFaceDirection.Cw : FrontFaceDirection.Ccw; + if (face != frontFace) + GL.FrontFace(frontFace = face); + } + + int stateTransparent, stateCull, stateDecal, stateTexture, stateCutoffLayer, stateUvAnimation, stateTexGen; + ulong stateFog; + bool fogShown; + LevelModel drawing; + + void ResetState() => stateTransparent = stateCull = stateDecal = stateTexture = stateCutoffLayer = stateUvAnimation = stateTexGen = int.MinValue; + + void ResetFogState() => stateFog = ulong.MaxValue; + + void DrawBatch(Batch batch, bool cull) + { + int transparent = batch.layer >= LAYER_TRANSPARENT ? 1 : 0; + if (transparent != stateTransparent) + { + if (transparent == 1) + GL.Enable(EnableCap.Blend); + else + GL.Disable(EnableCap.Blend); + GL.DepthMask(transparent == 0); + GL.Uniform1(uniform_opaque, 1 - transparent); + stateTransparent = transparent; + } + + int cullMode = cull ? (int)batch.cull : (int)CullMode.None; + if (cullMode != stateCull) + { + if (cullMode != (int)CullMode.None) + { + GL.Enable(EnableCap.CullFace); + GL.CullFace(cullMode == (int)CullMode.Back ? TriangleFace.Back : TriangleFace.Front); + } + else + GL.Disable(EnableCap.CullFace); + stateCull = cullMode; + } + + int decal = batch.decal ? 1 : 0; + if (decal != stateDecal) + { + float offset = decal == 1 ? 1 : 2; + GL.PolygonOffset(offset, offset); + GL.Uniform1(uniform_decal, decal); + stateDecal = decal; + } + + int cutoffLayer = batch.layer == LAYER_ALPHA ? 1 : transparent == 1 ? 2 : 0; + if (cutoffLayer != stateCutoffLayer) + { + GL.Uniform1(uniform_alphaCutoff, cutoffLayer == 1 ? 0.5f : cutoffLayer == 2 ? 1 / 255f : 0); + stateCutoffLayer = cutoffLayer; + } + + int texture = batch.texture >= 0 && batch.texture < textures.Count ? textures[batch.texture] : 0; + if (texture != stateTexture) + { + GL.Uniform1(uniform_useTexture, texture != 0 ? 1 : 0); + GL.BindTexture(TextureTarget.Texture2D, texture); + stateTexture = texture; + } + + if (batch.uvAnimation != stateUvAnimation) + { + var uvTransform = batch.uvAnimation >= 0 && batch.uvAnimation < drawing.uvAnimations.Count + ? drawing.uvAnimations[batch.uvAnimation].transform + : Matrix3.Identity; + GL.UniformMatrix3(uniform_uvTransform, false, ref uvTransform); + stateUvAnimation = batch.uvAnimation; + } + + ulong fog = fogShown ? batch.fog : 0; + if (fog != stateFog) + { + GL.Uniform1(uniform_fogEnabled, fog != 0 ? 1 : 0); + if (fog != 0) + { + uint color = (uint)(fog >> 32); + GL.Uniform3(uniform_fogColor, (color >> 24) / 255f, ((color >> 16) & 0xFF) / 255f, ((color >> 8) & 0xFF) / 255f); + GL.Uniform2(uniform_fogFactors, (float)(short)(fog >> 16), (float)(short)fog); + } + + stateFog = fog; + } + + if (batch.texGen != stateTexGen) + { + GL.Uniform1(uniform_texGen, batch.texGen); + stateTexGen = batch.texGen; + } + + GL.DrawArrays(PrimitiveType.Triangles, batch.first, batch.count); + } + + static int Phase(Batch batch) => batch.layer >= LAYER_TRANSPARENT ? 2 : batch.layer == LAYER_ALPHA ? 1 : 0; + + static readonly Vector3[] NoOffset = { Vector3.Zero }; + + const float MARIO_HEIGHT = 160; + + static float MarioScale(MapGraphics graphics, Vector3 position, float minPixels) + { + if (minPixels <= 0 || graphics.viewMode != MapGraphics.ViewMode.ThreeDimensional) + return 1; + float distance = (position - graphics.currentView.position).Length; + float pixels = MARIO_HEIGHT * graphics.glControl.Height / (2 * (float)Math.Tan(0.5) * Math.Max(distance, 1)); + return Math.Max(1, minPixels / pixels); + } + + void DrawInstances(List instances, int phase, bool cull, in Matrix4 billboardRotation, Vector3 offset = default, + bool recolorCap = false, int skipStart = -1, int skipCount = 0, Vector3 scaleCenter = default, float scale = 1) + { + var translation = Matrix4.CreateTranslation(offset); + if (scale != 1) + translation *= Matrix4.CreateTranslation(-scaleCenter) * Matrix4.CreateScale(scale) * Matrix4.CreateTranslation(scaleCenter); + for (int index = 0; index < instances.Count; index++) + { + if (index >= skipStart && index < skipStart + skipCount) + continue; + var instance = instances[index]; + bool modelSet = false; + foreach (var batch in instance.mesh.batches) + { + if (Phase(batch) != phase) + continue; + if (!modelSet) + { + GL.Uniform1(uniform_recolorRed, recolorCap && instance.cap ? 1 : 0); + GL.BindVertexArray(instance.frame ? frameVao : vao); + SetModel((instance.billboard + ? instance.matrix * Matrix4.CreateScale(instance.anchorScale) * billboardRotation * Matrix4.CreateTranslation(instance.anchor) + : instance.matrix) * translation); + modelSet = true; + } + + DrawBatch(batch, cull); + } + + if (instance.frame && modelSet) + GL.BindVertexArray(vao); + } + } + + public void Draw(MapGraphics graphics, LevelModel model, float maxY, bool cull, bool drawObjects, + IReadOnlyList<(int y, int xMin, int xMax, int zMin, int zMax)> waters, bool drawSky, bool drawFog = false, + IReadOnlyList marioCopies = null, IReadOnlyList levelCopies = null, float marioMinPixels = 0) + { + if (shader == 0) + Init(); + Upload(model); + int dynamicVertexCount = UploadDynamic(drawObjects ? model.shadows : null, waters); + if (model.frameStore.length > 0) + { + GL.BindBuffer(BufferTarget.ArrayBuffer, frameBuffer); + GL.BufferData(BufferTarget.ArrayBuffer, model.frameStore.length, model.frameStore.data, BufferUsageHint.StreamDraw); + } + + bool blendWasEnabled = GL.IsEnabled(EnableCap.Blend); + if (drawSky) + DrawSky(graphics, model); + + GL.UseProgram(shader); + GL.Uniform1(uniform_recolorRed, 0); + ResetState(); + ResetFogState(); + drawing = model; + fogShown = drawFog; + var planes = model.perspectivePlanes; + GL.Uniform2(uniform_fogPlanes, ref planes); + GL.Uniform3(uniform_fogColor, 0f, 0f, 0f); + var viewProjection = graphics.ViewMatrix; + GL.UniformMatrix4(uniform_viewProjection, false, ref viewProjection); + var viewRotation = Matrix3.Transpose(new Matrix3(graphics.BillboardMatrix)); + GL.UniformMatrix3(uniform_viewRotation, false, ref viewRotation); + GL.Uniform1(uniform_tex, 0); + GL.Uniform1(uniform_maxY, maxY); + GL.ActiveTexture(TextureUnit.Texture0); + + GL.Enable(EnableCap.DepthTest); + GL.DepthFunc(DepthFunction.Lequal); + viewMirrors = Determinant3(graphics.ViewMatrix) > 0; + frontFace = FrontFaceDirection.Ccw; + GL.FrontFace(frontFace); + GL.Enable(EnableCap.PolygonOffsetFill); + + var billboardRotation = graphics.BillboardMatrix; + billboardRotation.Row3 = Vector4.UnitW; + + GL.BindVertexArray(vao); + for (int phase = 0; phase < 3; phase++) + { + foreach (var levelOffset in levelCopies ?? NoOffset) + { + SetModel(Matrix4.CreateTranslation(levelOffset)); + foreach (var batch in model.level.batches) + if (Phase(batch) == phase) + DrawBatch(batch, cull); + + DrawInstances(model.levelInstances, phase, cull, billboardRotation, levelOffset); + } + + if (drawObjects) + { + int marioCount = model.marioInstancesStart >= 0 ? model.marioInstances.Count : 0; + DrawInstances(model.instances, phase, cull, billboardRotation, skipStart: model.marioInstancesStart, skipCount: marioCount); + if (marioCount > 0) + DrawInstances(model.marioInstances, phase, cull, billboardRotation, + scaleCenter: model.marioModelPosition, scale: MarioScale(graphics, model.marioModelPosition, marioMinPixels)); + if (marioCopies != null) + foreach (var position in marioCopies) + DrawInstances(model.marioInstances, phase, cull, billboardRotation, position - model.marioModelPosition, true, + scaleCenter: position, scale: MarioScale(graphics, position, marioMinPixels)); + GL.Uniform1(uniform_recolorRed, 0); + } + } + + if (dynamicVertexCount > 0) + { + GL.BindVertexArray(dynamicVao); + SetModel(Matrix4.Identity); + DrawBatch(new Batch { texture = -1, layer = LAYER_TRANSPARENT, decal = true, first = 0, count = dynamicVertexCount }, false); + } + + GL.BindTexture(TextureTarget.Texture2D, 0); + GL.BindVertexArray(0); + GL.DepthMask(true); + GL.Disable(EnableCap.CullFace); + GL.FrontFace(FrontFaceDirection.Ccw); + GL.Disable(EnableCap.PolygonOffsetFill); + GL.PolygonOffset(0, 0); + if (blendWasEnabled) + GL.Enable(EnableCap.Blend); + else + GL.Disable(EnableCap.Blend); + } + } +} diff --git a/STROOP/Tabs/MapTab/Renderers/LineRenderer.cs b/STROOP/Tabs/MapTab/Renderers/LineRenderer.cs index 8bd1348dc..e8950cb11 100644 --- a/STROOP/Tabs/MapTab/Renderers/LineRenderer.cs +++ b/STROOP/Tabs/MapTab/Renderers/LineRenderer.cs @@ -1,4 +1,5 @@ -using OpenTK; +using System; +using OpenTK; using OpenTK.Graphics.OpenGL; using OpenTK.Mathematics; using STROOP.Core; @@ -70,6 +71,25 @@ public void AddArrow(float x, float y, float z, float size, float yaw, float _ar Add(new Vector3(arrowHeadX, y, arrowHeadZ), new Vector3(pointSide2X, y, pointSide2Z), color, thickness); } + public void AddArrow3D(Vector3 from, Vector3 to, Vector4 color, float thickness, float headLength, Vector3 cameraPosition) + { + Add(from, to, color, thickness); + + Vector3 shaft = to - from; + if (shaft.LengthSquared < 1e-8f) + return; + Vector3 dir = shaft.Normalized(); + Vector3 side = Vector3.Cross(dir, cameraPosition - to); + if (side.LengthSquared < 1e-8f) + side = Vector3.Cross(dir, Math.Abs(dir.Y) < 0.99f ? Vector3.UnitY : Vector3.UnitX); + side.Normalize(); + + float component = headLength * (float)Math.Sqrt(0.5); + Vector3 headBase = to - dir * component; + Add(to, headBase + side * component, color, thickness); + Add(to, headBase - side * component, color, thickness); + } + public override void SetDrawCalls(MapGraphics graphics) { instances.Clear(); diff --git a/STROOP/Tabs/MapTab/Renderers/TriangleRenderer.cs b/STROOP/Tabs/MapTab/Renderers/TriangleRenderer.cs index 98fcd2ae7..0e9b1493d 100644 --- a/STROOP/Tabs/MapTab/Renderers/TriangleRenderer.cs +++ b/STROOP/Tabs/MapTab/Renderers/TriangleRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using OpenTK.Graphics.OpenGL; using OpenTK; using System.Runtime.InteropServices; @@ -24,7 +24,7 @@ public TransparentTriangleRenderer(TriangleRenderer parent) : base() public void DrawMask(TransparencyRenderer renderer) { - if (triangles.Count == 0) + if (vertices.Count == 0) return; GL.UseProgram(shader); renderer.SetUniforms(shader); @@ -33,19 +33,20 @@ public void DrawMask(TransparencyRenderer renderer) public void DrawTransparent(TransparencyRenderer renderer) { - if (triangles.Count == 0) + if (vertices.Count == 0) return; DrawTriangles(renderer.graphics, parent.shader); } public void Prepare(TransparencyRenderer renderer) { - if (triangles.Count == 0) + if (vertices.Count == 0) return; WriteDataToBuffer(); } } + [StructLayout(LayoutKind.Sequential)] struct TriangleVertex { public const int Size = sizeof(float) * 4 + sizeof(float) * 4 + sizeof(float) * 4 + sizeof(float) * 3; @@ -56,23 +57,13 @@ struct TriangleVertex internal Vector3 outlineThickness; } - struct Triangle - { - public Vector3[] positions; - public Vector4[] colors; - public bool showUnitSquares; - public Vector4 outlineColor; - public Vector3 outlineThickness; - } - int shader; int buffer; int vao; int bufferSize = 0; - IntPtr dataPtr; - List triangles = new List(); + readonly List vertices = new List(); int uniform_viewProjection, uniform_pixelsPerUnit, uniform_unitShift; @@ -124,16 +115,16 @@ void InitInternal(int maxExpectedTriangles) uniform_unitShift = GL.GetUniformLocation(shader, "unitShift"); }); - dataPtr = Marshal.AllocHGlobal((IntPtr)(bufferSize = expectedSize)); + bufferSize = expectedSize; } public override void SetDrawCalls(MapGraphics graphics) { - triangles.Clear(); - transparentRenderer.triangles.Clear(); + vertices.Clear(); + transparentRenderer.vertices.Clear(); graphics.drawLayers[(int)drawlayer].Add(() => { - if (triangles.Count == 0) + if (vertices.Count == 0) return; WriteDataToBuffer(); @@ -150,7 +141,6 @@ public override void SetDrawCalls(MapGraphics graphics) protected void DrawTriangles(MapGraphics graphics, int program) { Vector2 pixelsPerUnit = graphics.pixelsPerUnit; - var error = GL.GetError(); GL.UseProgram(program); GL.BindVertexArray(vao); var mat = graphics.ViewMatrix; @@ -161,8 +151,7 @@ protected void DrawTriangles(MapGraphics graphics, int program) GL.Uniform2(GL.GetUniformLocation(program, "gridOffset"), new Vector2(0)); GL.Disable(EnableCap.CullFace); - GL.DrawArrays(PrimitiveType.Triangles, 0, triangles.Count * 3); - error = GL.GetError(); + GL.DrawArrays(PrimitiveType.Triangles, 0, vertices.Count); GL.BindVertexArray(0); } @@ -174,45 +163,24 @@ public void Add(Vector3 v1, Vector3 v2, Vector3 v3, bool showTriUnits, Vector4 c public void Add(Vector3 v1, Vector3 v2, Vector3 v3, bool showTriUnits, Vector4 color1, Vector4 color2, Vector4 color3, Vector4 outlineColor, Vector3 outlineThickness, bool transparent) { - (transparent ? transparentRenderer.triangles : triangles).Add(new Triangle - { - positions = new[] { v1, v2, v3 }, - colors = new[] { color1, color2, color3 }, - outlineColor = outlineColor, - showUnitSquares = showTriUnits, - outlineThickness = outlineThickness - }); + var target = transparent ? transparentRenderer.vertices : vertices; + float showUnitSquares = showTriUnits ? 1.0f : 0.0f; + target.Add(new TriangleVertex { position = v1, color = color1, outlineColor = outlineColor, showUnitSquares = showUnitSquares, outlineThickness = outlineThickness }); + target.Add(new TriangleVertex { position = v2, color = color2, outlineColor = outlineColor, showUnitSquares = showUnitSquares, outlineThickness = outlineThickness }); + target.Add(new TriangleVertex { position = v3, color = color3, outlineColor = outlineColor, showUnitSquares = showUnitSquares, outlineThickness = outlineThickness }); } void WriteDataToBuffer() { - var dataSize = triangles.Count * TriangleVertex.Size * 3; + var dataSize = vertices.Count * TriangleVertex.Size; GL.BindBuffer(BufferTarget.ArrayBuffer, buffer); if (dataSize > bufferSize) { - Marshal.FreeHGlobal(dataPtr); - dataPtr = Marshal.AllocHGlobal((IntPtr)(bufferSize = Math.Max(dataSize, bufferSize * 2))); + bufferSize = Math.Max(dataSize, bufferSize * 2); GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)bufferSize, IntPtr.Zero, BufferUsageHint.DynamicDraw); } - IntPtr ptr = dataPtr; - foreach (var instance in triangles) - { - for (int i = 0; i < 3; i++) - { - Marshal.StructureToPtr(new TriangleVertex - { - position = instance.positions[i], - color = instance.colors[i], - outlineColor = instance.outlineColor, - showUnitSquares = instance.showUnitSquares ? 1.0f : 0.0f, - outlineThickness = instance.outlineThickness - }, ptr, false); - ptr = IntPtr.Add(ptr, TriangleVertex.Size); - } - } - - GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, (IntPtr)(dataSize), dataPtr); + GL.BufferSubData(BufferTarget.ArrayBuffer, IntPtr.Zero, dataSize, ref CollectionsMarshal.AsSpan(vertices)[0]); } } } diff --git a/STROOP/Tabs/MapTab/Views/View3D.cs b/STROOP/Tabs/MapTab/Views/View3D.cs index 7a5f29192..164320310 100644 --- a/STROOP/Tabs/MapTab/Views/View3D.cs +++ b/STROOP/Tabs/MapTab/Views/View3D.cs @@ -13,11 +13,13 @@ public enum Camera3DMode } public Camera3DMode camera3DMode = Camera3DMode.FocusOnPositionAngle; - public float camera3DDistanceController = 50; - public bool display3DLevelGeometry = true; + public float camera3DDistanceController = 75; + public bool display3DLevelGeometry = false; public PositionAngle focusPositionAngle { get; set; } = PositionAngle.Mario; + public View3D() => pitch = 0.6f; + void PivotingView.Pivot(PositionAngle pivotPoint) { focusPositionAngle = pivotPoint; diff --git a/STROOP/Utilities/DecompHackDetector.cs b/STROOP/Utilities/DecompHackDetector.cs new file mode 100644 index 000000000..194ec43d1 --- /dev/null +++ b/STROOP/Utilities/DecompHackDetector.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using STROOP.Core; +using STROOP.Structs; +using STROOP.Structs.Configurations; +using STROOP.Tabs.MapTab.DataUtil; +using STROOP.Variables.SM64MemoryLayout; + +namespace STROOP.Utilities +{ + public static class DecompHackDetector + { + const uint OBJECT_SIZE = 0x260, OBJECT_LIST_SIZE = 0x68; + const int OBJECT_LIST_COUNT = 13, MAX_OBJECT_SLOTS = 240; + const uint MARIO_STATE_SIZE = 0xC8; + const uint G_MARIO_STATE_US = 0x8032D93C, SEGMENT_TABLE_US = 0x8033B400; + + static bool SameStride(uint a, uint b) => ((long)a - b) % OBJECT_SIZE == 0; + + static readonly Stopwatch sinceScan = Stopwatch.StartNew(); + static readonly TimeSpan ScanInterval = TimeSpan.FromSeconds(1.5); + static Rdram scanRam; + + const int SYMBOL_COUNT = 7; + + static bool complete, vanillaLayout; + + public static void Update() + { + if (Config.Stream == null || MappingConfig.HasUserMapping) + return; + if (RomVersionConfig.IsKnownBuild() + || RomVersionConfig.Version != RomVersion.US && RomVersionConfig.Version != RomVersion.JP) + { + MappingConfig.ClearAutoMapping(); + complete = vanillaLayout = false; + return; + } + + if ((MappingConfig.IsAutoMapping && complete || vanillaLayout) && MarioStateOk(StreamReader, MarioConfig.StructAddress)) + return; + if (sinceScan.Elapsed < ScanInterval) + return; + sinceScan.Restart(); + + scanRam ??= new Rdram(new byte[Config.RamSize]); + Config.Stream.CopyCachedRam(scanRam.data); + if (scanRam.data.AsSpan().IndexOfAnyExcept((byte)0) < 0) + return; + var found = Detect(scanRam); + if (found == null) + return; + vanillaLayout = found.All(entry => entry.Key == entry.Value); + if (vanillaLayout) + { + MappingConfig.ClearAutoMapping(); + return; + } + + MappingConfig.SetAutoMapping(found); + complete = found.Count == SYMBOL_COUNT; + } + + static readonly Func StreamReader = a => Config.Stream.GetUInt32(a); + + public static Dictionary Detect(Rdram ram) + { + uint U32(uint a) => ram.U32(a); + uint marioState = FindMarioState(ram); + if (marioState == 0) + return null; + uint marioObject = U32(marioState + 0x88), area = U32(marioState + 0x90); + var result = new Dictionary { [MarioConfig.StructAddressUS] = marioState }; + + uint listArray = FindObjectListArray(ram, marioObject); + uint pool = listArray != 0 ? FindObjectPool(ram, listArray) : 0; + if (listArray != 0) + result[ObjectSlotsConfig.ProcessGroupsStartAddressUS] = listArray; + if (pool != 0) + result[ObjectSlotsConfig.ObjectSlotsStartAddressUS] = pool; + + var inside = new List<(uint start, uint end)> { (marioState, marioState + MARIO_STATE_SIZE) }; + if (listArray != 0) + inside.Add((listArray, listArray + OBJECT_LIST_COUNT * OBJECT_LIST_SIZE)); + if (pool != 0) + inside.Add((pool, pool + MAX_OBJECT_SLOTS * OBJECT_SIZE)); + uint anchor = listArray != 0 ? listArray : marioState; + + var references = FindReferences(ram, new[] { marioState, marioObject, area }, inside); + void Pick(uint value, uint vanilla) + { + if (!references.TryGetValue(value, out var addresses) || addresses.Count == 0) + return; + result[vanilla] = addresses.OrderBy(a => Math.Abs((long)a - anchor)).First(); + } + + Pick(marioState, G_MARIO_STATE_US); + Pick(marioObject, MarioObjectConfig.PointerAddressUS); + Pick(area, AreaConfig.CurrentAreaPointerAddressUS); + + uint segmentTable = FindSegmentTable(ram, area); + if (segmentTable != 0) + result[Config.ObjectAssociations?.SegmentTableUS ?? SEGMENT_TABLE_US] = segmentTable; + return result; + } + + #region MarioState + + static bool MarioStateOk(Func u32, uint m) + { + float F32(uint a) => BitConverter.Int32BitsToSingle((int)u32(a)); + if (!Rdram.IsPointer(m) || !Rdram.IsPointer(m + MARIO_STATE_SIZE)) + return false; + uint obj = u32(m + 0x88), area = u32(m + 0x90); + if (!Rdram.IsPointer(obj) || !Rdram.IsPointer(area) || u32(m + 0x0C) == 0) + return false; + short health = (short)u32(m + 0xAC); + if (health < 0 || health > 0x880) + return false; + uint objType = u32(obj) >> 16; + if (objType != 0x018 && objType != 9) + return false; + bool any = false; + for (uint i = 0; i < 3; i++) + { + float p = F32(m + 0x3C + 4 * i); + if (p != 0 && (!float.IsNormal(p) || Math.Abs(p) > 1e6f) || p != F32(obj + 0xA0 + 4 * i)) + return false; + any |= p != 0; + } + + return any; + } + + static uint FindMarioState(Rdram ram) + { + uint Reader(uint a) => ram.U32(a); + for (uint p = 0x88; p + 4 <= ram.data.Length; p += 4) + { + uint value = ram.U32Phys(p); + if (!Rdram.IsPointer(value) || (value & 3) != 0) + continue; + uint objType = ram.U16(value); + if (objType != 0x018 && objType != 9) + continue; + uint m = 0x80000000 | (p - 0x88); + if (MarioStateOk(Reader, m)) + return m; + } + + return 0; + } + + #endregion + + #region objects + + static uint FindObjectListArray(Rdram ram, uint marioObject) + { + uint n = ram.U32(marioObject + 0x60); + for (int i = 0; i < 1024 && Rdram.IsPointer(n); i++) + { + if (!SameStride(n, marioObject)) + return CountClosedLists(ram, n) >= OBJECT_LIST_COUNT - 1 ? n : 0; + n = ram.U32(n + 0x60); + } + + return 0; + } + + static IEnumerable ListMembers(Rdram ram, uint head) + { + uint n = ram.U32(head + 0x60); + for (int i = 0; i < 1024 && Rdram.IsPointer(n) && n != head; i++) + { + yield return n; + n = ram.U32(n + 0x60); + } + } + + static bool IsClosed(Rdram ram, uint head) + { + uint n = ram.U32(head + 0x60); + for (int i = 0; i < 1024 && Rdram.IsPointer(n); i++) + { + if (n == head) + return true; + n = ram.U32(n + 0x60); + } + + return false; + } + + static int CountClosedLists(Rdram ram, uint listArray) + { + int closed = 0; + for (uint i = 0; i < OBJECT_LIST_COUNT; i++) + if (IsClosed(ram, listArray + i * OBJECT_LIST_SIZE)) + closed++; + return closed; + } + + static uint FindObjectPool(Rdram ram, uint listArray) + { + var objects = new List(); + for (uint i = 0; i < OBJECT_LIST_COUNT; i++) + { + uint head = listArray + i * OBJECT_LIST_SIZE; + if (IsClosed(ram, head)) + objects.AddRange(ListMembers(ram, head)); + } + + if (objects.Count == 0) + return 0; + uint start = objects.Min(), end = objects.Max() + OBJECT_SIZE; + if (objects.Any(o => !SameStride(o, start))) + return 0; + + bool InPool(uint p) => p == 0 || (Rdram.IsPointer(p) && SameStride(p, start) + && Math.Abs((long)p - start) < (long)OBJECT_SIZE * MAX_OBJECT_SLOTS); + while ((end - (start - OBJECT_SIZE)) / OBJECT_SIZE <= MAX_OBJECT_SLOTS) + { + uint slot = start - OBJECT_SIZE; + uint next = ram.U32(slot + 0x60), prev = ram.U32(slot + 0x64); + if (next == 0 && prev == 0 || !InPool(next) || !InPool(prev)) + break; + start = slot; + } + + return start; + } + + #endregion + + static Dictionary> FindReferences(Rdram ram, uint[] values, List<(uint start, uint end)> exclude) + { + var found = values.Distinct().ToDictionary(v => v, _ => new List()); + for (uint p = 0; p + 4 <= ram.data.Length; p += 4) + { + uint value = ram.U32Phys(p); + if (!Rdram.IsPointer(value) || !found.TryGetValue(value, out var list)) + continue; + uint address = 0x80000000 | p; + if (!exclude.Any(r => address >= r.start && address < r.end)) + list.Add(address); + } + + return found; + } + + #region segment table + + static bool PlausibleOpcode(uint op) => + op >= 0xB1 && op <= 0xBF || op >= 0xD7 && op <= 0xFF || op <= 0x07; + + static bool LooksLikeDisplayList(Rdram ram, uint physical) + { + for (uint i = 0; i < 1024; i++, physical += 8) + { + if (!ram.Ok(physical, 8)) + return false; + uint w0 = ram.U32Phys(physical), op = w0 >> 24; + if (w0 == 0xB8000000 || w0 == 0xDF000000) + return i > 0; + if (!PlausibleOpcode(op) || w0 == 0 && ram.U32Phys(physical + 4) == 0) + return false; + } + + return false; + } + + static List SampleDisplayLists(Rdram ram, uint area) + { + var samples = new List(); + uint root = ram.U32(area + 4); + var stack = new Stack(); + var seen = new HashSet(); + stack.Push(root); + while (stack.Count > 0 && seen.Count < 4096 && samples.Count < 24) + { + uint n = stack.Pop(); + if (!Rdram.IsPointer(n) || !seen.Add(n)) + continue; + uint field = ram.U32(n + 0x14); + uint segment = field >> 24; + if (segment >= 0x01 && segment <= 0x0F && (field & 7) == 0 && !samples.Contains(field)) + samples.Add(field); + uint first = ram.U32(n + 0x10), c = first; + for (int i = 0; i < 256 && Rdram.IsPointer(c); i++) + { + stack.Push(c); + c = ram.U32(c + 0x08); + if (c == first) + break; + } + } + + return samples; + } + + static uint FindSegmentTable(Rdram ram, uint area) + { + var samples = SampleDisplayLists(ram, area); + if (samples.Count == 0) + return 0; + uint best = 0; + int bestScore = 0; + var entries = new uint[32]; + uint usedSegment = samples.GroupBy(s => s >> 24).OrderByDescending(g => g.Count()).First().Key; + for (uint p = 0; p + 32 * 4 <= ram.data.Length; p += 4) + { + uint used = ram.U32Phys(p + 4 * usedSegment); + if (used == 0 || used >= 0x00800000 || (used & 7) != 0 || ram.U32Phys(p) != 0) + continue; + int nonZero = 0; + bool ok = true; + for (uint i = 0; i < 32 && ok; i++) + { + entries[i] = ram.U32Phys(p + 4 * i); + ok = entries[i] < 0x00800000 && (entries[i] & 7) == 0; + if (entries[i] != 0) + nonZero++; + } + + if (!ok || nonZero < 3) + continue; + int score = 0, needed = Math.Max(bestScore + 1, (samples.Count + 1) / 2); + for (int s = 0; s < samples.Count && score + samples.Count - s >= needed; s++) + { + uint dl = samples[s], baseAddress = entries[(dl >> 24) & 31]; + if (baseAddress != 0 && LooksLikeDisplayList(ram, (baseAddress + (dl & 0xFFFFFF)) & 0x7FFFFF)) + score++; + } + + if (score > bestScore) + { + bestScore = score; + best = 0x80000000 | p; + } + } + + return bestScore * 2 >= samples.Count ? best : 0; + } + + #endregion + } +}