From 3d92312ab93bdb0bf4984c0d2fd1b2cdfa4f0040 Mon Sep 17 00:00:00 2001 From: Bavario Date: Sat, 18 Oct 2025 12:45:47 +0200 Subject: [PATCH 1/4] Add Hack.io.KCL --- Hack.io.KCL/Hack.io.KCL.csproj | 23 + Hack.io.KCL/KCL.cs | 1157 ++++++++++++++++++++++++++++++++ Hack.io.KCL/OpenTK.dll.config | 25 + Hack.io.KCL/packages.config | 4 + 4 files changed, 1209 insertions(+) create mode 100644 Hack.io.KCL/Hack.io.KCL.csproj create mode 100644 Hack.io.KCL/KCL.cs create mode 100644 Hack.io.KCL/OpenTK.dll.config create mode 100644 Hack.io.KCL/packages.config diff --git a/Hack.io.KCL/Hack.io.KCL.csproj b/Hack.io.KCL/Hack.io.KCL.csproj new file mode 100644 index 0000000..bba3d62 --- /dev/null +++ b/Hack.io.KCL/Hack.io.KCL.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + Hack.io.KCL + 2.0.0.0 + SuperHackio + Super Hackio Incorporated + IO Library for KCollision (.kcl) + ©Super Hackio Incorporated 2023-2025 + https://github.com/SuperHackio/Hack.io + https://github.com/SuperHackio/Hack.io + First Release + + + + + + + + diff --git a/Hack.io.KCL/KCL.cs b/Hack.io.KCL/KCL.cs new file mode 100644 index 0000000..24a168c --- /dev/null +++ b/Hack.io.KCL/KCL.cs @@ -0,0 +1,1157 @@ +using Hack.io.Utility; +using static Hack.io.Utility.MathUtil; +using Hack.io.BCSV; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using System.Collections; +using System.Reflection; +using System.ComponentModel; +using System.Globalization; +using System.Numerics; + +namespace Hack.io.KCL +{ + /// + /// Super Mario Galaxy Collision files. Also supports .pa files with Hack.io.BCSV + /// + public class KCL + { + /// + /// Filename of this KCL file. + /// + public string? FileName { get; set; } = null; + public List Positions = new List(); + public List Normals = new List(); + public List Triangles = new List(); + public Octree[] OctreeRoots = null; + public Vector3 MinCoords; + + uint MaskX, MaskY, MaskZ, ShiftX, ShiftY, ShiftZ; + + public KCL() {} + public KCL(string file, BackgroundWorker? bgw = null) + { + FileStream FS = new FileStream(file, FileMode.Open); + Read(FS, bgw); + FS.Close(); + FileName = file; + } + public KCL(WavefrontObj obj, BCSV.BCSV CollisionCodes, int MaxTrianglesPerCube, int MinCubeWidth, BackgroundWorker? bgw = null) + { + bgw?.ReportProgress(0, 0); + if (obj.Count > 0xFFFE) + throw new GeometryOverflowException(obj.Count, 0xFFFE); + + float min_x = 0, min_y = 0, min_z = 0, max_x = 0, max_y = 0, max_z = 0; + Dictionary positionTable = new Dictionary(); + Dictionary normalTable = new Dictionary(); + List faces = new List(); + Dictionary triangledict = new Dictionary(); + for (int i = 0; i < obj.Count; i++) + { + KCLFace face = new KCLFace(); + Triangle triangle = obj[i]; + + Vector3 direction = Vector3.Cross( + triangle[1] - triangle[0], + triangle[2] - triangle[0]); + + if (((direction.X * direction.X) + (direction.Y * direction.Y) + (direction.Z * direction.Z)) < 0.001) + continue; + direction = Vector3.Normalize(direction); + + for (int j = 0; j < 3; j++) + { + min_x = Math.Min(triangle[j].X, min_x); + min_y = Math.Min(triangle[j].Y, min_y); + min_z = Math.Min(triangle[j].Z, min_z); + max_x = Math.Max(triangle[j].X, max_x); + max_y = Math.Max(triangle[j].Y, max_y); + max_z = Math.Max(triangle[j].Z, max_z); + } + + //Calculate the ABC normal values. + Vector3 normalA = Vector3.Cross(direction, triangle[2] - triangle[0]); + Vector3 normalB = (-(Vector3.Cross(direction, triangle[1] - triangle[0]))); + Vector3 normalC = Vector3.Cross(direction, triangle[1] - triangle[2]); + //Normalize the ABC normal values. + normalA = Vector3.Normalize(normalA); + normalB = Vector3.Normalize(normalB); + normalC = Vector3.Normalize(normalC); + + //Vector3 normalA = Vector3.Cross(triangle[0] - triangle[2], triangle[3]).Unit(); + //Vector3 normalB = Vector3.Cross(triangle[1] - triangle[0], triangle[3]).Unit(); + //Vector3 normalC = Vector3.Cross(triangle[2] - triangle[1], triangle[3]).Unit(); + face.Length = Vector3.Dot(triangle[1] - triangle[0], normalC); + face.PositionIndex = (ushort)IndexOfVertex(triangle[0], Positions, positionTable); + face.DirectionIndex = (ushort)IndexOfVertex(triangle[3], Normals, normalTable); + face.NormalAIndex = (ushort)IndexOfVertex(normalA, Normals, normalTable); + face.NormalBIndex = (ushort)IndexOfVertex(normalB, Normals, normalTable); + face.NormalCIndex = (ushort)IndexOfVertex(normalC, Normals, normalTable); + face.GroupIndex = (ushort)triangle.GroupIndex; + Triangles.Add(face); + triangledict.Add((ushort)triangledict.Count, triangle); + + bgw?.ReportProgress((int)GetPercentOf(i, obj.Count), 0); + } + Vector3 Max = new Vector3(max_x, max_y, max_z); + MinCoords = new Vector3(min_x, min_y, min_z); + bgw?.ReportProgress(100, 0); + + if (positionTable.Count >= 0xFFFF) + throw new GeometryOverflowException(positionTable.Count, 0xFFFF, "Verticies"); + if (normalTable.Count >= 0xFFFF) + throw new GeometryOverflowException(normalTable.Count, 0xFFFF, "Normals"); + + //Positions.AddRange(vw.Verticies); + //Normals.AddRange(nw.Verticies); + + //Generate Octree + bgw?.ReportProgress(0, 1); + //GetBounds(out Vector3 minCoordinate, out Vector3 maxCoordinate); + Vector3 size = Max - MinCoords; + + uint ExponentX = (uint)GetNext2Exponent(size.X); + uint ExponentY = (uint)GetNext2Exponent(size.Y); + uint ExponentZ = (uint)GetNext2Exponent(size.Z); + float m = Math.Min(Math.Min(size.X, size.Y), size.Z); + int cubeSizePower = GetNext2Exponent(m); + int mx = GetNext2Exponent(2048); + if (cubeSizePower > mx) + cubeSizePower = GetNext2Exponent(2048); + + int cubeSize = 1 << cubeSizePower; + ShiftX = (uint)cubeSizePower; + ShiftY = (uint)(ExponentX - cubeSizePower); + ShiftZ = (uint)(ExponentX - cubeSizePower + ExponentY - cubeSizePower); + + MaskX = (uint)(0xFFFFFFFF << (int)ExponentX); + MaskY = (uint)(0xFFFFFFFF << (int)ExponentY); + MaskZ = (uint)(0xFFFFFFFF << (int)ExponentZ); + + uint CubeCountX = (uint)Math.Max(1, (1 << (int)ExponentX) / cubeSize), + CubeCountY = (uint)Math.Max(1, (1 << (int)ExponentY) / cubeSize), + CubeCountZ = (uint)Math.Max(1, (1 << (int)ExponentZ) / cubeSize); + + // Generate the root nodes, which are square cubes required to cover all of the model. + OctreeRoots = new Octree[CubeCountX * CubeCountY * CubeCountZ]; + + + int cubeBlow = 2; + int index = 0; + for (int z = 0; z < CubeCountZ; z++) + { + for (int y = 0; y < CubeCountY; y++) + { + for (int x = 0; x < CubeCountX; x++) + { + Vector3 cubePosition = MinCoords + ((float)cubeSize) * new Vector3(x, y, z); + if (index >= OctreeRoots.Length) + { + //Something went REALLY WRONG for this to happen... + throw new Exception($"Octree Failure: count={OctreeRoots.Length} z={CubeCountZ} y={CubeCountY} x={CubeCountX} cube={cubeSize} size={size.ToString()} min={MinCoords.ToString()} max={Max.ToString()}"); + } + OctreeRoots[index++] = new Octree(triangledict, cubePosition, cubeSize, MaxTrianglesPerCube, 1048576, MinCubeWidth, cubeBlow, 10); + bgw?.ReportProgress((int)GetPercentOf(index, OctreeRoots.Length), 1); + } + } + } + bgw?.ReportProgress(100, 1); + } + + public void Save(string file, BackgroundWorker? bgw = null) + { + FileStream FS = new FileStream(file, FileMode.Create); + Write(FS, bgw); + FS.Close(); + FileName = file; + } + + public void Read(Stream KCLFile, BackgroundWorker? bgw = null) + { + int MaxItemsForPogress = 0; + bgw?.ReportProgress(0, 0); + long FileStart = KCLFile.Position; + //Header - There's no magic so we'll just have to hope the user inputted a real KCL file... + int PositionsOffset = KCLFile.ReadInt32(); + int NormalsOffset = KCLFile.ReadInt32(); + int TrianglesOffset = KCLFile.ReadInt32() + 0x10; //Why...? + int OctreeOffset = KCLFile.ReadInt32(); + float Thickness = KCLFile.ReadSingle(); + MinCoords = new Vector3(KCLFile.ReadSingle(), KCLFile.ReadSingle(), KCLFile.ReadSingle()); + MaskX = KCLFile.ReadUInt32(); + MaskY = KCLFile.ReadUInt32(); + MaskZ = KCLFile.ReadUInt32(); + ShiftX = KCLFile.ReadUInt32(); + ShiftY = KCLFile.ReadUInt32(); + ShiftZ = KCLFile.ReadUInt32(); + int PositionCount = (NormalsOffset - PositionsOffset) / 12, + NormalCount = (TrianglesOffset - NormalsOffset) / 12, + TriangleCount = (OctreeOffset - TrianglesOffset) / 16, + OctreeNodeCount + = ((~(int)MaskX >> (int)ShiftX) + 1) + * ((~(int)MaskY >> (int)ShiftX) + 1) + * ((~(int)MaskZ >> (int)ShiftX) + 1); + MaxItemsForPogress = PositionCount + NormalCount + TriangleCount + OctreeNodeCount; + + //Section 1 - Verticies + KCLFile.Position = FileStart + PositionsOffset; + for (int i = 0; i < PositionCount; i++) + { + Positions.Add(new Vector3(KCLFile.ReadSingle(), KCLFile.ReadSingle(), KCLFile.ReadSingle())); + bgw?.ReportProgress((int)GetPercentOf(i, MaxItemsForPogress), 0); + } + + //Section 2 - Normals + KCLFile.Position = FileStart + NormalsOffset; + for (int i = 0; i < NormalCount; i++) + { + Normals.Add(new Vector3(KCLFile.ReadSingle(), KCLFile.ReadSingle(), KCLFile.ReadSingle())); + bgw?.ReportProgress((int)GetPercentOf(PositionCount + i, MaxItemsForPogress), 0); + } + + //Section 3 - Triangles + KCLFile.Position = FileStart + TrianglesOffset; + + for (int i = 0; i < TriangleCount; i++) + { + Triangles.Add(new KCLFace(KCLFile)); + bgw?.ReportProgress((int)GetPercentOf(PositionCount + NormalCount + i, MaxItemsForPogress), 0); + } + + //Section 4 - Spatial Index + KCLFile.Position = FileStart + OctreeOffset; + + OctreeRoots = new Octree[OctreeNodeCount]; + for (int i = 0; i < OctreeNodeCount; i++) + { + OctreeRoots[i] = new Octree(KCLFile, FileStart + OctreeOffset); + bgw?.ReportProgress((int)GetPercentOf(PositionCount + NormalCount + TriangleCount + i, MaxItemsForPogress), 0); + } + bgw?.ReportProgress(100, 0); + } + + public void Write(Stream KCLFile, BackgroundWorker? bgw = null) + { + int OctreeNodeCount = GetNodeCount(OctreeRoots); + Queue queuedNodes = new Queue(); + Dictionary indexPool = CreateIndexBuffer(queuedNodes); + + int MaxItemsForPogress = Positions.Count + Normals.Count + Triangles.Count + OctreeNodeCount; + foreach (KeyValuePair item in indexPool) + MaxItemsForPogress += item.Key.Length; + + bgw?.ReportProgress(0, 2); + long FileStart = KCLFile.Position; + //Header + KCLFile.WriteUInt32(0x38); + KCLFile.WriteUInt32((uint)(0x38 + (Positions.Count*12))); + KCLFile.WriteUInt32((uint)(0x38 + (Positions.Count * 12) + (Normals.Count * 12) - 0x10)); + uint OctreeOffset = (uint)((0x38 + (Positions.Count * 12) + (Normals.Count * 12) + (Triangles.Count * 0x10))); + KCLFile.WriteUInt32(OctreeOffset); + + KCLFile.WriteSingle(40f); + KCLFile.WriteSingle(MinCoords.X); + KCLFile.WriteSingle(MinCoords.Y); + KCLFile.WriteSingle(MinCoords.Z); + KCLFile.WriteUInt32(MaskX); + KCLFile.WriteUInt32(MaskY); + KCLFile.WriteUInt32(MaskZ); + KCLFile.WriteUInt32(ShiftX); + KCLFile.WriteUInt32(ShiftY); + KCLFile.WriteUInt32(ShiftZ); + + for (int i = 0; i < Positions.Count; i++) + { + KCLFile.WriteSingle(Positions[i].X); + KCLFile.WriteSingle(Positions[i].Y); + KCLFile.WriteSingle(Positions[i].Z); + bgw?.ReportProgress((int)GetPercentOf(i, MaxItemsForPogress), 2); + } + for (int i = 0; i < Normals.Count; i++) + { + KCLFile.WriteSingle(Normals[i].X); + KCLFile.WriteSingle(Normals[i].Y); + KCLFile.WriteSingle(Normals[i].Z); + bgw?.ReportProgress((int)GetPercentOf(Positions.Count + i, MaxItemsForPogress), 2); + } + for (int i = 0; i < Triangles.Count; i++) + { + Triangles[i].Write(KCLFile); + bgw?.ReportProgress((int)GetPercentOf(Positions.Count + Normals.Count + i, MaxItemsForPogress), 2); + } + + //Octree time + int triangleListPos = OctreeNodeCount * sizeof(uint); + + queuedNodes.Enqueue(OctreeRoots); + int OctreeCounter = 0; + while (queuedNodes.Count > 0) + { + Octree[] nodes = queuedNodes.Dequeue(); + long offset = KCLFile.Position - FileStart - OctreeOffset; + foreach (Octree node in nodes) + { + if (node.Children == null) + { + // Node is a leaf and points to triangle index list. + ushort[] indices = node.TriangleIndices.ToArray(); + int listPos = triangleListPos + indexPool[indices]; + node.Key = (uint)Octree.Flags.Values | (uint)(listPos - offset - sizeof(ushort)); + } + else + { + // Node is a branch and points to 8 children. + node.Key = (uint)(nodes.Length + queuedNodes.Count * 8) * sizeof(uint); + queuedNodes.Enqueue(node.Children); + } + KCLFile.WriteUInt32(node.Key); + bgw?.ReportProgress((int)GetPercentOf(Positions.Count + Normals.Count + Triangles.Count + OctreeCounter++, MaxItemsForPogress), 2); + } + } + + int indexindex = 0; + foreach (var ind in indexPool) + { + //Last value skip. Uses terminator of previous index list + if (ind.Key.Length == 0) + break; + //Save the index lists and terminator + for (int i = 0; i < ind.Key.Length; i++) + KCLFile.WriteUInt16((ushort)(ind.Key[i] + 1)); //-1 indexed + KCLFile.Write(new byte[2], 0, 2); // Terminator + + bgw?.ReportProgress((int)GetPercentOf(Positions.Count + Normals.Count + Triangles.Count + OctreeNodeCount + indexindex++ , MaxItemsForPogress), 2); + } + bgw?.ReportProgress(100, 2); + } + + private int IndexOfVertex(Vector3 value, List valueList, Dictionary lookupTable) + { + //Correct all -0's... no idea why they appear + //if (value.X == -0) + // value.X = 0; + //if (value.Y == -0) + // value.Y = 0; + //if (value.Z == -0) + // value.Z = 0; + string key = value.ToString(); + if (!lookupTable.ContainsKey(key)) + { + valueList.Add(value); + lookupTable.Add(key, lookupTable.Count); + } + + return lookupTable[key]; + } + + /// + /// Gets the next power of 2 which results in a value bigger than or equal to . + /// + /// The value to which the next power of 2 will be determined. + /// The next power of resulting in a value bigger than or equal to the given value. + internal static int GetNext2Exponent(float value) + { + if (value <= 1) return 0; + return (int)Math.Ceiling(Math.Log(value, 2)); + } + + public Triangle GetTriangle(KCLFace prism) + { + Vector3 A = Positions[prism.PositionIndex]; + Vector3 CrossA = Vector3.Cross(Normals[prism.NormalAIndex], Normals[prism.DirectionIndex]); + Vector3 CrossB = Vector3.Cross(Normals[prism.NormalBIndex], Normals[prism.DirectionIndex]); + Vector3 B = A + CrossB * (prism.Length / Vector3.Dot(CrossB, Normals[prism.NormalCIndex])); + Vector3 C = A + CrossA * (prism.Length / Vector3.Dot(CrossA, Normals[prism.NormalCIndex])); + Vector3 N = Vector3.Normalize(Vector3.Cross(B - A, C - A)); + return new Triangle() { Vertex1 = A, Vertex2 = B, Vertex3 = C, Normal = N, GroupIndex = prism.GroupIndex }; + } + + private int GetNodeCount(Octree[] nodes) + { + int count = nodes.Length; + foreach (Octree node in nodes) + if (node.Children != null) + count += GetNodeCount(node.Children); + return count; + } + + //Create an index buffer to find matching index lists + private Dictionary CreateIndexBuffer(Queue queuedNodes) + { + Dictionary indexPool = new Dictionary(new IndexEqualityComparer()); + int offset = 0; + queuedNodes.Enqueue(OctreeRoots); + int index = 0; + while (queuedNodes.Count > 0) + { + Octree[] nodes = queuedNodes.Dequeue(); + foreach (Octree node in nodes) + { + if (node.Children == null) + { + ushort[] indices = node.TriangleIndices.ToArray(); + if (node.TriangleIndices.Count > 0 && !indexPool.ContainsKey(indices)) + { + indexPool.Add(indices, offset); + offset += (node.TriangleIndices.Count + 1) * sizeof(ushort); //+1 to add terminator + index++; + } + } + else + { + // Node is a branch and points to 8 children. + queuedNodes.Enqueue(node.Children); + } + } + } + //Empty values are last in the buffer using the last terminator + indexPool.Add(new ushort[0], offset - sizeof(ushort)); + return indexPool; + } + + private class IndexEqualityComparer : IEqualityComparer + { + public bool Equals(ushort[]? x, ushort[]? y) + { + if (x.Length != y.Length) + return false; + for (int i = 0; i < x.Length; i++) + if (x[i] != y[i]) + return false; + return true; + } + + public int GetHashCode(ushort[] obj) + { + int result = 17; + for (int i = 0; i < obj.Length; i++) + unchecked + { + result = result * 23 + obj[i]; + } + return result; + } + } + + public class KCLFace + { + public float Length; + public ushort PositionIndex; + public ushort DirectionIndex; + public ushort NormalAIndex, NormalBIndex, NormalCIndex; + public ushort GroupIndex; + + public KCLFace() { } + public KCLFace(Stream KCLFile) + { + Length = KCLFile.ReadSingle(); + PositionIndex = KCLFile.ReadUInt16(); + DirectionIndex = KCLFile.ReadUInt16(); + NormalAIndex = KCLFile.ReadUInt16(); + NormalBIndex = KCLFile.ReadUInt16(); + NormalCIndex = KCLFile.ReadUInt16(); + GroupIndex = KCLFile.ReadUInt16(); + } + + public override string ToString() => $"KCLFace: P = {PositionIndex} | D = {DirectionIndex} | A = {NormalAIndex} | B = {NormalBIndex} | C = {NormalCIndex} | Group = {GroupIndex}"; + + internal void Write(Stream KCLFile) + { + KCLFile.WriteSingle(Length); + KCLFile.WriteUInt16(PositionIndex); + KCLFile.WriteUInt16(DirectionIndex); + KCLFile.WriteUInt16(NormalAIndex); + KCLFile.WriteUInt16(NormalBIndex); + KCLFile.WriteUInt16(NormalCIndex); + KCLFile.WriteUInt16(GroupIndex); + } + } + + public class Octree : IEnumerable + { + // ---- CONSTANTS ---------------------------------------------------------------------------------------------- + + /// + /// The number of children of an octree node. + /// + public const int ChildCount = 8; + + /// + /// The bits storing the flags of this node. + /// + protected const uint _flagMask = 0b11000000_00000000_00000000_00000000; + + // ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------ + + internal Octree(Stream KCLFile, long parentOffset) + { + Key = KCLFile.ReadUInt32(); + int terminator = 0x00; + + // Get and seek to the data offset in bytes relative to the parent node's start. + long offset = parentOffset + Key & ~_flagMask; + if ((Key >> 31) == 1) //Check for leaf + { + // Node is a leaf and key points to triangle list starting 2 bytes later. + long pauseposition = KCLFile.Position; + KCLFile.Position = offset + sizeof(ushort); + TriangleIndices = new List(); + ushort index; + while ((index = KCLFile.ReadUInt16()) != terminator) + { + TriangleIndices.Add((ushort)(index - 1)); + } + + KCLFile.Position = pauseposition; + } + else + { + // Node is a branch and points to 8 child nodes. + long pauseposition = KCLFile.Position; + KCLFile.Position = offset; + + Octree[] children = new Octree[ChildCount]; + for (int i = 0; i < ChildCount; i++) + { + children[i] = new Octree(KCLFile, offset); + } + Children = children; + + KCLFile.Position = pauseposition; + } + } + + internal Octree(Dictionary triangles, Vector3 cubePosition, float cubeSize, int maxTrianglesInCube, int maxCubeSize, int minCubeSize, int cubeBlow, int maxDepth, int depth = 0) + { + Key = 0; + //Adjust the cube sizes based on EFE's method + Vector3 cubeCenter = cubePosition + new Vector3(cubeSize / 2f, cubeSize / 2f, cubeSize / 2f); + float newsize = cubeSize + cubeBlow; + Vector3 newPosition = cubeCenter - new Vector3(newsize / 2f, newsize / 2f, newsize / 2f); + + // Go through all triangles and remember them if they overlap with the region of this cube. + Dictionary containedTriangles = new Dictionary(); + foreach (KeyValuePair triangle in triangles) + { + if (TriangleCubeOverlap(triangle.Value, newPosition, newsize)) + { + containedTriangles.Add(triangle.Key, triangle.Value); + } + } + + float halfWidth = cubeSize / 2f; + + bool isTriangleList = cubeSize <= maxCubeSize && containedTriangles.Count <= maxTrianglesInCube || cubeSize <= minCubeSize || depth > maxDepth; + + if (containedTriangles.Count > maxTrianglesInCube && halfWidth >= minCubeSize) + { + // Too many triangles are in this cube, and it can still be subdivided into smaller cubes. + float childCubeSize = cubeSize / 2f; + Children = new Octree[ChildCount]; + int i = 0; + for (int z = 0; z < 2; z++) + { + for (int y = 0; y < 2; y++) + { + for (int x = 0; x < 2; x++) + { + Vector3 childCubePosition = cubePosition + childCubeSize * new Vector3(x, y, z); + Children[i++] = new Octree(containedTriangles, childCubePosition, childCubeSize, + maxTrianglesInCube, maxCubeSize, minCubeSize, cubeBlow, maxDepth, depth + 1); + } + } + } + } + else + { + // Either the amount of triangles in this cube is okay or it cannot be subdivided any further. + TriangleIndices = containedTriangles.Keys.ToList(); + } + } + + internal Octree() + { + + } + + // ---- PROPERTIES --------------------------------------------------------------------------------------------- + + /// + /// Gets the octree key used to reference this node. + /// + public uint Key { get; internal set; } + + /// + /// Gets the eight children of this node. + /// + public Octree[]? Children { get; internal set; } = null; + + /// + /// Gets the indices to triangles of the model appearing in this cube. + /// + public List TriangleIndices { get; internal set; } + + // ---- METHODS (PUBLIC) --------------------------------------------------------------------------------------- + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that can be used to iterate through the collection. + public IEnumerator GetEnumerator() => Children == null ? null : ((IEnumerable)Children).GetEnumerator(); + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() => Children?.GetEnumerator(); + + public bool IsEmpty() => TriangleIndices?.Count == 0; + + public override string ToString() => IsEmpty() ? "Empty Octree" : $"{Key & 0x0FFFFFFF} | {TriangleIndices?.Count} tris | {(Children is null ? "No Children" : "Children")}"; + + public static bool TriangleCubeOverlap(Triangle t, Vector3 Position, float BoxSize) + { + float half = BoxSize / 2f; + //Position is the min pos, so add half the box size + Position += new Vector3(half, half, half); + Vector3 v0 = t[0] - Position; + Vector3 v1 = t[1] - Position; + Vector3 v2 = t[2] - Position; + + float min = Math.Min(Math.Min(v0.X, v1.X), v2.X); + float max = Math.Max(Math.Max(v0.X, v1.X), v2.X); + if (min > half || max < -half) return false; + if (Math.Min(Math.Min(v0.Y, v1.Y), v2.Y) > half || Math.Max(Math.Max(v0.Y, v1.Y), v2.Y) < -half) return false; + if (Math.Min(Math.Min(v0.Z, v1.Z), v2.Z) > half || Math.Max(Math.Max(v0.Z, v1.Z), v2.Z) < -half) return false; + + float d = Vector3.Dot(t.Normal, v0); + double r = half * (Math.Abs(t.Normal.X) + Math.Abs(t.Normal.Y) + Math.Abs(t.Normal.Z)); + if (d > r || d < -r) return false; + + Vector3 e = v1 - v0; + if (AxisTest(e.Z, -e.Y, v0.Y, v0.Z, v2.Y, v2.Z, half)) return false; + if (AxisTest(-e.Z, e.X, v0.X, v0.Z, v2.X, v2.Z, half)) return false; + if (AxisTest(e.Y, -e.X, v1.X, v1.Y, v2.X, v2.Y, half)) return false; + + e = v2 - v1; + if (AxisTest(e.Z, -e.Y, v0.Y, v0.Z, v2.Y, v2.Z, half)) return false; + if (AxisTest(-e.Z, e.X, v0.X, v0.Z, v2.X, v2.Z, half)) return false; + if (AxisTest(e.Y, -e.X, v0.X, v0.Y, v1.X, v1.Y, half)) return false; + + e = v0 - v2; + if (AxisTest(e.Z, -e.Y, v0.Y, v0.Z, v1.Y, v1.Z, half)) return false; + if (AxisTest(-e.Z, e.X, v0.X, v0.Z, v1.X, v1.Z, half)) return false; + if (AxisTest(e.Y, -e.X, v1.X, v1.Y, v2.X, v2.Y, half)) return false; + return true; + } + private static bool AxisTest(double a1, double a2, double b1, double b2, double c1, double c2, double half) + { + var p = a1 * b1 + a2 * b2; + var q = a1 * c1 + a2 * c2; + var r = half * (Math.Abs(a1) + Math.Abs(a2)); + return Math.Min(p, q) > r || Math.Max(p, q) < -r; + } + + // ---- ENUMERATIONS ------------------------------------------------------------------------------------------- + + internal enum Flags : uint + { + Divide = 0b00000000_00000000_00000000_00000000, + Values = 0b10000000_00000000_00000000_00000000, + NoData = 0b11000000_00000000_00000000_00000000 + } + } + + public class PaEntry : BCSV.BCSV.Entry + { + private const string CAMERA_ID = "camera_id"; + private const uint CAMERA_ID_MASK = 0x000000FF; + private const byte CAMERA_ID_SHIFT = 0; + private const string SOUND_CODE = "Sound_code"; + private const uint SOUND_CODE_MASK = 0x00007F00; + private const byte SOUND_CODE_SHIFT = 8; + private const string FLOOR_CODE = "Floor_code"; + private const uint FLOOR_CODE_MASK = 0x01F8000; + private const byte FLOOR_CODE_SHIFT = 15; + private const string WALL_CODE = "Wall_code"; + private const uint WALL_CODE_MASK = 0x01E00000; + private const byte WALL_CODE_SHIFT = 21; + private const string CAMERA_THROUGH = "Camera_through"; + private const uint CAMERA_THROUGH_MASK = 0x02000000; + private const byte CAMERA_THROUGH_SHIFT = 25; + + public int CameraID + { + get => (int)Data[BCSV.BCSV.StringToHash_JGadget(CAMERA_ID)]; + set => Data[BCSV.BCSV.StringToHash_JGadget(CAMERA_ID)] = (int)(value & (CAMERA_ID_MASK >> CAMERA_ID_SHIFT)); + } + public int SoundCode + { + get => (int)Data[BCSV.BCSV.StringToHash_JGadget(SOUND_CODE)]; + set => Data[BCSV.BCSV.StringToHash_JGadget(SOUND_CODE)] = (int)(value & (SOUND_CODE_MASK >> SOUND_CODE_SHIFT)); + } + public int FloorCode + { + get => (int)Data[BCSV.BCSV.StringToHash_JGadget(FLOOR_CODE)]; + set => Data[BCSV.BCSV.StringToHash_JGadget(FLOOR_CODE)] = (int)(value & (FLOOR_CODE_MASK >> FLOOR_CODE_SHIFT)); + } + public int WallCode + { + get => (int)Data[BCSV.BCSV.StringToHash_JGadget(WALL_CODE)]; + set => Data[BCSV.BCSV.StringToHash_JGadget(WALL_CODE)] = (int)(value & (WALL_CODE_MASK >> WALL_CODE_SHIFT)); + } + public int CameraThrough + { + get => (int)Data[BCSV.BCSV.StringToHash_JGadget(CAMERA_THROUGH)]; + set => Data[BCSV.BCSV.StringToHash_JGadget(CAMERA_THROUGH)] = (int)(value & (CAMERA_THROUGH_MASK >> WALL_CODE_SHIFT)); + } + + /// + /// The index of the string is the number for the BCSV Entry. 23 sound codes in SMG2 + /// + public static readonly string[] SOUND_CODES = new string[] + { + "Default", + "Dirt", + "Grass", + "Stone", + "Marble Tile", + "Wood", + "Hollow Wood", + "Metal", + "Snow", + "Ice", + "No Sound", + "Desert Sand", + "Beach Sand", + "Carpet", + "Mud", + "Honey", + "Metal (Higher Pitched)", + "Marble Tile (Snow w/ Bulb Berry)", + "Marble Tile (Dirt w/ Bulb Berry)", + "Metal (Soil w/ Bulb Berry)", + "Cloud", + "Marble Tile (Beach Sand w/ Bulb Berry)", + "Marble Tile (Desert Sand w/ Bulb Berry)" + }; + + /// + /// The index of the string is the number for the BCSV Entry. 44 Floor codes in SMG2 + /// + public static readonly string[] FLOOR_CODES = new string[] + { + "Default", //0 + "Death", //1 + "Encourage Slipping", //2 + "Prevent Slipping", //3 + "Inflict Knockback Damage", //4 + "Skateable Ice", //5 + "Bouncy (Small Bounce)", //6 + "Bouncy (Medium Bounce)", //7 + "Bouncy (Large Bounce)", //8 + "Force Sliding [Surface must be tilted]", //9 + "Lava", //10 + "Bouncy (Small Bounce) [Slightly Different]", //11 + "Wandering Dry Bones Repellant", //12 + "Sand", //13 + "Glass", //14 + "Inflict Electric Damage", //15 + "Activate Return Bubble", //16 + "Quicksand", //17 + "Poison Quicksand", //18 + "No Traction", //19 + "Chest Deep Water Pool Floor", + "Waist Deep Water Pool Floor", + "Knees Deep Water Pool Floor", + "Water Puddle Floor", + "Inflict Spike Damage", + "Deadly Quicksand", + "Snow", + "Move Player in Rail Direction", + "Activate MoveAreaSphere", + "Allow Crushing the Player", + "Sand (No Footprints)", + "Deadly Poison Quicksand", + "Mud", + "Ice (w/ Player Reflection)", + "Bouncy (Small Bounce) [Beach Umbrella]", + "Non-Skatable Ice", + "No Spin Drill Dig", + "Grass", + "Cloud", + "Allow Crushing the Player (No Slipping)", + "Activate ForceDashCube", + "Dark Matter", + "Dusty", + "Snow (No Slipping)" + }; + + /// + /// The index of the string is the number for the BCSV Entry. 9 Wall codes in SMG2 + /// + public static readonly string[] WALL_CODES = new string[] + { + "Default", + "No Wall Jumps", + "No Auto Ledge Getup", + "No Ledge Grabbing", + "Ghost Through", + "No Sidestepping", + "Rebound the Player", + "Honey Climb", + "No Action" + }; + + public PaEntry() { Init(); } + public PaEntry(Dictionary FieldSource) + { + Data = new Dictionary(); + foreach (var kv in FieldSource) + Data.Add(kv.Key, kv.Value.GetDefaultValue()); + Init(); + } + private void Init() => CameraID = 0xFF; + // !!! + /*public static void ConvertBCSV(ref BCSV.BCSV x) + { + for (int i = 0; i < x.EntryCount; i++) + { + if (x[i] is PaEntry) + continue; + + BCSV.BCSV.Entry entry = x[i]; + PaEntry newentry = new PaEntry + { + Data = entry.Data + }; + x[i] = newentry; + } + } + + public static BCSV.BCSV CreateBCSV(WavefrontObj obj) + { + BCSV.BCSV bcsv = new BCSV.BCSV(); + + BCSV.BCSV.Field Cam = new BCSV.BCSV.Field(CAMERA_ID, BCSV.BCSV.DataTypes.INT32, CAMERA_ID_MASK, CAMERA_ID_SHIFT, false); + BCSV.BCSV.Field Sound = new BCSV.BCSV.Field(SOUND_CODE, BCSV.BCSV.DataTypes.INT32, SOUND_CODE_MASK, SOUND_CODE_SHIFT, false); + BCSV.BCSV.Field Floor = new BCSV.BCSV.Field(FLOOR_CODE, BCSV.BCSV.DataTypes.INT32, FLOOR_CODE_MASK, FLOOR_CODE_SHIFT, false); + BCSV.BCSV.Field Wall = new BCSV.BCSV.Field(WALL_CODE, BCSV.BCSV.DataTypes.INT32, WALL_CODE_MASK, WALL_CODE_SHIFT, false); + BCSV.BCSV.Field CamCol = new BCSV.BCSV.Field(CAMERA_THROUGH, BCSV.BCSV.DataTypes.INT32, CAMERA_THROUGH_MASK, CAMERA_THROUGH_SHIFT, false); + + bcsv.Add(Cam, Sound, Floor, Wall, CamCol); + + for (int i = 0; i < obj.GroupNames.Count; i++) + { + bcsv.Add(new PaEntry()); + } + + return bcsv; + }*/ + } + } + + public class GeometryOverflowException : Exception + { + public GeometryOverflowException(int count, int max, string item = "Triangles") : base($"Too Many {item}! The Max {item} count is {max} (0x{max.ToString("X4")}). You have {count - max} {item.ToLower()} too many.") { } + } + + public class WavefrontObj : List + { + public List GroupNames = new List(); + public WavefrontObj() : base() { } + + public static WavefrontObj OpenWavefront(string OBJFile) + { + List Verticies = new List(); + Dictionary GroupTable = new Dictionary(); + string[] Lines = File.ReadAllLines(OBJFile); + string? GroupName = null; + int GroupID = 0; + WavefrontObj Triangles = new WavefrontObj(); + + for (int i = 0; i < Lines.Length; i++) + { + if (string.IsNullOrWhiteSpace(Lines[i]) || Lines[i].StartsWith("#")) + continue; + + string[] args = Lines[i].Split(new char[0], StringSplitOptions.RemoveEmptyEntries); + + if (args[0].Equals("usemtl")) + { + GroupName = args[1]; + if (!GroupTable.ContainsKey(GroupName)) + { + GroupTable.Add(GroupName, GroupTable.Count); + Triangles.GroupNames.Add(GroupName); + } + GroupID = GroupTable[GroupName]; + } + else if (args[0].Equals("v")) + { + Verticies.Add(new Vector3(float.Parse(args[1], CultureInfo.InvariantCulture), float.Parse(args[2], CultureInfo.InvariantCulture), float.Parse(args[3], CultureInfo.InvariantCulture))); + } + else if (args[0].Equals("f")) + { + if (Triangles.GroupNames.Count == 0) + Triangles.GroupNames.Add(GroupName); + + Vector3 U = Verticies[int.Parse(args[1].Split('/')[0])-1], + V = Verticies[int.Parse(args[2].Split('/')[0]) - 1], + W = Verticies[int.Parse(args[3].Split('/')[0]) - 1]; + if (Vector3.Cross(V - U, W - U).NormalSquare() < 0.001) + continue; //Haven't had issues with this yet... + + Triangles.Add(new Triangle(U, V, W, GroupID)); + } + } + + + if (Triangles.GroupNames[0] is null || Triangles.GroupNames[0].Equals("None")) + Triangles.GroupNames[0] = "Default"; + + return Triangles; + } + + public static WavefrontObj CreateWavefront(KCL kcl, BCSV.BCSV? CollisionCodes = null) + { + WavefrontObj Triangles = new WavefrontObj(); + int LastGroup = -1; + Dictionary Groups = new Dictionary(); + for (int i = 0; i < kcl.Triangles.Count; i++) + { + Triangles.Add(kcl.GetTriangle(kcl.Triangles[i])); + + if (Triangles[Triangles.Count-1].GroupIndex != LastGroup) + { + LastGroup = Triangles[Triangles.Count - 1].GroupIndex; + + if (CollisionCodes is null) + { + string item = $"Material{LastGroup}"; + if (!Groups.ContainsKey(LastGroup)) + Groups.Add(LastGroup, item); + } + else + { + string item = $"{((KCL.PaEntry)CollisionCodes[LastGroup]).CameraID} {KCL.PaEntry.SOUND_CODES[((KCL.PaEntry)CollisionCodes[LastGroup]).SoundCode]} {KCL.PaEntry.FLOOR_CODES[((KCL.PaEntry)CollisionCodes[LastGroup]).FloorCode]} {KCL.PaEntry.WALL_CODES[((KCL.PaEntry)CollisionCodes[LastGroup]).WallCode]} {((KCL.PaEntry)CollisionCodes[LastGroup]).CameraThrough}"; + if (!Groups.ContainsKey(LastGroup)) + Groups.Add(LastGroup, item); + } + } + } + + int m = 0; + foreach (KeyValuePair item in Groups) + { + if (item.Key > m) + m = item.Key; + } + for (int i = 0; i < m+1; i++) + { + if (!Groups.ContainsKey(i)) + Triangles.GroupNames.Insert(Math.Min(i, Triangles.GroupNames.Count), $"Unused {i}"); + else + Triangles.GroupNames.Insert(Math.Min(i, Triangles.GroupNames.Count), Groups[i]); + } + + return Triangles; + } + + public static void SaveWavefront(string OBJFile, WavefrontObj obj, BCSV.BCSV? CollisionCodes = null, BackgroundWorker? bgw = null) + { + bgw?.ReportProgress(0, 0); + Version version = Assembly.GetEntryAssembly().GetName().Version; + string result = +$@"#KCL dumped with Hack.io.KCL version {version.ToString()} +#https://github.com/SuperHackio/Hack.io +o {new FileInfo(OBJFile).Name} +"; + List Verts = new List(); + List Norms = new List(); + int Counter = (obj.Count * 3) + obj.Count; + for (int i = 0; i < obj.Count; i++) + { + AddVert(MakeVertexString(obj[i].Vertex1)); + AddVert(MakeVertexString(obj[i].Vertex2)); + AddVert(MakeVertexString(obj[i].Vertex3)); + AddNorm(MakeNormalString(obj[i].Normal)); + bgw?.ReportProgress((int)GetPercentOf(i/4f,Counter),0); + } + + for (int i = 0; i < Verts.Count; i++) + result += Verts[i] + Environment.NewLine; + for (int i = 0; i < Norms.Count; i++) + result += Norms[i] + Environment.NewLine; + + bgw?.ReportProgress(0, 1); + + int LastGroup = -1; + for (int i = 0; i < obj.Count; i++) + { + if (obj[i].GroupIndex != LastGroup) + { + LastGroup = obj[i].GroupIndex; + result += "usemtl "; + if (CollisionCodes is null) + result += $"Material{LastGroup}" + Environment.NewLine; + else + result += $"m{LastGroup} | {((KCL.PaEntry)CollisionCodes[LastGroup]).CameraID} | {KCL.PaEntry.SOUND_CODES[((KCL.PaEntry)CollisionCodes[LastGroup]).SoundCode]} | {KCL.PaEntry.FLOOR_CODES[((KCL.PaEntry)CollisionCodes[LastGroup]).FloorCode]} | {KCL.PaEntry.WALL_CODES[((KCL.PaEntry)CollisionCodes[LastGroup]).WallCode]} | {((KCL.PaEntry)CollisionCodes[LastGroup]).CameraThrough}" + Environment.NewLine; + result += $"s off{Environment.NewLine}"; //Turn off Smooth Shading + } + + int targetvec1 = Find(Verts, MakeVertexString(obj[i].Vertex1)), + targetvec2 = Find(Verts, MakeVertexString(obj[i].Vertex2)), + targetvec3 = Find(Verts, MakeVertexString(obj[i].Vertex3)), + targetnorm = Find(Norms, MakeNormalString(obj[i].Normal)); + result += $"f {targetvec1 + 1}//{targetnorm + 1} {targetvec2 + 1}//{targetnorm + 1} {targetvec3 + 1}//{targetnorm + 1}{Environment.NewLine}"; + bgw?.ReportProgress((int)GetPercentOf(i, obj.Count), 1); + } + + File.WriteAllText(OBJFile, result); + bgw?.ReportProgress(100, 1); + + string MakeVertexString(Vector3 vector) => MakeVectorString("v", vector, "0.000000"); + string MakeNormalString(Vector3 vector) => MakeVectorString("vn", vector, "0.00000"); + string MakeVectorString(string prefix, Vector3 vector, string format) => $"{prefix} {vector.X.ToString(format)} {vector.Y.ToString(format)} {vector.Z.ToString(format)}"; + + void AddVert(string s) => Add(Verts, s); + void AddNorm(string s) => Add(Norms, s); + void Add(List list, T item) + { + if (!list.Contains(item)) + list.Add(item); + } + + int Find(List list, T item) + { + for (int i = 0; i < list.Count; i++) + if (list[i].Equals(item)) + return i; + return -1; + } + } + } + + internal class VertexWelder + { + // Three randomly chosen large primes. Just like the good 'ol days + const uint MAGIC_X = 0x8DA6B343; + const uint MAGIC_Y = 0xD8163841; + const uint MAGIC_Z = 0x61B40079; + private readonly float Threshold, CellWidth; + //public List> Buckets = new List>(); + public List Verticies = new List(); + + public VertexWelder(float threshold) + { + Threshold = threshold; + CellWidth = 16.0f * threshold; + } + + //private int CalculateHash(int ix, int iy, int iz) => (int)Math.Abs(((ix * MAGIC_X) + (iy * MAGIC_Y) + (iz * MAGIC_Z)) % Buckets.Count); + + public short Add(Vector3 Vertex) + { + //Correct all -0's... no idea why they appear + if (Vertex.X == -0) + Vertex.X = 0; + if (Vertex.Y == -0) + Vertex.Y = 0; + if (Vertex.Z == -0) + Vertex.Z = 0; + + for (int i = 0; i < Verticies.Count; i++) + { + if (Math.Abs(Vertex.X - Verticies[i].X) < Threshold && Math.Abs(Vertex.Y - Verticies[i].Y) < Threshold && Math.Abs(Vertex.Z - Verticies[i].Z) < Threshold) + return (short)i; + } + + //int MinX = (int)((Vertex.X - Threshold) / CellWidth), + // MinY = (int)((Vertex.Y - Threshold) / CellWidth), + // MinZ = (int)((Vertex.Z - Threshold) / CellWidth), + // MaxX = (int)((Vertex.X + Threshold) / CellWidth), + // MaxY = (int)((Vertex.Y + Threshold) / CellWidth), + // MaxZ = (int)((Vertex.Z + Threshold) / CellWidth); + //List Bucket; + //for (int ix = MinX; ix < MaxX+1; ix++) + //{ + // for (int iy = MinY; iy < MaxY+1; iy++) + // { + // for (int iz = MinZ; iz < MaxZ+1; iz++) + // { + // int id = CalculateHash(ix, iy, iz); + // Bucket = Buckets[id]; + // for (int i = 0; i < Bucket.Count; i++) + // { + + // } + // } + // } + //} + + + Verticies.Add(Vertex); + //Bucket = Buckets[CalculateHash((int)(Vertex.X / CellWidth), (int)(Vertex.Y / CellWidth), (int)(Vertex.Z / CellWidth))]; + //Bucket.Add(Verticies.Count-1); + return (short)(Verticies.Count - 1); + } + } + + /// + /// Note: Will probably get moved to it's own project after another project needs it's usage + /// + public struct Triangle + { + public Vector3 Vertex1, Vertex2, Vertex3, Normal; + public int GroupIndex; + + public Vector3 this[int index] + { + get + { + switch (index) + { + case 0: + return Vertex1; + case 1: + return Vertex2; + case 2: + return Vertex3; + case 3: + return Normal; + default: + return Vector3.Zero; + } + } + } + + public Triangle(Vector3 u, Vector3 v, Vector3 w, int groupIndex) + { + Vertex1 = u; + Vertex2 = v; + Vertex3 = w; + + Normal = Vector3.Cross(v - u, w - u).Unit(); + GroupIndex = groupIndex; + } + + public override string ToString() + { + return $"Group {GroupIndex}"; + } + } + + public static class VectorEx + { + public static float NormalSquare(this Vector3 vector) => (vector.X * vector.X) + (vector.Y * vector.Y) + (vector.Z * vector.Z); + public static float Normal(this Vector3 vector) => (float)Math.Sqrt(vector.NormalSquare()); + public static Vector3 Unit(this Vector3 origin) => origin / origin.Normal(); + } +} diff --git a/Hack.io.KCL/OpenTK.dll.config b/Hack.io.KCL/OpenTK.dll.config new file mode 100644 index 0000000..a700f57 --- /dev/null +++ b/Hack.io.KCL/OpenTK.dll.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Hack.io.KCL/packages.config b/Hack.io.KCL/packages.config new file mode 100644 index 0000000..643e773 --- /dev/null +++ b/Hack.io.KCL/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From d8182d295ade4594649be21ac5f9bb0185c52203 Mon Sep 17 00:00:00 2001 From: Bavario Date: Sat, 18 Oct 2025 12:46:08 +0200 Subject: [PATCH 2/4] Add Little Endian support --- Hack.io.BCK/BCK.cs | 11 +- Hack.io.BPK/BPK.cs | 11 +- Hack.io.BRK/BRK.cs | 13 +- Hack.io.BTK/BTK.cs | 11 +- Hack.io.BTP/BTP.cs | 11 +- Hack.io.BVA/BVA.cs | 11 +- Hack.io.CANM/CANM.cs | 8 +- Hack.io.MSBF/MSBF.cs | 26 ++- Hack.io.MSBT/MSBT.cs | 333 ++++++++++++++++++++++++++++++++-- Hack.io.RARC/RARC.cs | 98 ++++++++-- Hack.io/Utility/FileUtil.cs | 37 +++- Hack.io/Utility/StreamUtil.cs | 49 ++++- 12 files changed, 532 insertions(+), 87 deletions(-) diff --git a/Hack.io.BCK/BCK.cs b/Hack.io.BCK/BCK.cs index e40083c..82fccb9 100644 --- a/Hack.io.BCK/BCK.cs +++ b/Hack.io.BCK/BCK.cs @@ -28,7 +28,7 @@ public class BCK : J3DAnimationBase, ILoadSaveFile public void Load(Stream Strm) { uint StartPosition = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, MAGIC); + FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.Position += 0x0C; //Strm.ReadJ3DSubVersion(); //This is not used the same way the other formats are @@ -36,7 +36,7 @@ public void Load(Stream Strm) //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); RotationMultiplier = (sbyte)Strm.ReadByte(); @@ -102,13 +102,14 @@ public void Save(Stream Strm) float rotationScale = (float)(POW) * (180.0f / 32768.0f); long Start = Strm.Position; - Strm.WriteString(MAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x4A334431); // J3D1 + Strm.WriteUInt32(0x62636B31); // bck1 Strm.WritePlaceholder(4); //FileSize - Strm.Write(new byte[4] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); //Chunk Count + Strm.WriteUInt32(1); // ChunkCount Strm.Write(CollectionUtil.InitilizeArray((byte)0xFF, 0x10)); long ChunkStart = Strm.Position; - Strm.WriteString(CHUNKMAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x414E4B31); // ANK1 Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.WriteByte((byte)RotationMultiplier); diff --git a/Hack.io.BPK/BPK.cs b/Hack.io.BPK/BPK.cs index 6a5dedd..4cbc36f 100644 --- a/Hack.io.BPK/BPK.cs +++ b/Hack.io.BPK/BPK.cs @@ -20,14 +20,14 @@ public class BPK : J3DAnimationBase, ILoadSaveFile /// public void Load(Stream Strm) { - FileUtil.ExceptionOnBadMagic(Strm, MAGIC); + FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.ReadJ3DSubVersion(); //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); Strm.Position+=0x03; //Padding 0xFF @@ -79,13 +79,14 @@ public void Load(Stream Strm) public void Save(Stream Strm) { long Start = Strm.Position; - Strm.WriteString(MAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x4A334431); // J3D1 + Strm.WriteUInt32(0x62706B31); // bpk1 Strm.WritePlaceholder(4); //FileSize - Strm.Write(new byte[4] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); //Chunk Count + Strm.WriteUInt32(1); // ChunkCount Strm.WriteJ3DSubVersion(); long ChunkStart = Strm.Position; - Strm.WriteString(CHUNKMAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x50414B31); // PAK1 Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.PadTo(0x04, 0xFF); //Padding diff --git a/Hack.io.BRK/BRK.cs b/Hack.io.BRK/BRK.cs index bdd6108..bf451a6 100644 --- a/Hack.io.BRK/BRK.cs +++ b/Hack.io.BRK/BRK.cs @@ -20,14 +20,14 @@ public class BRK : J3DAnimationBase, ILoadSaveFile /// public void Load(Stream Strm) { - FileUtil.ExceptionOnBadMagic(Strm, MAGIC); + FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.ReadJ3DSubVersion(); //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); Strm.Position++; //Padding 0xFF @@ -150,13 +150,14 @@ public void Save(Stream Strm) List Constants = new(this.Where(x => x.RegisterType == AnimationType.CONSTANT)); long Start = Strm.Position; - Strm.WriteString(MAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x4A334431); // J3D1 + Strm.WriteUInt32(0x62726B31); // brk1 Strm.WritePlaceholder(4); //FileSize - Strm.Write([0x00, 0x00, 0x00, 0x01], 0, 4); //Chunk Count + Strm.WriteUInt32(1); Strm.WriteJ3DSubVersion(); long ChunkStart = Strm.Position; - Strm.WriteString(CHUNKMAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x54524B31); // TRK1 Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.WriteByte(0xFF); //Padding @@ -268,7 +269,7 @@ public void Save(Stream Strm) Strm.WriteUInt32((uint)(FileLength - Start)); Strm.Position = ChunkStart + 0x04; - Strm.WriteUInt32((uint)(FileLength - (ChunkStart - Start))); + Strm.WriteUInt32((uint)StreamUtil.ApplyEndian(FileLength - (ChunkStart - Start))); Strm.Position = ChunkStart + 0x10; Strm.WriteUInt16((ushort)RegisterRedTable.Count); diff --git a/Hack.io.BTK/BTK.cs b/Hack.io.BTK/BTK.cs index 7879677..38fde2c 100644 --- a/Hack.io.BTK/BTK.cs +++ b/Hack.io.BTK/BTK.cs @@ -30,14 +30,14 @@ public class BTK : J3DAnimationBase, ILoadSaveFile /// public void Load(Stream Strm) { - FileUtil.ExceptionOnBadMagic(Strm, MAGIC); + FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.ReadJ3DSubVersion(); //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); RotationMultiplier = (sbyte)Strm.ReadByte(); @@ -107,13 +107,14 @@ public void Save(Stream Strm) float rotationScale = (float)(Math.Pow(2, RotationMultiplier) / 0x7FFF); long Start = Strm.Position; - Strm.WriteString(MAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x4A334431); // J3D1 + Strm.WriteUInt32(0x62746B31); // btk1 Strm.WritePlaceholder(4); //FileSize - Strm.Write(new byte[4] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); //Chunk Count + Strm.WriteUInt32(1); // ChunkCount Strm.WriteJ3DSubVersion(); long ChunkStart = Strm.Position; - Strm.WriteString(CHUNKMAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x54544B31); // TTK1 Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.WriteByte((byte)RotationMultiplier); diff --git a/Hack.io.BTP/BTP.cs b/Hack.io.BTP/BTP.cs index 597e2d3..2ae1868 100644 --- a/Hack.io.BTP/BTP.cs +++ b/Hack.io.BTP/BTP.cs @@ -16,14 +16,14 @@ public class BTP : J3DAnimationBase, ILoadSaveFile public void Load(Stream Strm) { - FileUtil.ExceptionOnBadMagic(Strm, MAGIC); + FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.Position += 0x10; //Strm.ReadJ3DSubVersion(); //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); Strm.Position++; //Padding 0xFF @@ -62,13 +62,14 @@ public void Load(Stream Strm) public void Save(Stream Strm) { long Start = Strm.Position; - Strm.WriteString(MAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x4A334431); // J3D1 + Strm.WriteUInt32(0x62747031); // btp1 Strm.WritePlaceholder(4); //FileSize - Strm.Write(new byte[4] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); //Chunk Count + Strm.WriteUInt32(1); // ChunkCount Strm.Write(CollectionUtil.InitilizeArray((byte)0xFF, 0x10)); long ChunkStart = Strm.Position; - Strm.WriteString(CHUNKMAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x54505431); // TPT1 Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.WriteByte(0xFF); diff --git a/Hack.io.BVA/BVA.cs b/Hack.io.BVA/BVA.cs index 4b91d77..71ddd63 100644 --- a/Hack.io.BVA/BVA.cs +++ b/Hack.io.BVA/BVA.cs @@ -15,14 +15,14 @@ public class BVA : J3DAnimationBase, ILoadSaveFile public void Load(Stream Strm) { - FileUtil.ExceptionOnBadMagic(Strm, MAGIC); + FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.Position += 0x10; //Strm.ReadJ3DSubVersion(); //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); Strm.Position++; //Padding 0xFF @@ -62,13 +62,14 @@ public void Load(Stream Strm) public void Save(Stream Strm) { long Start = Strm.Position; - Strm.WriteString(MAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x4A334431); // J3D1 + Strm.WriteUInt32(0x62766131); // bva1 Strm.WritePlaceholder(4); //FileSize - Strm.Write(new byte[4] { 0x00, 0x00, 0x00, 0x01 }, 0, 4); //Chunk Count + Strm.WriteUInt32(1); //ChunkCount Strm.Write(CollectionUtil.InitilizeArray((byte)0xFF, 0x10)); long ChunkStart = Strm.Position; - Strm.WriteString(CHUNKMAGIC, Encoding.ASCII, null); + Strm.WriteUInt32(0x56414631); // VAF1 Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.WriteByte(0xFF); diff --git a/Hack.io.CANM/CANM.cs b/Hack.io.CANM/CANM.cs index 7b70730..752dc7d 100644 --- a/Hack.io.CANM/CANM.cs +++ b/Hack.io.CANM/CANM.cs @@ -79,10 +79,10 @@ public void Save(Stream Strm) //long Start = Strm.Position; Strm.WriteString(MAGIC, Encoding.ASCII, null); Strm.WriteString(IsFullFrames ? FRAMETYPE_CANM : FRAMETYPE_CKAN, Encoding.ASCII, null); - Strm.WriteInt32(Unknown1); - Strm.WriteInt32(Unknown2); - Strm.WriteInt32(Unknown3); - Strm.WriteInt32(Unknown4); + Strm.WriteInt32((int)StreamUtil.ApplyEndian(Unknown1)); + Strm.WriteInt32((int)StreamUtil.ApplyEndian(Unknown2)); + Strm.WriteInt32((int)StreamUtil.ApplyEndian(Unknown3)); + Strm.WriteInt32((int)StreamUtil.ApplyEndian(Unknown4)); Strm.WriteInt32(Length); Strm.WriteInt32(IsFullFrames ? 0x40 : 0x60); diff --git a/Hack.io.MSBF/MSBF.cs b/Hack.io.MSBF/MSBF.cs index f380a7d..38a23db 100644 --- a/Hack.io.MSBF/MSBF.cs +++ b/Hack.io.MSBF/MSBF.cs @@ -1,5 +1,6 @@ using Hack.io.Interface; using Hack.io.Utility; +using System.Buffers.Binary; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; @@ -15,6 +16,9 @@ public class MSBF : ILoadSaveFile public const string MAGIC_FLW2 = "FLW2"; public const string MAGIC_FEN1 = "FEN1"; public const string MAGIC_REF1 = "REF1"; + public const string MAGIC_FLW2_LE = "2WLF"; + public const string MAGIC_FEN1_LE = "1NEF"; + public const string MAGIC_REF1_LE = "1FER"; [DisallowNull] public List Flows = []; @@ -24,7 +28,19 @@ public void Load(Stream Strm) { long FileStart = Strm.Position; FileUtil.ExceptionOnBadMagic(Strm, MAGIC); - FileUtil.ExceptionOnMisMatchedBOM(Strm); + ushort BOM = Strm.ReadUInt16(); + if (BOM == 0xFEFF) + { + StreamUtil.SetEndianBig(); + } + else if (BOM == 0xFFFE) + { + StreamUtil.SetEndianLittle(); + } + else + { + throw new InvalidOperationException($"Unknown File BOM {BOM:X2}"); + } Strm.Position += 0x03; if (Strm.ReadUInt8() != 0x03) throw new NotImplementedException("MSBF versions other than 3 are currently not supported"); @@ -45,11 +61,11 @@ public void Load(Stream Strm) uint ChunkSize = Strm.ReadUInt32(); Strm.Position += 0x08; - if (Header.Equals(MAGIC_FLW2)) + if (Header.Equals(MAGIC_FLW2) || Header.Equals(MAGIC_FLW2_LE)) ReadFLW2(); - if (Header.Equals(MAGIC_FEN1)) + if (Header.Equals(MAGIC_FEN1) || Header.Equals(MAGIC_FEN1_LE)) ReadFEN1(); - if (Header.Equals(MAGIC_REF1)) + if (Header.Equals(MAGIC_REF1) || Header.Equals(MAGIC_REF1_LE)) ReadREF1(); Strm.Position = ChunkStart + 0x10 + ChunkSize; @@ -107,6 +123,8 @@ void ReadFLW2() long ChunkStart = Strm.Position; ushort NodeCount = Strm.ReadUInt16(); ushort IndexCount = Strm.ReadUInt16(); + if (!StreamUtil.GetCurrentEndian()) + IndexCount = BinaryPrimitives.ReverseEndianness(IndexCount); Strm.Position += 0x04; for (int i = 0; i < NodeCount; i++) diff --git a/Hack.io.MSBT/MSBT.cs b/Hack.io.MSBT/MSBT.cs index 3a13e59..2c6f155 100644 --- a/Hack.io.MSBT/MSBT.cs +++ b/Hack.io.MSBT/MSBT.cs @@ -13,7 +13,10 @@ public class MSBT : ILoadSaveFile public const string MAGIC_LBL1 = "LBL1"; public const string MAGIC_ATR1 = "ATR1"; public const string MAGIC_TXT2 = "TXT2"; - + public const string MAGIC_LBL1_LE = "1LBL"; + public const string MAGIC_ATR1_LE = "1RTA"; + public const string MAGIC_TXT2_LE = "2TXT"; + private Encoding mEncoding = Encoding.UTF8; public Encoding TextEncoding { @@ -34,7 +37,19 @@ public void Load(Stream Strm) { long FileStart = Strm.Position; FileUtil.ExceptionOnBadMagic(Strm, MAGIC); - FileUtil.ExceptionOnMisMatchedBOM(Strm); + ushort BOM = Strm.ReadUInt16(); + if (BOM == 0xFEFF) + { + StreamUtil.SetEndianBig(); + } + else if (BOM == 0xFFFE) + { + StreamUtil.SetEndianLittle(); + } + else + { + throw new InvalidOperationException($"Unknown File BOM {BOM:X2}"); + } Strm.Position += 0x02; byte EncByte = Strm.ReadUInt8(); if (EncByte == 0) @@ -62,11 +77,11 @@ public void Load(Stream Strm) uint ChunkSize = Strm.ReadUInt32(); Strm.Position += 0x08; - if (Header.Equals(MAGIC_LBL1)) + if (Header.Equals(MAGIC_LBL1) || Header.Equals(MAGIC_LBL1_LE)) ReadLBL1(); - if (Header.Equals(MAGIC_ATR1)) + if (Header.Equals(MAGIC_ATR1) || Header.Equals(MAGIC_ATR1_LE)) ReadATR1(); - if (Header.Equals(MAGIC_TXT2)) + if (Header.Equals(MAGIC_TXT2) || Header.Equals(MAGIC_TXT2_LE)) ReadTXT2(); Strm.Position = ChunkStart + 0x10 + ChunkSize; @@ -74,7 +89,6 @@ public void Load(Stream Strm) Strm.Position += (16 - (ChunkSize % 16)); } - //Join things for (int i = 0; i < Messages.Count; i++) { @@ -103,9 +117,13 @@ void ReadLBL1() for (uint i = 0; i < Count; i++) { Strm.Position = BucketStart + (i * 8); - int EntryCount = Strm.ReadInt32(); + uint EntryCount = Strm.ReadUInt32(); uint Offset = Strm.ReadUInt32(); Strm.Position = ChunkStart + Offset; + LabelEntry labelEntry = new(); + labelEntry.numStrings = EntryCount; + labelEntry.indexes = new int[EntryCount]; + labelEntry.strings = new string[EntryCount]; for (int l = 0; l < EntryCount; l++) { @@ -113,7 +131,10 @@ void ReadLBL1() string label = Strm.ReadString(length, Encoding.ASCII); int Index = Strm.ReadInt32(); TemporaryLabelStorage.Add(Index, label); + labelEntry.indexes[l] = Index; + labelEntry.strings[l] = label; } + LabelEntries.Add(labelEntry); } } @@ -137,10 +158,18 @@ void ReadATR1() AlreadyTalked = Strm.ReadUInt8() }; - uint StringOffset = Strm.ReadUInt32(); + uint StringOffset = (uint)StreamUtil.ApplyEndian(Strm.ReadUInt32()); + long curPos = Strm.Position; Strm.Position = ChunkStart + StringOffset; NewAttribute.Comment = Strm.ReadString(TextEncoding, TextEncoding.GetStride()); - + if (Size == 0x10) + { + // In the Switch port of SMG2 an unknown string offset has been added + Strm.Position = curPos; + StringOffset = (uint)StreamUtil.ApplyEndian(Strm.ReadUInt32()); + Strm.Position = ChunkStart + StringOffset; + NewAttribute.Unknown = Strm.ReadString(TextEncoding, TextEncoding.GetStride()); + } TemporaryAttributes.Add(NewAttribute); } } @@ -165,10 +194,176 @@ void ReadTXT2() public void Save(Stream Strm) { - throw new NotImplementedException(); - } + Strm.WriteString("MsgStdBn", Encoding.ASCII, null); + Strm.WriteUInt16(0xFEFF); + Strm.Position += 0x02; + if (TextEncoding == Encoding.UTF8) + Strm.WriteUInt8(0); + else if (TextEncoding == Encoding.BigEndianUnicode || TextEncoding == Encoding.Unicode) + Strm.WriteUInt8(1); + Strm.WriteUInt8(0x03); + long FileStart = Strm.Position; + // Section Count + // Strm.Position += 0x02 + // File Size + // Strm.Position += 0x0A + Strm.Position += 0x12; + + ushort SectionCount = 0; + WriteLBL1(); + WriteATR1(); + WriteTXT2(); + uint FileSize = (uint)Strm.Position; + + Strm.Position = FileStart; + Strm.WriteUInt16(SectionCount); + Strm.Position += 0x02; + Strm.WriteUInt32(FileSize); + Strm.Close(); + /*uint CalcHashBucketIndex(string label, uint bucketCount) + { + uint hash = 0; + foreach (char c in label) + hash = (hash * 0x492 + c) & 0xFFFFFFFF; + return hash % bucketCount; + }*/ + + void WriteLBL1() + { + Strm.WriteUInt32(0x4C424C31); // LBL1 + long SectionStart = Strm.Position; + Strm.Position += 0xC; + long ChunkStart = Strm.Position; + int labelCount = LabelEntries.Count; + Strm.WriteUInt32((uint)labelCount); + + long LabelEntrySize = 4 + labelCount * 8; + long StringSize = 0; + + for (int i = 0; i < labelCount; i++) + { + LabelEntry Current = LabelEntries[i]; + Strm.Position = ChunkStart + 4 + i * 8; + Strm.WriteUInt32(Current.numStrings); + uint StringOffset = (uint)(LabelEntrySize + StringSize); + Strm.WriteUInt32(StringOffset); + Strm.Position = ChunkStart + StringOffset; + for (int j = 0; j < Current.numStrings; j++) + { + Strm.WriteUInt8((byte)Current.strings[j].Length); + Strm.WriteString(Current.strings[j], Encoding.ASCII, null); + Strm.WriteInt32(Current.indexes[j]); + StringSize += 1 + Current.strings[j].Length + 4; + } + } + + long AfterEntries = Strm.Position; + uint SectionSize = (uint)(AfterEntries - ChunkStart); + Strm.Position = SectionStart; + Strm.WriteUInt32(SectionSize); + Strm.Position = AfterEntries; + Strm.PadTo(16, 0xAB); + SectionCount++; + } + + void WriteATR1() + { + Strm.WriteUInt32(0x41545231); // ATR1 + long SectionStart = Strm.Position; + Strm.Position += 0xC; + long ChunkStart = Strm.Position; + int bucketCount = Messages.Count; + Strm.WriteUInt32((uint)bucketCount); + + uint AttrSize = 0xC; + if (Messages.Count > 0 && Messages[0].Attributes.Unknown != null) + AttrSize = 0x10; + Strm.WriteUInt32(AttrSize); + + long StringSize = 0; + for (int i = 0; i < bucketCount; i++) + { + Attribute Current = Messages[i].Attributes; + Strm.WriteByte(Current.SoundId); + Strm.WriteEnum(Current.CameraType, StreamUtil.WriteUInt8); + Strm.WriteEnum(Current.TalkType, StreamUtil.WriteUInt8); + Strm.WriteEnum(Current.MessageBoxType, StreamUtil.WriteUInt8); + Strm.WriteUInt16(Current.CameraId); + Strm.WriteByte(Current.MessageAreaId); + Strm.WriteByte(Current.AlreadyTalked); + + uint StringOffset = (uint)(8 + AttrSize * bucketCount + StringSize); + Strm.WriteUInt32((uint)StreamUtil.ApplyEndian(StringOffset)); + long curPos = Strm.Position; + Strm.Position = ChunkStart + StringOffset; + Strm.WriteString(Current.Comment, TextEncoding); + StringSize += TextEncoding.GetByteCount(Current.Comment) + TextEncoding.GetByteCount("\0"); + Strm.Position = curPos; + + if (Current.Unknown != null) + { + StringOffset = (uint)(8 + AttrSize * bucketCount + StringSize); + Strm.WriteUInt32((uint)StreamUtil.ApplyEndian(StringOffset)); + curPos = Strm.Position; + Strm.Position = ChunkStart + StringOffset; + Strm.WriteString(Current.Comment, TextEncoding); + StringSize += TextEncoding.GetByteCount(Current.Comment) + TextEncoding.GetByteCount("\0"); + Strm.Position = curPos; + } + } + long AfterEntries = 8 + AttrSize * bucketCount + StringSize; + uint SectionSize = (uint)AfterEntries; + Strm.Position = SectionStart; + Strm.WriteUInt32(SectionSize); + + Strm.Position = ChunkStart + AfterEntries; + Strm.PadTo(16, 0xAB); + SectionCount++; + } + + void WriteTXT2() + { + Strm.WriteUInt32(0x54585432); // TXT2 + long SectionStart = Strm.Position; + Strm.Position += 0xC; + long ChunkStart = Strm.Position; + int messageCount = Messages.Count; + Strm.WriteUInt32((uint)messageCount); + + uint TextSize = 0; + long curPos; + for (int i = 0; i < messageCount; i++) + { + Message Current = Messages[i]; + uint StringOffset = (uint)(4 + messageCount * 4 + TextSize); // offsets are relative to ChunkStart + Strm.WriteUInt32(StringOffset); + + curPos = Strm.Position; + Strm.Position = ChunkStart + StringOffset; + long before = Strm.Position; + Current.WriteToBinary(Strm, TextEncoding); + long after = Strm.Position; + + uint written = (uint)(after - before); // how many bytes this message used + TextSize += written; + + Strm.Position = curPos; + } + + // section size = 4 (count) + 4*messageCount (offsets) + TextSize + uint sectionSize = (uint)(4 + 4 * messageCount + TextSize); + long afterEntries = ChunkStart + sectionSize; + Strm.Position = SectionStart; + Strm.WriteUInt32(sectionSize); + + Strm.Position = ChunkStart + sectionSize; + Strm.PadTo(16, 0xAB); + SectionCount++; + } + + } public Message? FindByLabel(string Label) { @@ -182,6 +377,14 @@ public void Save(Stream Strm) public ushort IndexOf(Message message) => (ushort)Messages.IndexOf(message); + public struct LabelEntry + { + public uint numStrings; + public int[] indexes; + public string[] strings; + } + + public List LabelEntries { get; set; } = new(); public class Message { @@ -208,7 +411,7 @@ public string Content public Attribute Attributes { get => mAttributes; - set => mAttributes = value; + set => mAttributes = value; } @@ -270,6 +473,111 @@ internal static string ReadTag(Stream Strm, Encoding Enc) return $"[{Group}:{TagId};{d}]"; } + internal void WriteToBinary(Stream Strm, Encoding Enc) + { + for (int i = 0; i < mContent.Length; i++) + { + // guard against lookahead out of range + if (mContent[i] == '\\' && i + 1 < mContent.Length && mContent[i + 1] == '[') + { + Strm.WriteString("[", Enc); + i++; + continue; + } + if (mContent[i] == '\\' && i + 1 < mContent.Length && mContent[i + 1] == ']') + { + Strm.WriteString("]", Enc); + i++; + continue; + } + if (mContent[i] == '\\' && i + 1 < mContent.Length && mContent[i + 1] == '\\') + { + Strm.WriteString("\\", Enc); + i++; + continue; + } + + if (mContent[i] == '[') + { + // WriteTag returns the number of characters consumed (including brackets) + int consumed = WriteTag(mContent, i, Strm, Enc); + if (consumed <= 0) + throw new InvalidOperationException("WriteTag failed to consume characters"); + // the for-loop will increment i by 1; compensate: + i += consumed - 1; + continue; + } + + Strm.WriteString(mContent[i].ToString(), Enc, null, !StreamUtil.GetCurrentEndian()); + } + + Strm.WriteString("\0", Enc, null); + } + + internal static int WriteTag(string Content, int StartIndex, Stream Strm, Encoding Enc) + { + // StartIndex should point at '[' + if (StartIndex >= Content.Length || Content[StartIndex] != '[') + throw new ArgumentException("StartIndex must point at '['"); + + int endIndex = Content.IndexOf(']', StartIndex + 1); + if (endIndex == -1) + throw new InvalidOperationException("Tag is missing closing ']'"); + + // extract inside of brackets (without the [ ]) + string inside = Content.Substring(StartIndex + 1, endIndex - (StartIndex + 1)); + // expected format "Group:TagId;HEXDATA" + string[] parts = inside.Split(new char[] { ':', ';' }, StringSplitOptions.None); + if (parts.Length < 3) + throw new InvalidOperationException($"Invalid tag format: {inside}"); + + ushort group = Convert.ToUInt16(parts[0]); + ushort tagId = Convert.ToUInt16(parts[1]); + string hex = parts[2]; + + if (hex.Length % 2 != 0) + throw new InvalidOperationException("Tag hex data length must be even"); + + ushort size = (ushort)(hex.Length / 2); + + Strm.WriteUInt16(0x0E); + + // write group, tagId, size as UInt16 respecting current endianness (use your helpers) + Strm.WriteUInt16(group); + Strm.WriteUInt16(tagId); + Strm.WriteUInt16(size); + + if (group == 2) + { + // write the data bytes as they are (no endian inversion for single bytes) + for (int i = 1; i >= 0; i--) + { + string bs = hex.Substring(i * 2, 2); + byte b = Convert.ToByte(bs, 16); + Strm.WriteByte(b); + } + for (int i = 2; i < size; i++) + { + string bs = hex.Substring(i * 2, 2); + byte b = Convert.ToByte(bs, 16); + Strm.WriteByte(b); + } + } + else + { + // write the data bytes as they are (no endian inversion for single bytes) + for (int i = size - 1; i >= 0; i--) + { + string bs = hex.Substring(i * 2, 2); + byte b = Convert.ToByte(bs, 16); + Strm.WriteByte(b); + } + } + + // total consumed characters including '[' and ']' + return (endIndex - StartIndex + 1); + } + public override string ToString() => $"{mLabel}: {mContent}"; } @@ -287,6 +595,7 @@ public struct Attribute public byte AlreadyTalked; [DisallowNull] public string Comment; + public string? Unknown; public Attribute() { diff --git a/Hack.io.RARC/RARC.cs b/Hack.io.RARC/RARC.cs index d39940b..387a092 100644 --- a/Hack.io.RARC/RARC.cs +++ b/Hack.io.RARC/RARC.cs @@ -17,6 +17,10 @@ public class RARC : Archive #region Properties /// + /// Specifies whether the RARC is in Big Endian (true) or Little Endian (false) + /// + public bool IsBigEndian { get; set; } + /// /// If false, the user must set all unique ID's for each file /// public bool KeepFileIDsSynced { get; set; } = true; @@ -68,7 +72,19 @@ protected override void OnItemSet(object? value, string Path) protected override void Read(Stream Strm) { #region Header - FileUtil.ExceptionOnBadMagic(Strm, MAGIC); + FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); + Strm.Position = 0; + uint magic = Strm.ReadUInt32(); + if (magic == 0x43524152) // CRAR + { + StreamUtil.SetEndianLittle(); + IsBigEndian = false; + } + else if (magic == 0x52415243) // RARC + { + StreamUtil.SetEndianBig(); + IsBigEndian = true; + } uint FileSize = Strm.ReadUInt32(), DataHeaderOffset = Strm.ReadUInt32(), @@ -104,13 +120,25 @@ protected override void Read(Stream Strm) Strm.Seek(FileEntryTableOffset, SeekOrigin.Begin); for (int i = 0; i < FileEntryCount; i++) { - FlatFileList.Add(new RARCFileEntry() + ushort CurrentNameOffset; + if (IsBigEndian == true) { - FileID = Strm.ReadInt16(), - NameHash = Strm.ReadInt16(), - Type = Strm.ReadInt16() - }); - ushort CurrentNameOffset = Strm.ReadUInt16(); + RARCFileEntry fileEntry = new(); + fileEntry.FileID = Strm.ReadInt16(); + fileEntry.NameHash = Strm.ReadInt16(); + fileEntry.Type = Strm.ReadInt16(); + CurrentNameOffset = Strm.ReadUInt16(); + FlatFileList.Add(fileEntry); + } + else + { + RARCFileEntry fileEntry = new(); + fileEntry.FileID = Strm.ReadInt16(); + fileEntry.NameHash = Strm.ReadInt16(); + CurrentNameOffset = Strm.ReadUInt16(); + fileEntry.Type = Strm.ReadInt16(); + FlatFileList.Add(fileEntry); + } FlatFileList[^1].ModularA = Strm.ReadInt32(); FlatFileList[^1].ModularB = Strm.ReadInt32(); Strm.Position += 0x04; @@ -175,24 +203,39 @@ protected override void Write(Stream Strm) List FlatFileList = GetFlatFileList(Root, FileOffsets, ref FileID, 0, ref NextFolderID, -1); uint FirstFileOffset = 0; List FlatDirectoryList = GetFlatDirectoryList(Root, ref FirstFileOffset); - FlatDirectoryList.Insert(0, new RARCDirEntry() { FileCount = (ushort)(Root.Items.Count + 2), FirstFileOffset = 0, Name = Root.Name, NameHash = StringToHash(Root.Name), NameOffset = 0, Type = "ROOT" }); + + RARCDirEntry root = new RARCDirEntry(); + root.FileCount = (ushort)(Root.Items.Count + 2); + root.FirstFileOffset = 0; + root.Name = Root.Name; + root.NameHash = StringToHash(Root.Name); + root.NameOffset = 0; + if (IsBigEndian == true) + root.Type = "ROOT"; + else + root.Type = "TOOR"; + FlatDirectoryList.Insert(0, root); + Dictionary StringLocations = new(); byte[] StringDataBuffer = GetStringTableBytes(FlatFileList, Root.Name, ref StringLocations).ToArray(); #region File Writing long StartPosition = Strm.Position; - Strm.WriteString(MAGIC, Encoding.ASCII, null); - Strm.Write(new byte[16] { 0xDD, 0xDD, 0xDD, 0xDD, 0x00, 0x00, 0x00, 0x20, 0xDD, 0xDD, 0xDD, 0xDD, 0xEE, 0xEE, 0xEE, 0xEE }, 0, 16); + Strm.WriteUInt32(0x52415243); // RARC + Strm.WriteUInt32(0xDDDDDDDD); // Placeholder + Strm.WriteUInt32(0x20); + Strm.WriteUInt32(0xDDDDDDDD); // Placeholder + Strm.WriteUInt32(0xEEEEEEEE); // Placeholder Strm.WriteUInt32(MRAMSize); Strm.WriteUInt32(ARAMSize); Strm.WriteUInt32(DVDSize); //Data Header Strm.WriteInt32(FlatDirectoryList.Count); - Strm.Write(new byte[4] { 0xDD, 0xDD, 0xDD, 0xDD }, 0, 4); //Directory Nodes Location (-0x20) + Strm.WriteUInt32(0xDDDDDDDD); //Directory Nodes Location (-0x20) Strm.WriteInt32(FlatFileList.Count); - Strm.Write(new byte[4] { 0xDD, 0xDD, 0xDD, 0xDD }, 0, 4); //File Entries Location (-0x20) - Strm.Write(new byte[4] { 0xEE, 0xEE, 0xEE, 0xEE }, 0, 4); //String Table Size - Strm.Write(new byte[4] { 0xEE, 0xEE, 0xEE, 0xEE }, 0, 4); //string Table Location (-0x20) + Strm.WriteUInt32(0xDDDDDDDD); //File Entries Location (-0x20) + Strm.WriteUInt32(0xEEEEEEEE); //String Table Size + Strm.WriteUInt32(0xEEEEEEEE); //string Table Location (-0x20) Strm.WriteUInt16((ushort)FlatFileList.Count); Strm.WriteByte((byte)(KeepFileIDsSynced ? 0x01 : 0x00)); Strm.Write(new byte[5], 0, 5); @@ -217,11 +260,19 @@ protected override void Write(Stream Strm) { Strm.WriteInt16(FlatFileList[i].FileID); Strm.WriteUInt16(StringToHash(FlatFileList[i].Name)); - Strm.WriteInt16(FlatFileList[i].Type); - Strm.WriteUInt16((ushort)StringLocations[FlatFileList[i].Name]); + if (IsBigEndian == true) + { + Strm.WriteInt16(FlatFileList[i].Type); + Strm.WriteUInt16((ushort)StringLocations[FlatFileList[i].Name]); + } + else + { + Strm.WriteUInt16((ushort)StringLocations[FlatFileList[i].Name]); + Strm.WriteInt16(FlatFileList[i].Type); + } Strm.WriteInt32(FlatFileList[i].ModularA); Strm.WriteInt32(FlatFileList[i].ModularB); - Strm.Write(new byte[4], 0, 4); + Strm.WriteUInt32(0); } Strm.PadTo(32); #endregion @@ -568,7 +619,11 @@ internal Directory(RARC Owner, int ID, List DirectoryNodeList, Lis /// protected override ArchiveFile NewFile() => new File(); - + /// + /// Whether a given directory is equal to this one. + /// + /// The other directory. + /// public override bool Equals(object? obj) { if (obj is not Directory OtherDir) @@ -616,10 +671,15 @@ internal File(RARCFileEntry entry, uint DataBlockStart, Stream RARCFile) FileData = new byte[entry.ModularB]; RARCFile.Read(FileData); } - + /// public override string ToString() => $"{ID} - {Name} ({FileSettings}) [0x{FileData?.Length ?? 0:X8}]"; + /// + /// Whether a given file is equal to this one. + /// + /// The other directory. + /// public override bool Equals(object? obj) { return obj is File OtherFile && diff --git a/Hack.io/Utility/FileUtil.cs b/Hack.io/Utility/FileUtil.cs index a1ba71d..b34302b 100644 --- a/Hack.io/Utility/FileUtil.cs +++ b/Hack.io/Utility/FileUtil.cs @@ -142,18 +142,35 @@ public static void ExceptionOnBadMagic(Stream Strm, ReadOnlySpan Magic, En } /// - /// throws an exception if the current Endian mode of Hack.io does not match the one present in the stream's BOM + /// throws an exception if the current stream position does not contain the requested magic /// - /// - public static void ExceptionOnMisMatchedBOM(Stream Strm) + /// The stream to check + /// The magic to check for + /// Whether to check for both endians + /// + public static void ExceptionOnBadMagic(Stream Strm, ReadOnlySpan Magic, bool BothEndians = false) + { + if (!Strm.IsMagicMatch(Magic, BothEndians)) + throw new BadImageFormatException($"Invalid Magic. Expected \"{Magic.ToString()}\""); + } + /// + public static void ExceptionOnBadMagic(Stream Strm, ReadOnlySpan Magic, bool BothEndians = false) + { + if (!Strm.IsMagicMatch(Magic, BothEndians)) + throw new BadImageFormatException($"Invalid Magic. Expected \"{Magic.ToString()}\""); + } + /// + /// throws an exception if the current stream position does not contain the requested magic + /// + /// The stream to check + /// The magic to check for + /// The encoding to read the stream with + /// The alternative magic to check for + /// + public static void ExceptionOnBadMagic(Stream Strm, ReadOnlySpan Magic, Encoding Enc, ReadOnlySpan AlternativeMagic) { - byte[] Raw = new byte[2]; - Strm.Read(Raw); - ushort BOM = BitConverter.ToUInt16(Raw); - if (StreamUtil.GetCurrentEndian() && BOM != 0xFFFE) - throw new InvalidOperationException("File BOM does not match Hack.io's active Endian"); - else if (!StreamUtil.GetCurrentEndian() && BOM != 0xFEFF) - throw new InvalidOperationException("File BOM does not match Hack.io's active Endian"); + if (!Strm.IsMagicMatch(Magic, Enc) && !Strm.IsMagicMatch(AlternativeMagic, Enc)) + throw new BadImageFormatException($"Invalid Magic. Expected \"{Magic}\" or \"{AlternativeMagic}\""); } /// diff --git a/Hack.io/Utility/StreamUtil.cs b/Hack.io/Utility/StreamUtil.cs index d0fd27b..8818431 100644 --- a/Hack.io/Utility/StreamUtil.cs +++ b/Hack.io/Utility/StreamUtil.cs @@ -56,6 +56,20 @@ public static void ApplyEndian(Span data, bool Invert = false) if (!Invert) data.Reverse(); } + /// + /// Applies the endian on a given value using bit math. + /// + /// Value to apply endian to + /// + public static long ApplyEndian(long value) + { + if (!UseBigEndian != BitConverter.IsLittleEndian) + return value; + return ((value & 0x000000FFU) << 24) | + ((value & 0x0000FF00U) << 8) | + ((value & 0x00FF0000U) >> 8) | + ((value & 0xFF000000U) >> 24); + } //==================================================================================================== @@ -530,33 +544,51 @@ public static string ReadString(this Stream Strm, int StringLength, Encoding Enc /// /// The stream to read /// The magic to check + /// Whether both endians of the magic should be checked for /// TRUE if the next bytes match the magic, FALSE otherwise. - public static bool IsMagicMatch(this Stream Strm, ReadOnlySpan Magic) + public static bool IsMagicMatch(this Stream Strm, ReadOnlySpan Magic, bool BothEndians = false) { Debug.Assert(Magic.Length is > 0 and < 16); Span read = stackalloc byte[Magic.Length]; //Should be fine since MAGIC's are typically only 4 bytes long. Strm.ReadExactly(read); ApplyEndian(read, true); - return read.SequenceEqual(Magic); + bool isMatch = read.SequenceEqual(Magic); + if (isMatch) + return true; + if (BothEndians) + { + ApplyEndian(read, false); + isMatch = read.SequenceEqual(Magic); + } + return isMatch; } - /// - public static bool IsMagicMatch(this Stream Strm, ReadOnlySpan Magic) => IsMagicMatch(Strm, Magic, Encoding.ASCII); + /// + public static bool IsMagicMatch(this Stream Strm, ReadOnlySpan Magic, bool BothEndians = false) => IsMagicMatch(Strm, Magic, Encoding.ASCII); /// /// Checks the stream for a given Magic identifier.Advances the Stream's Position forwards by Magic.Length /// /// The stream to read /// The magic to check /// The encoding that should be used when reading the file + /// Whether both endians of the magic should be checked for /// TRUE if the next bytes match the magic, FALSE otherwise. - public static bool IsMagicMatch(this Stream Strm, ReadOnlySpan Magic, Encoding Enc) + public static bool IsMagicMatch(this Stream Strm, ReadOnlySpan Magic, Encoding Enc, bool BothEndians = false) { Debug.Assert(Magic.Length is > 0 and < 16); string str = Strm.ReadString(Magic.Length, Enc); Span to = new(str.ToCharArray()); ApplyEndian(to, true); - return to.SequenceEqual(Magic); + bool isMatch = to.SequenceEqual(Magic); + if (isMatch) + return true; + if (BothEndians) + { + ApplyEndian(to, false); + isMatch = to.SequenceEqual(Magic); + } + return isMatch; } //==================================================================================================== @@ -773,9 +805,12 @@ public static void WriteVariableLength(this Stream Strm, long value) /// The string to write /// The encoding to write the string in /// The terminator byte. Set to NULL to dsiable termination (for MAGICs and whatnot) - public static void WriteString(this Stream Strm, string String, Encoding Enc, byte? Terminator = 0x00) + /// Whether to reverse the string or not + public static void WriteString(this Stream Strm, string String, Encoding Enc, byte? Terminator = 0x00, bool ApplyEndian = false) { byte[] Write = Enc.GetBytes(String); + if (ApplyEndian) + Array.Reverse(Write); Strm.Write(Write, 0, Write.Length); if (Terminator is not null) Strm.WriteByte(Terminator.Value); From 982e297240f948b26e6bc153832a247e37552937 Mon Sep 17 00:00:00 2001 From: Bavario Date: Sat, 18 Oct 2025 12:47:59 +0200 Subject: [PATCH 3/4] Remove old KCL funcs --- Hack.io.KCL/KCL.cs | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/Hack.io.KCL/KCL.cs b/Hack.io.KCL/KCL.cs index 24a168c..03e7e07 100644 --- a/Hack.io.KCL/KCL.cs +++ b/Hack.io.KCL/KCL.cs @@ -818,42 +818,6 @@ public PaEntry(Dictionary FieldSource) Init(); } private void Init() => CameraID = 0xFF; - // !!! - /*public static void ConvertBCSV(ref BCSV.BCSV x) - { - for (int i = 0; i < x.EntryCount; i++) - { - if (x[i] is PaEntry) - continue; - - BCSV.BCSV.Entry entry = x[i]; - PaEntry newentry = new PaEntry - { - Data = entry.Data - }; - x[i] = newentry; - } - } - - public static BCSV.BCSV CreateBCSV(WavefrontObj obj) - { - BCSV.BCSV bcsv = new BCSV.BCSV(); - - BCSV.BCSV.Field Cam = new BCSV.BCSV.Field(CAMERA_ID, BCSV.BCSV.DataTypes.INT32, CAMERA_ID_MASK, CAMERA_ID_SHIFT, false); - BCSV.BCSV.Field Sound = new BCSV.BCSV.Field(SOUND_CODE, BCSV.BCSV.DataTypes.INT32, SOUND_CODE_MASK, SOUND_CODE_SHIFT, false); - BCSV.BCSV.Field Floor = new BCSV.BCSV.Field(FLOOR_CODE, BCSV.BCSV.DataTypes.INT32, FLOOR_CODE_MASK, FLOOR_CODE_SHIFT, false); - BCSV.BCSV.Field Wall = new BCSV.BCSV.Field(WALL_CODE, BCSV.BCSV.DataTypes.INT32, WALL_CODE_MASK, WALL_CODE_SHIFT, false); - BCSV.BCSV.Field CamCol = new BCSV.BCSV.Field(CAMERA_THROUGH, BCSV.BCSV.DataTypes.INT32, CAMERA_THROUGH_MASK, CAMERA_THROUGH_SHIFT, false); - - bcsv.Add(Cam, Sound, Floor, Wall, CamCol); - - for (int i = 0; i < obj.GroupNames.Count; i++) - { - bcsv.Add(new PaEntry()); - } - - return bcsv; - }*/ } } From 260a637ec5e3d110dc310a4738af14c83b2a3bfa Mon Sep 17 00:00:00 2001 From: Bavario Date: Sat, 18 Oct 2025 18:34:38 +0200 Subject: [PATCH 4/4] Make Magic checks integer-based --- Hack.io.BCK/BCK.cs | 12 +++---- Hack.io.BPK/BPK.cs | 12 +++---- Hack.io.BRK/BRK.cs | 12 +++---- Hack.io.BTK/BTK.cs | 12 +++---- Hack.io.BTP/BTP.cs | 12 +++---- Hack.io.BVA/BVA.cs | 12 +++---- Hack.io.MSBF/MSBF.cs | 25 ++++++--------- Hack.io.MSBT/MSBT.cs | 25 +++++++-------- Hack.io.RARC/RARC.cs | 4 ++- Hack.io.U8/U8.cs | 6 ++-- Hack.io/Utility/FileUtil.cs | 62 ++++++++++++------------------------- 11 files changed, 81 insertions(+), 113 deletions(-) diff --git a/Hack.io.BCK/BCK.cs b/Hack.io.BCK/BCK.cs index 82fccb9..05caf53 100644 --- a/Hack.io.BCK/BCK.cs +++ b/Hack.io.BCK/BCK.cs @@ -9,9 +9,9 @@ namespace Hack.io.BCK; public class BCK : J3DAnimationBase, ILoadSaveFile { /// - public const string MAGIC = "J3D1bck1"; + public const uint MAGIC = 0x62636B31; // bck1 /// - public const string CHUNKMAGIC = "ANK1"; + public const uint CHUNKMAGIC = 0x414E4B31; /// /// Rotational Multiplier. @@ -28,7 +28,7 @@ public class BCK : J3DAnimationBase, ILoadSaveFile public void Load(Stream Strm) { uint StartPosition = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); + FileUtil.ExceptionOnBadJ3DMagic(Strm, MAGIC); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.Position += 0x0C; //Strm.ReadJ3DSubVersion(); //This is not used the same way the other formats are @@ -36,7 +36,7 @@ public void Load(Stream Strm) //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); RotationMultiplier = (sbyte)Strm.ReadByte(); @@ -103,13 +103,13 @@ public void Save(Stream Strm) long Start = Strm.Position; Strm.WriteUInt32(0x4A334431); // J3D1 - Strm.WriteUInt32(0x62636B31); // bck1 + Strm.WriteUInt32(MAGIC); // bck1 Strm.WritePlaceholder(4); //FileSize Strm.WriteUInt32(1); // ChunkCount Strm.Write(CollectionUtil.InitilizeArray((byte)0xFF, 0x10)); long ChunkStart = Strm.Position; - Strm.WriteUInt32(0x414E4B31); // ANK1 + Strm.WriteUInt32(CHUNKMAGIC); // ANK1 Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.WriteByte((byte)RotationMultiplier); diff --git a/Hack.io.BPK/BPK.cs b/Hack.io.BPK/BPK.cs index 4cbc36f..4e49d05 100644 --- a/Hack.io.BPK/BPK.cs +++ b/Hack.io.BPK/BPK.cs @@ -13,21 +13,21 @@ namespace Hack.io.BPK; public class BPK : J3DAnimationBase, ILoadSaveFile { /// - public const string MAGIC = "J3D1bpk1"; + public const uint MAGIC = 0x62706B31; /// - public const string CHUNKMAGIC = "PAK1"; + public const uint CHUNKMAGIC = 0x50414B31; /// public void Load(Stream Strm) { - FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); + FileUtil.ExceptionOnBadJ3DMagic(Strm, MAGIC); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.ReadJ3DSubVersion(); //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); Strm.Position+=0x03; //Padding 0xFF @@ -80,13 +80,13 @@ public void Save(Stream Strm) { long Start = Strm.Position; Strm.WriteUInt32(0x4A334431); // J3D1 - Strm.WriteUInt32(0x62706B31); // bpk1 + Strm.WriteUInt32(MAGIC); Strm.WritePlaceholder(4); //FileSize Strm.WriteUInt32(1); // ChunkCount Strm.WriteJ3DSubVersion(); long ChunkStart = Strm.Position; - Strm.WriteUInt32(0x50414B31); // PAK1 + Strm.WriteUInt32(CHUNKMAGIC); Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.PadTo(0x04, 0xFF); //Padding diff --git a/Hack.io.BRK/BRK.cs b/Hack.io.BRK/BRK.cs index bf451a6..b0b1b40 100644 --- a/Hack.io.BRK/BRK.cs +++ b/Hack.io.BRK/BRK.cs @@ -13,21 +13,21 @@ namespace Hack.io.BRK; public class BRK : J3DAnimationBase, ILoadSaveFile { /// - public const string MAGIC = "J3D1brk1"; + public const uint MAGIC = 0x62726B31; /// - public const string CHUNKMAGIC = "TRK1"; + public const uint CHUNKMAGIC = 0x54524B31; /// public void Load(Stream Strm) { - FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); + FileUtil.ExceptionOnBadJ3DMagic(Strm, MAGIC); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.ReadJ3DSubVersion(); //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); Strm.Position++; //Padding 0xFF @@ -151,13 +151,13 @@ public void Save(Stream Strm) long Start = Strm.Position; Strm.WriteUInt32(0x4A334431); // J3D1 - Strm.WriteUInt32(0x62726B31); // brk1 + Strm.WriteUInt32(MAGIC); Strm.WritePlaceholder(4); //FileSize Strm.WriteUInt32(1); Strm.WriteJ3DSubVersion(); long ChunkStart = Strm.Position; - Strm.WriteUInt32(0x54524B31); // TRK1 + Strm.WriteUInt32(CHUNKMAGIC); Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.WriteByte(0xFF); //Padding diff --git a/Hack.io.BTK/BTK.cs b/Hack.io.BTK/BTK.cs index 38fde2c..29328d5 100644 --- a/Hack.io.BTK/BTK.cs +++ b/Hack.io.BTK/BTK.cs @@ -13,9 +13,9 @@ namespace Hack.io.BTK; public class BTK : J3DAnimationBase, ILoadSaveFile { /// - public const string MAGIC = "J3D1btk1"; + public const uint MAGIC = 0x62746B31; /// - public const string CHUNKMAGIC = "TTK1"; + public const uint CHUNKMAGIC = 0x54544B31; /// /// If true, uses Maya math instead of normal J3D Math @@ -30,14 +30,14 @@ public class BTK : J3DAnimationBase, ILoadSaveFile /// public void Load(Stream Strm) { - FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); + FileUtil.ExceptionOnBadJ3DMagic(Strm, MAGIC); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.ReadJ3DSubVersion(); //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); RotationMultiplier = (sbyte)Strm.ReadByte(); @@ -108,13 +108,13 @@ public void Save(Stream Strm) long Start = Strm.Position; Strm.WriteUInt32(0x4A334431); // J3D1 - Strm.WriteUInt32(0x62746B31); // btk1 + Strm.WriteUInt32(MAGIC); Strm.WritePlaceholder(4); //FileSize Strm.WriteUInt32(1); // ChunkCount Strm.WriteJ3DSubVersion(); long ChunkStart = Strm.Position; - Strm.WriteUInt32(0x54544B31); // TTK1 + Strm.WriteUInt32(CHUNKMAGIC); Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.WriteByte((byte)RotationMultiplier); diff --git a/Hack.io.BTP/BTP.cs b/Hack.io.BTP/BTP.cs index 2ae1868..0f61837 100644 --- a/Hack.io.BTP/BTP.cs +++ b/Hack.io.BTP/BTP.cs @@ -10,20 +10,20 @@ namespace Hack.io.BTP; public class BTP : J3DAnimationBase, ILoadSaveFile { /// - public const string MAGIC = "J3D1btp1"; + public const uint MAGIC = 0x62747031; /// - public const string CHUNKMAGIC = "TPT1"; + public const uint CHUNKMAGIC = 0x54505431; public void Load(Stream Strm) { - FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); + FileUtil.ExceptionOnBadJ3DMagic(Strm, MAGIC); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.Position += 0x10; //Strm.ReadJ3DSubVersion(); //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); Strm.Position++; //Padding 0xFF @@ -63,13 +63,13 @@ public void Save(Stream Strm) { long Start = Strm.Position; Strm.WriteUInt32(0x4A334431); // J3D1 - Strm.WriteUInt32(0x62747031); // btp1 + Strm.WriteUInt32(MAGIC); Strm.WritePlaceholder(4); //FileSize Strm.WriteUInt32(1); // ChunkCount Strm.Write(CollectionUtil.InitilizeArray((byte)0xFF, 0x10)); long ChunkStart = Strm.Position; - Strm.WriteUInt32(0x54505431); // TPT1 + Strm.WriteUInt32(CHUNKMAGIC); Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.WriteByte(0xFF); diff --git a/Hack.io.BVA/BVA.cs b/Hack.io.BVA/BVA.cs index 71ddd63..d556750 100644 --- a/Hack.io.BVA/BVA.cs +++ b/Hack.io.BVA/BVA.cs @@ -9,20 +9,20 @@ namespace Hack.io.BVA; public class BVA : J3DAnimationBase, ILoadSaveFile { /// - public const string MAGIC = "J3D1bva1"; + public const uint MAGIC = 0x62766131; /// - public const string CHUNKMAGIC = "VAF1"; + public const uint CHUNKMAGIC = 0x56414631; public void Load(Stream Strm) { - FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); + FileUtil.ExceptionOnBadJ3DMagic(Strm, MAGIC); uint FileSize = Strm.ReadUInt32(), ChunkCount = Strm.ReadUInt32(); Strm.Position += 0x10; //Strm.ReadJ3DSubVersion(); //Only 1 chunk is supported uint ChunkStart = (uint)Strm.Position; - FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC, true); + FileUtil.ExceptionOnBadMagic(Strm, CHUNKMAGIC); uint ChunkSize = Strm.ReadUInt32(); Loop = Strm.ReadEnum(StreamUtil.ReadUInt8); Strm.Position++; //Padding 0xFF @@ -63,13 +63,13 @@ public void Save(Stream Strm) { long Start = Strm.Position; Strm.WriteUInt32(0x4A334431); // J3D1 - Strm.WriteUInt32(0x62766131); // bva1 + Strm.WriteUInt32(MAGIC); Strm.WritePlaceholder(4); //FileSize Strm.WriteUInt32(1); //ChunkCount Strm.Write(CollectionUtil.InitilizeArray((byte)0xFF, 0x10)); long ChunkStart = Strm.Position; - Strm.WriteUInt32(0x56414631); // VAF1 + Strm.WriteUInt32(CHUNKMAGIC); Strm.WritePlaceholder(4); //ChunkSize Strm.WriteByte((byte)Loop); Strm.WriteByte(0xFF); diff --git a/Hack.io.MSBF/MSBF.cs b/Hack.io.MSBF/MSBF.cs index 38a23db..53b1827 100644 --- a/Hack.io.MSBF/MSBF.cs +++ b/Hack.io.MSBF/MSBF.cs @@ -13,12 +13,9 @@ public class MSBF : ILoadSaveFile public const int LABEL_MAX_LENGTH = 255; /// public const string MAGIC = "MsgFlwBn"; - public const string MAGIC_FLW2 = "FLW2"; - public const string MAGIC_FEN1 = "FEN1"; - public const string MAGIC_REF1 = "REF1"; - public const string MAGIC_FLW2_LE = "2WLF"; - public const string MAGIC_FEN1_LE = "1NEF"; - public const string MAGIC_REF1_LE = "1FER"; + public const uint MAGIC_FLW2 = 0x464C5732; + public const uint MAGIC_FEN1 = 0x46454E31; + public const uint MAGIC_REF1 = 0x52454631; [DisallowNull] public List Flows = []; @@ -57,15 +54,15 @@ public void Load(Stream Strm) for (int i = 0; i < SectionCount; i++) { long ChunkStart = Strm.Position; - string Header = Strm.ReadString(4, Encoding.ASCII); + uint Header = Strm.ReadUInt32(); uint ChunkSize = Strm.ReadUInt32(); Strm.Position += 0x08; - if (Header.Equals(MAGIC_FLW2) || Header.Equals(MAGIC_FLW2_LE)) + if (Header.Equals(MAGIC_FLW2)) ReadFLW2(); - if (Header.Equals(MAGIC_FEN1) || Header.Equals(MAGIC_FEN1_LE)) + if (Header.Equals(MAGIC_FEN1)) ReadFEN1(); - if (Header.Equals(MAGIC_REF1) || Header.Equals(MAGIC_REF1_LE)) + if (Header.Equals(MAGIC_REF1)) ReadREF1(); Strm.Position = ChunkStart + 0x10 + ChunkSize; @@ -185,10 +182,6 @@ public void Save(Stream Strm) Strm.WriteUInt16(0xFEFF); Strm.Write(CollectionUtil.InitilizeArray(0, 3)); Strm.WriteUInt8(3); //Version - Strm.WritePlaceholder(2); //Section Count - Strm.WriteUInt16(0); - Strm.WritePlaceholder(4); //Filesize - Strm.Write(CollectionUtil.InitilizeArray(0, 0x0A)); WriteFLW2(); WriteFEN1(); @@ -204,7 +197,7 @@ void WriteFLW2() GetFlattenedNodes(ref TemporaryNodes); long ChunkStart = Strm.Position; - Strm.WriteString(MAGIC_FLW2, Encoding.ASCII, null); + Strm.WriteUInt32(MAGIC_FLW2); Strm.WritePlaceholder(4); //Size Strm.Write(CollectionUtil.InitilizeArray(0, 0x08)); Strm.WriteUInt16((ushort)TemporaryNodes.Count); @@ -275,7 +268,7 @@ void WriteFEN1() Buckets.Add([]); long ChunkStart = Strm.Position; - Strm.WriteString(MAGIC_FEN1, Encoding.ASCII, null); + Strm.WriteUInt32(MAGIC_FEN1); Strm.WritePlaceholder(4); Strm.Write(CollectionUtil.InitilizeArray(0, 0x08)); diff --git a/Hack.io.MSBT/MSBT.cs b/Hack.io.MSBT/MSBT.cs index 2c6f155..2259d61 100644 --- a/Hack.io.MSBT/MSBT.cs +++ b/Hack.io.MSBT/MSBT.cs @@ -10,12 +10,9 @@ public class MSBT : ILoadSaveFile public const int LABEL_MAX_LENGTH = 255; /// public const string MAGIC = "MsgStdBn"; - public const string MAGIC_LBL1 = "LBL1"; - public const string MAGIC_ATR1 = "ATR1"; - public const string MAGIC_TXT2 = "TXT2"; - public const string MAGIC_LBL1_LE = "1LBL"; - public const string MAGIC_ATR1_LE = "1RTA"; - public const string MAGIC_TXT2_LE = "2TXT"; + public const uint MAGIC_LBL1 = 0x4C424C31; + public const uint MAGIC_ATR1 = 0x41545231; + public const uint MAGIC_TXT2 = 0x54585432; private Encoding mEncoding = Encoding.UTF8; public Encoding TextEncoding @@ -73,15 +70,15 @@ public void Load(Stream Strm) for (int i = 0; i < SectionCount; i++) { long ChunkStart = Strm.Position; - string Header = Strm.ReadString(4, Encoding.ASCII); + uint Header = Strm.ReadUInt32(); uint ChunkSize = Strm.ReadUInt32(); Strm.Position += 0x08; - if (Header.Equals(MAGIC_LBL1) || Header.Equals(MAGIC_LBL1_LE)) + if (Header.Equals(MAGIC_LBL1)) ReadLBL1(); - if (Header.Equals(MAGIC_ATR1) || Header.Equals(MAGIC_ATR1_LE)) + if (Header.Equals(MAGIC_ATR1)) ReadATR1(); - if (Header.Equals(MAGIC_TXT2) || Header.Equals(MAGIC_TXT2_LE)) + if (Header.Equals(MAGIC_TXT2)) ReadTXT2(); Strm.Position = ChunkStart + 0x10 + ChunkSize; @@ -194,7 +191,7 @@ void ReadTXT2() public void Save(Stream Strm) { - Strm.WriteString("MsgStdBn", Encoding.ASCII, null); + Strm.WriteString(MAGIC, Encoding.ASCII, null); Strm.WriteUInt16(0xFEFF); Strm.Position += 0x02; if (TextEncoding == Encoding.UTF8) @@ -232,7 +229,7 @@ public void Save(Stream Strm) void WriteLBL1() { - Strm.WriteUInt32(0x4C424C31); // LBL1 + Strm.WriteUInt32(MAGIC_LBL1); long SectionStart = Strm.Position; Strm.Position += 0xC; long ChunkStart = Strm.Position; @@ -270,7 +267,7 @@ void WriteLBL1() void WriteATR1() { - Strm.WriteUInt32(0x41545231); // ATR1 + Strm.WriteUInt32(MAGIC_ATR1); long SectionStart = Strm.Position; Strm.Position += 0xC; long ChunkStart = Strm.Position; @@ -325,7 +322,7 @@ void WriteATR1() void WriteTXT2() { - Strm.WriteUInt32(0x54585432); // TXT2 + Strm.WriteUInt32(MAGIC_TXT2); long SectionStart = Strm.Position; Strm.Position += 0xC; long ChunkStart = Strm.Position; diff --git a/Hack.io.RARC/RARC.cs b/Hack.io.RARC/RARC.cs index 387a092..c281929 100644 --- a/Hack.io.RARC/RARC.cs +++ b/Hack.io.RARC/RARC.cs @@ -72,7 +72,6 @@ protected override void OnItemSet(object? value, string Path) protected override void Read(Stream Strm) { #region Header - FileUtil.ExceptionOnBadMagic(Strm, MAGIC, true); Strm.Position = 0; uint magic = Strm.ReadUInt32(); if (magic == 0x43524152) // CRAR @@ -84,6 +83,9 @@ protected override void Read(Stream Strm) { StreamUtil.SetEndianBig(); IsBigEndian = true; + } else + { + throw new BadImageFormatException($"Invalid Magic. Expected \"RARC\" or \"CRAR\""); } uint FileSize = Strm.ReadUInt32(), diff --git a/Hack.io.U8/U8.cs b/Hack.io.U8/U8.cs index 05cddf2..3cce1a4 100644 --- a/Hack.io.U8/U8.cs +++ b/Hack.io.U8/U8.cs @@ -10,7 +10,7 @@ namespace Hack.io.U8; public class U8 : Archive { /// - public static byte[] MAGIC => [0x55, 0xAA, 0x38, 0x2D]; + public const uint MAGIC = 0x55AA382D; /// /// Create an empty U8 archive @@ -22,7 +22,7 @@ public U8() /// protected override void Read(Stream Strm) { - FileUtil.ExceptionOnBadMagic(Strm, MAGIC.AsSpan()); + FileUtil.ExceptionOnBadMagic(Strm, MAGIC); uint OffsetToNodeSection = Strm.ReadUInt32(); //usually 0x20 _ = Strm.ReadUInt32(); @@ -174,7 +174,7 @@ protected override void Write(Stream Strm) } //Write the Header - Strm.Write(MAGIC); + Strm.WriteUInt32(MAGIC); Strm.WriteInt32(0x20); Strm.WriteInt32(Nodes.Count * 0x0C + StringBytes.Count); Strm.WriteUInt32(DataOffset); diff --git a/Hack.io/Utility/FileUtil.cs b/Hack.io/Utility/FileUtil.cs index b34302b..5766f45 100644 --- a/Hack.io/Utility/FileUtil.cs +++ b/Hack.io/Utility/FileUtil.cs @@ -117,60 +117,36 @@ public static TResult RunForFileStream(string FilePath, FileMode Mode, /// The stream to check /// The magic to check for /// - public static void ExceptionOnBadMagic(Stream Strm, ReadOnlySpan Magic) + public static void ExceptionOnBadMagic(Stream Strm, uint Magic) { - if (!Strm.IsMagicMatch(Magic)) - throw new BadImageFormatException($"Invalid Magic. Expected \"{Magic.ToString()}\""); - } - /// - public static void ExceptionOnBadMagic(Stream Strm, ReadOnlySpan Magic) - { - if (!Strm.IsMagicMatch(Magic)) - throw new BadImageFormatException($"Invalid Magic. Expected \"{Magic}\""); - } - /// - /// throws an exception if the current stream position does not contain the requested magic - /// - /// The stream to check - /// The magic to check for - /// The encoding to read the stream with - /// - public static void ExceptionOnBadMagic(Stream Strm, ReadOnlySpan Magic, Encoding Enc) - { - if (!Strm.IsMagicMatch(Magic, Enc)) - throw new BadImageFormatException($"Invalid Magic. Expected \"{Magic}\""); + uint val = Strm.ReadUInt32(); + if (val != Magic) + throw new BadImageFormatException(String.Format("Invalid Magic. Expected 0x{0:X}", Magic)); } - /// - /// throws an exception if the current stream position does not contain the requested magic - /// - /// The stream to check - /// The magic to check for - /// Whether to check for both endians - /// - public static void ExceptionOnBadMagic(Stream Strm, ReadOnlySpan Magic, bool BothEndians = false) - { - if (!Strm.IsMagicMatch(Magic, BothEndians)) - throw new BadImageFormatException($"Invalid Magic. Expected \"{Magic.ToString()}\""); - } - /// - public static void ExceptionOnBadMagic(Stream Strm, ReadOnlySpan Magic, bool BothEndians = false) + /// + public static void ExceptionOnBadJ3DMagic(Stream Strm, uint Magic) { - if (!Strm.IsMagicMatch(Magic, BothEndians)) - throw new BadImageFormatException($"Invalid Magic. Expected \"{Magic.ToString()}\""); + uint j3dVersion = Strm.ReadUInt32(); + uint val = Strm.ReadUInt32(); + + if (j3dVersion != 0x4A334431 // J3D1 + || val != Magic) + throw new BadImageFormatException(String.Format("Invalid Magic. Expected 0x{0:X}{1:X}", 0x4A334431, Magic)); } + /// - /// throws an exception if the current stream position does not contain the requested magic + /// throws an exception if the current stream position does not contain the requested magic. + /// DOES NOT ACCOUNT FOR ENDIAN. /// /// The stream to check /// The magic to check for - /// The encoding to read the stream with - /// The alternative magic to check for /// - public static void ExceptionOnBadMagic(Stream Strm, ReadOnlySpan Magic, Encoding Enc, ReadOnlySpan AlternativeMagic) + public static void ExceptionOnBadMagic(Stream Strm, string Magic) { - if (!Strm.IsMagicMatch(Magic, Enc) && !Strm.IsMagicMatch(AlternativeMagic, Enc)) - throw new BadImageFormatException($"Invalid Magic. Expected \"{Magic}\" or \"{AlternativeMagic}\""); + string val = Strm.ReadString(Magic.Length, Encoding.ASCII); + if (val != Magic) + throw new BadImageFormatException(String.Format("Invalid Magic. Expected \"{0}\"", Magic)); } ///