diff --git a/PDTools.Files/Courses/PS2/GT3CourseParameters.cs b/PDTools.Files/Courses/PS2/GT3CourseParameters.cs new file mode 100644 index 00000000..07cc89ab --- /dev/null +++ b/PDTools.Files/Courses/PS2/GT3CourseParameters.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace PDTools.Files.Courses.PS2; + +/// +/// GT3 "22_CourseParameters" — AI driving-line data (racing/AI lines, the pit-lane +/// line, pit boxes). It is a sibling course file, NOT part of the runway file. +/// +/// Layout (little-endian): +/// 0x00 u32 totalSize (offset of the end of the last section) +/// 0x04 u32 count (section-table entry count; observed 8 for 7 sections) +/// 0x08 u32 0 +/// 0x0C u32[] section offsets (until the build string) +/// 0x28 ASCII "build MM/DD/YY HH:MM:SS\n" + padding up to the first section +/// <section>* each: u32 nodeCount, then nodeCount × 40-byte node +/// node = 10 × int32: [0]flag [1]X [2]Z [3]Y [4..9] partly decoded +/// +/// The pit-lane line is the section whose first node lies on the runway's +/// pitlane-flagged surface (tri+12 bit0). Coordinates are integers ≈ ×4000 the +/// runway's float metres (transform not yet calibrated). +/// +/// Read()/Write() are lossless: the header (TOC + build string + padding) and the +/// trailing alignment bytes are preserved verbatim, and the section offsets + +/// totalSize are recomputed on write so edited line sizes stay consistent. +/// +public class GT3CourseParameters +{ + public const int NodeSize = 40; // bytes per node (10 × int32) + public const int NodeInts = 10; + + /// Header field at 0x04 — a constant 8 on every observed file (NOT the + /// section count, which is always 7). Written verbatim into synthetic headers. + public const uint HeaderCountConst = 8; + + /// Offset where the first section begins (0x48 on every file): the TOC + /// (0x00..0x27) + the ASCII build string (0x28) + zero-pad up to here. + public const int FirstSectionOffset = 0x48; + + /// Every official file's total length is a multiple of 0x40 (the data ends + /// at totalSize, then zero-pad up to the next 0x40 boundary). + public const int FileAlignment = 0x40; + + /// + /// If > 1, zero-pads the output up to this byte alignment + /// AFTER (totalSize still marks the data end). 0 = write + /// exactly (preserves byte-identical round-trips of files read from disk, whose + /// already carries the original padding). Synthetic files + /// () set this to so the + /// engine/volume sees a correctly-aligned file — without it the file is short and + /// downstream data shifts (crash). + /// + public int PadToAlignment { get; set; } = 0; + + /// + /// Fixed-point scale between integer course coords and runway float metres + /// (12 fractional bits). The X axis is flipped vs the runway mesh. + /// Calibrated against apricot: racing line lands within ~6 m of the centerline. + /// + public const float WorldScale = 4096f; + + /// Convert a node's integer (X,Z) to runway-mesh world metres (X flipped). + public static (float x, float z) ToWorld(int courseX, int courseZ) + => (-courseX / WorldScale, courseZ / WorldScale); + + /// Raw header bytes [0, FirstSectionOffset): TOC + build string + padding. + public byte[] HeaderBlob { get; set; } = []; + + /// Raw bytes after the last section (alignment padding), preserved verbatim. + public byte[] TailBlob { get; set; } = []; + + public List Lines { get; set; } = []; + + /// One AI driving line / pit line / pit-box set: a list of 10-int32 nodes. + public class Line + { + public List Nodes { get; set; } = []; + + public int Count => Nodes.Count; + public int X(int i) => Nodes[i][1]; + public int Z(int i) => Nodes[i][2]; + public int Y(int i) => Nodes[i][3]; + } + + private static uint U32(byte[] d, int o) => BitConverter.ToUInt32(d, o); + + /// + /// Builds a from-scratch instance with a synthetic header (no donor file). The + /// caller fills (always 7 sections; empties are allowed) and + /// calls , which patches the section offsets + totalSize. The + /// header is the verified-regular layout: count=8 at 0x04, 7 offset slots + /// (0x0C..0x27, patched on write), an ASCII build MM/DD/YY HH:MM:SS\n string + /// at 0x28, zero-padded to the first section at 0x48. + /// + public static GT3CourseParameters CreateForSynthesis(DateTime buildTime) + { + var h = new byte[FirstSectionOffset]; + BitConverter.GetBytes(HeaderCountConst).CopyTo(h, 0x04); + + string build = "build " + buildTime.ToString("MM/dd/yy HH:mm:ss", + System.Globalization.CultureInfo.InvariantCulture) + "\n"; + byte[] bs = System.Text.Encoding.ASCII.GetBytes(build); + Array.Copy(bs, 0, h, 0x28, Math.Min(bs.Length, FirstSectionOffset - 0x28)); + + return new GT3CourseParameters { HeaderBlob = h, PadToAlignment = FileAlignment }; + } + + public static GT3CourseParameters Read(byte[] d) + { + var cp = new GT3CourseParameters(); + int totalSize = (int)U32(d, 0x00); + + // Section offsets run from 0x0C until a u32 stops looking like an ascending, + // in-range offset (the next field is the ASCII build string). + var offs = new List(); + for (int p = 0x0C; p + 4 <= d.Length; p += 4) + { + uint v = U32(d, p); + if (v < 0x0C || v > (uint)d.Length) break; + if (offs.Count > 0 && v <= (uint)offs[^1]) break; + offs.Add((int)v); + } + if (offs.Count == 0) + throw new InvalidDataException("CourseParameters: no section offsets found."); + + cp.HeaderBlob = d[..offs[0]]; + + foreach (int o in offs) + { + int nc = (int)U32(d, o); + var line = new Line(); + for (int k = 0; k < nc; k++) + { + int b = o + 4 + k * NodeSize; + var node = new int[NodeInts]; + for (int j = 0; j < NodeInts; j++) + node[j] = BitConverter.ToInt32(d, b + j * 4); + line.Nodes.Add(node); + } + cp.Lines.Add(line); + } + + cp.TailBlob = totalSize <= d.Length ? d[totalSize..] : []; + return cp; + } + + public byte[] Write() + { + var outp = new List(HeaderBlob); // offsets/totalSize patched below + var sectionOffsets = new List(Lines.Count); + + foreach (var line in Lines) + { + sectionOffsets.Add(outp.Count); + Append32(outp, (uint)line.Nodes.Count); + foreach (var node in line.Nodes) + for (int j = 0; j < NodeInts; j++) + Append32(outp, unchecked((uint)node[j])); + } + int totalSize = outp.Count; // data end (excludes tail/alignment padding) + outp.AddRange(TailBlob); + + // Zero-pad the file up to the alignment boundary (synthetic files only; read + // files keep PadToAlignment = 0 so their original TailBlob padding is exact). + if (PadToAlignment > 1) + { + int aligned = (outp.Count + PadToAlignment - 1) / PadToAlignment * PadToAlignment; + while (outp.Count < aligned) outp.Add(0); + } + + var arr = outp.ToArray(); + Patch32(arr, 0x00, (uint)totalSize); // totalSize (data end) + for (int i = 0; i < sectionOffsets.Count; i++) + Patch32(arr, 0x0C + i * 4, (uint)sectionOffsets[i]); // section offset table + return arr; + } + + /// Section index of the direction-independent reference line (S4). + public const int ReferenceSectionIndex = 4; + + /// + /// Reverse the driving direction of the AI lines, to match a reversed runway. + /// reverse-gtrw keeps the collision mesh byte-identical, so node positions (f1/f2) + /// and the f4 collision-node link stay valid — they simply travel with the reversed + /// nodes (f4 then runs the opposite way, e.g. 11..93 → 93..11). + /// + /// Per line the node ORDER is reversed and each node's f6 turn-direction term has its + /// SIGN flipped (a left bend becomes a right bend when you drive the other way; f6=0 + /// on synthesized/plain lines, so this is a no-op there). The reference line + /// () is left untouched — in PD's own forward/ + /// reverse pairs it is direction-independent (≈byte-identical fwd vs rev). + /// + /// Validated against apricot_f vs apricot_r: reversing the forward racing line lands + /// its new first node where the reverse file's first node is. The transform is its + /// own inverse (reversing twice restores the original). + /// + public void ReverseDirection() + { + for (int s = 0; s < Lines.Count; s++) + { + if (s == ReferenceSectionIndex) continue; // direction-independent reference + + var nodes = Lines[s].Nodes; + nodes.Reverse(); + foreach (var node in nodes) + node[6] = unchecked(-node[6]); // turn-direction sign flips + } + } + + private static void Append32(List l, uint v) + { + l.Add((byte)v); l.Add((byte)(v >> 8)); l.Add((byte)(v >> 16)); l.Add((byte)(v >> 24)); + } + + private static void Patch32(byte[] b, int o, uint v) + { + b[o] = (byte)v; b[o + 1] = (byte)(v >> 8); b[o + 2] = (byte)(v >> 16); b[o + 3] = (byte)(v >> 24); + } +} diff --git a/PDTools.Files/Courses/PS2/Runway/GT3GateRecord.cs b/PDTools.Files/Courses/PS2/Runway/GT3GateRecord.cs new file mode 100644 index 00000000..d87e6573 --- /dev/null +++ b/PDTools.Files/Courses/PS2/Runway/GT3GateRecord.cs @@ -0,0 +1,40 @@ +using Syroot.BinaryData; + +namespace PDTools.Files.Courses.PS2.Runway; + +/// +/// One 8-byte entry in the GTRW gate/sector table. +/// Fields: +/// SectorId – track zone: 0 = global origin, 1 = start/finish gate, 2 = main track +/// GateFlag – record type: -1 = sentinel (b/e of list), 0 = timing gate, 1 = normal +/// TrackV – V-coordinate (metres) of this record along the track +/// +public class GT3GateRecord +{ + public short SectorId { get; set; } + public short GateFlag { get; set; } + public float TrackV { get; set; } + + /// True when this record marks a timing-sector boundary (T1/T2/T3). + public bool IsTimingGate => GateFlag == 0; + + /// True when this is a sentinel entry (start/finish markers). + public bool IsSentinel => GateFlag == -1; + + public static GT3GateRecord FromStream(BinaryStream bs) + => new GT3GateRecord + { + SectorId = bs.ReadInt16(), + GateFlag = bs.ReadInt16(), + TrackV = bs.ReadSingle(), + }; + + public void ToStream(BinaryStream bs) + { + bs.WriteInt16(SectorId); + bs.WriteInt16(GateFlag); + bs.WriteSingle(TrackV); + } + + public static int GetSize() => 8; +} diff --git a/PDTools.Files/Courses/PS2/Runway/GT3RunwayData.cs b/PDTools.Files/Courses/PS2/Runway/GT3RunwayData.cs new file mode 100644 index 00000000..c62d551b --- /dev/null +++ b/PDTools.Files/Courses/PS2/Runway/GT3RunwayData.cs @@ -0,0 +1,601 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Syroot.BinaryData; + +namespace PDTools.Files.Courses.PS2.Runway; + +/// +/// GT3 (PS2) runway file — magic "GTRW", little-endian. +/// +/// Binary layout (all offsets relative to file start): +/// 0x00 uint32 Magic = 0x57525447 ("GTRW") +/// 0x04 uint32 Runtime ptr (always 0 on disk) +/// 0x08 uint32 Runtime ptr (always 0 on disk) +/// 0x0C uint32 RelocSize (file length − 16) +/// 0x10 uint32 Version/flags field A +/// 0x14 uint32 Version/flags field B +/// 0x18 float TrackV – total track length in metres +/// 0x1C float A1VCoord – V of the start/finish gate record (mirrors GateRecords a=1 entry) +/// 0x20 uint16 HeaderField20 (observed = 1, meaning unclear) +/// 0x22 uint16 GateRecordCount +/// 0x24 uint32 GateSectionOffset (always 0x60) +/// 0x28 uint32 SpawnCount +/// 0x2C uint32 SpawnSectionOffset +/// 0x30–0x5F 48 raw bytes (section counts, offsets, and flags whose meaning is partially known) +/// 0x60+ Gate records (GateRecordCount × 8 bytes) +/// SpawnOff+ Spawn positions (SpawnCount × 16 bytes) +/// … All remaining sections (checkpoint pairs, clusters, road mesh, BSP…) — opaque blob +/// +public class GT3RunwayData +{ + // ── Magic ──────────────────────────────────────────────────────────────── + public const uint MAGIC = 0x57525447u; // "GTRW" + + // ── Parsed header fields ───────────────────────────────────────────────── + public uint Field04 { get; set; } + public uint Field08 { get; set; } + + /// File length − 16. Patched to the actual output length on Write(). + public uint RelocSize { get; set; } + + public uint Field10 { get; set; } + public uint Field14 { get; set; } + + /// Total track length in metres. + public float TrackV { get; set; } + + /// + /// V-coordinate of the start/finish gate (the SectorId=1 sentinel record). + /// Near 0 for a forward file; near TrackV for a reversed file. + /// Updated automatically by ReverseDirection(). + /// + public float A1VCoord { get; set; } + + public ushort HeaderField20 { get; set; } + + /// Raw 48-byte blob covering header offsets 0x30–0x5F. + public byte[] OpaqueHeaderTail { get; set; } + + // ── Data lists ─────────────────────────────────────────────────────────── + + /// + /// Sector/timing-gate table. Each record carries a V-coordinate, a sector ID, + /// and a gate flag (−1 = sentinel, 0 = timing gate, 1 = normal checkpoint). + /// In the forward file the records are in ascending V order: + /// [0] SectorId=0, GateFlag=−1, V=0 — global origin sentinel + /// [1] SectorId=1, GateFlag=−1, V≈0 — start/finish gate sentinel + /// [2…N-1] SectorId=2, GateFlag=0 or 1 — main track (gates and normals) + /// + public List GateRecords { get; set; } = []; + + /// + /// Starting-grid positions. Same 16-byte {X,Y,Z,Rotation} layout as RNW4. + /// GTRW does not use packed config slots — gate V-coords live in GateRecords. + /// + public List SpawnPositions { get; set; } = []; + + /// + /// Raw bytes covering everything from the end of the spawn block to EOF. + /// Includes checkpoint pairs, cluster table, road mesh, BSP tree, etc. + /// Preserved verbatim on Write(). + /// + public byte[] TrailingData { get; set; } + + // ── Reading ────────────────────────────────────────────────────────────── + + public GT3RunwayData FromStream(Stream stream) + { + BinaryStream bs = new BinaryStream(stream); + long basePos = bs.Position; + + // ── Header ────────────────────────────────────────────────────────── + uint magic = bs.ReadUInt32(); // 0x00 + if (magic != MAGIC) + throw new InvalidDataException( + $"Not a GTRW file (magic 0x{magic:X8}; expected 0x{MAGIC:X8})."); + + Field04 = bs.ReadUInt32(); // 0x04 + Field08 = bs.ReadUInt32(); // 0x08 + RelocSize = bs.ReadUInt32(); // 0x0C + Field10 = bs.ReadUInt32(); // 0x10 + Field14 = bs.ReadUInt32(); // 0x14 + TrackV = bs.ReadSingle(); // 0x18 + A1VCoord = bs.ReadSingle(); // 0x1C + HeaderField20 = bs.ReadUInt16(); // 0x20 + int gateCount = bs.ReadUInt16(); // 0x22 + uint gateOff = bs.ReadUInt32(); // 0x24 (= 0x60) + uint spawnCount = bs.ReadUInt32(); // 0x28 + uint spawnOff = bs.ReadUInt32(); // 0x2C + + OpaqueHeaderTail = bs.ReadBytes(0x30); // 0x30–0x5F (48 bytes) + + // ── Gate records ───────────────────────────────────────────────────── + bs.Position = basePos + gateOff; + for (int i = 0; i < gateCount; i++) + GateRecords.Add(GT3GateRecord.FromStream(bs)); + + // ── Spawn positions ────────────────────────────────────────────────── + bs.Position = basePos + spawnOff; + for (int i = 0; i < (int)spawnCount; i++) + SpawnPositions.Add(RunwayStartingPosition.FromStream(bs)); + + // ── Trailing opaque data ───────────────────────────────────────────── + long trailingStart = basePos + spawnOff + spawnCount * RunwayStartingPosition.GetSize(); + bs.Position = trailingStart; + TrailingData = bs.ReadBytes((int)(stream.Length - bs.Position)); + + return this; + } + + // ── Gadget transplant ──────────────────────────────────────────────────── + + /// + /// Replaces this file's gadget section with the gadget section from a + /// reference reversed GTRW file. Gadgets define the physical checkpoint + /// trigger volumes (T1/T2/T3 gate beams and start-line stanchions) that + /// the game uses for timing detection. The reference file must be a + /// reversed variant of the same course so that world coordinates are valid. + /// + /// OpaqueHeaderTail byte indices (relative to file header offset 0x30): + /// [0x20] = header 0x50 = gadget_count (updated to reference count) + /// [0x24] = header 0x54 = gadget_offset (unchanged — same absolute start) + /// [0x2C] = header 0x5C = light_vfx_ptr (shifted by gadget size delta) + /// + /// + /// Replaces the gate/sector records from a reference reversed GTRW file. + /// The reference sectordata encodes the correct reversed-track V positions for the + /// a=1 finish-line sentinel (in the footer, not the header) and the T1/T2/T3 + /// checkpoint thresholds that match the reversed pitbox V-coordinate system. + /// + public void ApplyReferenceSectorData(Stream referenceStream) + { + referenceStream.Position = 0; + var refBytes = new byte[referenceStream.Length]; + referenceStream.ReadExactly(refBytes, 0, refBytes.Length); + + // Gate records start at 0x60; count is at header 0x22; each record is 8 bytes. + int refGateCount = BitConverter.ToUInt16(refBytes, 0x22); + float refTrackV = BitConverter.ToSingle(refBytes, 0x18); + const int kGateOff = 0x60; + const int kGateStride = 8; + + if (refGateCount <= 0 || kGateOff + refGateCount * kGateStride > refBytes.Length) return; + + GateRecords.Clear(); + for (int i = 0; i < refGateCount; i++) + { + int off = kGateOff + i * kGateStride; + var rec = new GT3GateRecord + { + SectorId = BitConverter.ToInt16(refBytes, off), + GateFlag = BitConverter.ToInt16(refBytes, off + 2), + TrackV = BitConverter.ToSingle(refBytes, off + 4), + }; + GateRecords.Add(rec); + } + + // Sync A1VCoord header field to the reference file's a=1 sentinel value. + foreach (var rec in GateRecords) + if (rec.IsSentinel && rec.SectorId == 1) { A1VCoord = rec.TrackV; break; } + } + + /// + /// Copies spawn grid positions (X/Z/Rotation) from a reference reversed GTRW file. + /// The spawn grid must be on the correct side of the finish line gate; using forward + /// spawn positions places cars on the wrong side, causing wrong-way detection and a + /// misplaced start-line trigger. + /// + public void ApplyReferenceSpawns(Stream referenceStream) + { + referenceStream.Position = 0; + var refBytes = new byte[referenceStream.Length]; + referenceStream.ReadExactly(refBytes, 0, refBytes.Length); + + uint refSpawnCount = BitConverter.ToUInt32(refBytes, 0x28); + uint refSpawnOffset = BitConverter.ToUInt32(refBytes, 0x2C); + int stride = RunwayStartingPosition.GetSize(); // 16 + + if (refSpawnOffset == 0 || refSpawnCount == 0) return; + if ((int)(refSpawnOffset + refSpawnCount * stride) > refBytes.Length) return; + + // Replace our SpawnPositions with those from the reference file. + SpawnPositions.Clear(); + for (int i = 0; i < (int)refSpawnCount; i++) + { + int off = (int)(refSpawnOffset + i * stride); + var sp = new RunwayStartingPosition + { + X = BitConverter.ToSingle(refBytes, off), + Y = BitConverter.ToSingle(refBytes, off + 4), + Z = BitConverter.ToSingle(refBytes, off + 8), + Rotation = BitConverter.ToSingle(refBytes, off + 12), + }; + SpawnPositions.Add(sp); + } + } + + public void ApplyReferenceGadgets(Stream referenceStream) + { + var refBytes = new byte[referenceStream.Length]; + referenceStream.ReadExactly(refBytes, 0, refBytes.Length); + + // ── Read gadget bounds from the reference file ──────────────────── + uint refGadCount = BitConverter.ToUInt32(refBytes, 0x50); + uint refGadOffset = BitConverter.ToUInt32(refBytes, 0x54); + uint refLvxOffset = BitConverter.ToUInt32(refBytes, 0x5C); + if (refGadOffset == 0 || refLvxOffset <= refGadOffset) return; // nothing to copy + int refGadSize = (int)(refLvxOffset - refGadOffset); + + // ── Our current gadget bounds (from OpaqueHeaderTail) ───────────── + uint ourGadOffset = BitConverter.ToUInt32(OpaqueHeaderTail, 0x24); // header 0x54 + uint ourLvxOffset = BitConverter.ToUInt32(OpaqueHeaderTail, 0x2C); // header 0x5C + if (ourGadOffset == 0 || ourLvxOffset <= ourGadOffset) return; + int ourGadSize = (int)(ourLvxOffset - ourGadOffset); + + // ── Locate sections within TrailingData ─────────────────────────── + uint spawnSectOff = (uint)(0x60u + GateRecords.Count * GT3GateRecord.GetSize()); + uint spawnEnd = (uint)(spawnSectOff + SpawnPositions.Count * RunwayStartingPosition.GetSize()); + + int tdGadStart = (int)(ourGadOffset - spawnEnd); // gadget start in TrailingData + int tdAfterGads = (int)(ourLvxOffset - spawnEnd); // light_vfx start in TrailingData + if (tdGadStart < 0 || tdAfterGads > TrailingData.Length) return; + + // ── Extract new gadget bytes from reference file ─────────────────── + var newGadBytes = new byte[refGadSize]; + Array.Copy(refBytes, (int)refGadOffset, newGadBytes, 0, refGadSize); + + // ── Rebuild TrailingData with new gadgets ───────────────────────── + int sizeDelta = refGadSize - ourGadSize; + var newTrailing = new byte[TrailingData.Length + sizeDelta]; + + Array.Copy(TrailingData, 0, newTrailing, 0, tdGadStart); // before gadgets + Array.Copy(newGadBytes, 0, newTrailing, tdGadStart, refGadSize); // new gadgets + Array.Copy(TrailingData, tdAfterGads, newTrailing, // light_vfx+ + tdGadStart + refGadSize, TrailingData.Length - tdAfterGads); + + TrailingData = newTrailing; + + // ── Patch OpaqueHeaderTail fields ───────────────────────────────── + // gadget_count (OpaqueHeaderTail[0x20] = header 0x50) + PatchU32(OpaqueHeaderTail, 0x20, refGadCount); + + // light_vfx_ptr (OpaqueHeaderTail[0x2C] = header 0x5C) + PatchU32(OpaqueHeaderTail, 0x2C, (uint)(ourGadOffset + refGadSize)); + + // RelocSize (header 0x0C) is patched by Write() from actual output length. + } + + private static void PatchU32(byte[] buf, int off, uint val) + { + buf[off] = (byte)(val & 0xFF); + buf[off+1] = (byte)((val >> 8) & 0xFF); + buf[off+2] = (byte)((val >> 16) & 0xFF); + buf[off+3] = (byte)((val >> 24) & 0xFF); + } + + // ── Writing ────────────────────────────────────────────────────────────── + + public void Write(Stream stream) + { + BinaryStream bs = new BinaryStream(stream); + long basePos = bs.Position; + + // Gate and spawn section offsets are determined by layout: + // Gate section always starts at 0x60 (immediately after the 96-byte header). + // Spawn section follows immediately after the gate records. + const uint gateOff = 0x60u; + uint spawnOff = (uint)(gateOff + GateRecords.Count * GT3GateRecord.GetSize()); + + // ── Header ────────────────────────────────────────────────────────── + bs.WriteUInt32(MAGIC); // 0x00 + bs.WriteUInt32(Field04); // 0x04 + bs.WriteUInt32(Field08); // 0x08 + long relocPos = bs.Position; + bs.WriteUInt32(0); // 0x0C placeholder — patched below + bs.WriteUInt32(Field10); // 0x10 + bs.WriteUInt32(Field14); // 0x14 + bs.WriteSingle(TrackV); // 0x18 + bs.WriteSingle(A1VCoord); // 0x1C + bs.WriteUInt16(HeaderField20); // 0x20 + bs.WriteUInt16((ushort)GateRecords.Count); // 0x22 + bs.WriteUInt32(gateOff); // 0x24 + bs.WriteUInt32((uint)SpawnPositions.Count); // 0x28 + bs.WriteUInt32(spawnOff); // 0x2C + bs.WriteBytes(OpaqueHeaderTail); // 0x30–0x5F + + // ── Gate records ───────────────────────────────────────────────────── + // Position should already be at basePos + gateOff = basePos + 0x60. + foreach (var rec in GateRecords) + rec.ToStream(bs); + + // ── Spawn positions ────────────────────────────────────────────────── + foreach (var sp in SpawnPositions) + sp.ToStream(bs); + + // ── Trailing opaque data ───────────────────────────────────────────── + bs.WriteBytes(TrailingData); + + // ── Patch RelocSize ────────────────────────────────────────────────── + long endPos = bs.Position; + bs.Position = relocPos; + bs.WriteUInt32((uint)(endPos - basePos - 16)); + bs.Position = endPos; + } + + // ── Reversal ───────────────────────────────────────────────────────────── + + public void ReverseDirection() + { + float totalTrackV = TrackV; + int N = GateRecords.Count; + + uint spawnSectOff = (uint)(0x60u + N * GT3GateRecord.GetSize()); + uint spawnEnd = (uint)(spawnSectOff + SpawnPositions.Count * RunwayStartingPosition.GetSize()); + + uint pfFileOff = BitConverter.ToUInt32(OpaqueHeaderTail, 0x14); + int pfCount = (int)BitConverter.ToUInt32(OpaqueHeaderTail, 0x10); + int tdPfStart = (int)(pfFileOff - spawnEnd); + const int kPfStride = 32; + + // ── Step 1: Read start/finish line geometry from the forward path spline ─ + // The start/finish gate is at the max-V → min-V wrap point in the pitbox. + // The entry with the MAXIMUM V is physically just before the finish line — + // verified against a known-good same-size fwd/rev pair: spawn midpoints + // cluster near max-V.M, not min-V.M (which is ~27 m off on the far side). + int sfIdx = 0; float sfMaxV = float.MinValue; + if (tdPfStart >= 0 && tdPfStart + pfCount * kPfStride <= TrailingData.Length) + { + for (int k = 0; k < pfCount; k++) + { + float v = BitConverter.ToSingle(TrailingData, tdPfStart + k * kPfStride + 16); + if (v > sfMaxV) { sfMaxV = v; sfIdx = k; } + } + } + int sfBase = tdPfStart + sfIdx * kPfStride; + float sfLx = BitConverter.ToSingle(TrailingData, sfBase + 0); + float sfLz = BitConverter.ToSingle(TrailingData, sfBase + 4); + float sfRx = BitConverter.ToSingle(TrailingData, sfBase + 8); + float sfRz = BitConverter.ToSingle(TrailingData, sfBase + 12); + float sfNx = BitConverter.ToSingle(TrailingData, sfBase + 20); // road tangent = line normal + float sfNz = BitConverter.ToSingle(TrailingData, sfBase + 24); + float sfMx = (sfLx + sfRx) * 0.5f; + float sfMz = (sfLz + sfRz) * 0.5f; + + // ── Step 2: V-mirror sector records, then re-sort ascending ────────── + // Verified against known-good fwd/rev pair: mirror dist (L−d), re-sort + // ascending, sentinels kept unchanged (gate[0].V=0, gate[1].V=0.025). + // Re-sorting puts timing gates (gateFlag=0) in the order they are crossed + // going CCW, so the game's sequential scan correctly assigns T1/T2/T3. + for (int g = 0; g < GateRecords.Count; g++) + { + var rec = GateRecords[g]; + if (rec.SectorId == 0 && rec.GateFlag == -1) continue; // a=0 only: keep V=0 + rec.TrackV = totalTrackV - rec.TrackV; // a=1 mirrors to ≈3921 → sorts to end + } + GateRecords.Sort((a, b) => a.TrackV.CompareTo(b.TrackV)); + A1VCoord = GateRecords.FirstOrDefault(r => r.IsSentinel && r.SectorId == 1)?.TrackV ?? A1VCoord; + + // ── Step 3: Reflect start grid across the start/finish line ────────── + // Verified against known-good pair: spawns are REFLECTED across the gate + // plane (midpoint sfM, unit normal sfN = road tangent), not just rotated. + // Reflecting position and heading: heading component along sfN negated. + // Convention: dir = (sin rot, cos rot) in (X, Z) — verified by reflection + // formula giving +π/2 → −π/2 for an X-axis-normal start line. + foreach (var sp in SpawnPositions) + { + float d = (sp.X - sfMx) * sfNx + (sp.Z - sfMz) * sfNz; + sp.X -= 2f * d * sfNx; + sp.Z -= 2f * d * sfNz; + float sinR = MathF.Sin(sp.Rotation), cosR = MathF.Cos(sp.Rotation); + float proj = sinR * sfNx + cosR * sfNz; + sp.Rotation = MathF.Atan2(sinR - 2f * proj * sfNx, cosR - 2f * proj * sfNz); + } + + // ── Step 4: Reverse path spline — 5-part transform ─────────────────── + // Verified 116/116 against known-good fwd/rev pair: + // 1. Reverse station order: new[i] ← fwd[(N−2−i) mod N] + // 2. Swap L/R edge points: (Lx,Lz) ↔ (Rx,Rz) + // 3. Mirror lap distance: dist → TrackV − dist + // 4. Negate tangent: (tanX,tanZ) → (−tanX,−tanZ) + // 5. Negate aux (f7): aux → −aux ← the field the old code missed + // Collision mesh, vertex pool, spatial grids, tail: byte-identical — no transform. + if (tdPfStart >= 0 && tdPfStart + pfCount * kPfStride <= TrailingData.Length) + { + var pfCopy = new byte[pfCount * kPfStride]; + Array.Copy(TrailingData, tdPfStart, pfCopy, 0, pfCopy.Length); + + for (int i = 0; i < pfCount; i++) + { + int srcIdx = ((pfCount - 2 - i) % pfCount + pfCount) % pfCount; + int src = srcIdx * kPfStride; + int dst = tdPfStart + i * kPfStride; + + float f0 = BitConverter.ToSingle(pfCopy, src); + float f1 = BitConverter.ToSingle(pfCopy, src + 4); + float f2 = BitConverter.ToSingle(pfCopy, src + 8); + float f3 = BitConverter.ToSingle(pfCopy, src + 12); + float f4 = BitConverter.ToSingle(pfCopy, src + 16); + float f5 = BitConverter.ToSingle(pfCopy, src + 20); + float f6 = BitConverter.ToSingle(pfCopy, src + 24); + float f7 = BitConverter.ToSingle(pfCopy, src + 28); + + void WriteF(int off, float val) + => Array.Copy(BitConverter.GetBytes(val), 0, TrailingData, dst + off, 4); + + WriteF( 0, f2); WriteF( 4, f3); // Left ← old Right + WriteF( 8, f0); WriteF(12, f1); // Right ← old Left + WriteF(16, totalTrackV - f4); // mirror dist + WriteF(20, -f5); WriteF(24, -f6); // negate tangent + WriteF(28, -f7); // negate aux + } + + // Rotate so the entry with minimum V (= physical start/finish) is at index 0. + // The game renders the start-line marker at entry[0]; without rotation + // entry[0] sits at V≈1957 (the forward mid-track), placing the visual + // start line halfway around the circuit. + int minVIdx2 = 0; float minV2 = float.MaxValue; + for (int k = 0; k < pfCount; k++) + { + float v = BitConverter.ToSingle(TrailingData, tdPfStart + k * kPfStride + 16); + if (v < minV2) { minV2 = v; minVIdx2 = k; } + } + if (minVIdx2 != 0) + { + var tmp = new byte[pfCount * kPfStride]; + Array.Copy(TrailingData, tdPfStart, tmp, 0, tmp.Length); + for (int i = 0; i < pfCount; i++) + { + int srcIdx = (i + minVIdx2) % pfCount; + Array.Copy(tmp, srcIdx * kPfStride, + TrailingData, tdPfStart + i * kPfStride, kPfStride); + } + } + } + // ── Step 6: Reverse groundcolldata spatial grid (TODO — structure TBD) ─ + // The spatial grid (cell→node-index tables) encodes traversal direction. + // Correct transformation requires understanding the exact cell-list format + // to avoid over-modifying non-node values. Currently disabled. + if (false) // placeholder — do not execute + // ── Step 6 body ────────────────────────────────────────────────────── + // The spatial grid maps world-space cells to mesh node indices. + // For the reversed track, each node index k must become (S_fwd − k + N) % N, + // where S_fwd is the forward node closest to the start/finish gate (found + // via vertex centroid distance) and N is the total node count. + // This encodes the CCW traversal order without re-tessellating the mesh. + // + // Layout within groundcolldata (starting at magic 0x780053): + // [0 .. N×64−1] node descriptors (64 B each) + // [N×64 .. minOffA−1] spatial grid (the section we remap) + // [minOffA ..] vertex / triangle blocks (untouched) + { + // Locate groundcolldata: first try the magic 0x780053 (present in some tracks), + // otherwise fall back to scanning from the start of TrailingData. + // Some tracks (e.g. smtsouth) have no magic — node descriptors begin immediately. + const uint kGcMag = 0x00780053u; + int gcS = -1; + for (int p = 0; p + 4 <= TrailingData.Length; p++) + if (BitConverter.ToUInt32(TrailingData, p) == kGcMag) { gcS = p; break; } + if (gcS < 0) gcS = 0; // no magic — scan from beginning of TrailingData + + { + // Find first valid 64-byte node descriptor: n1>5, n2>5, deg 1-20, zeros at +28..+63 + int dBase = gcS; + for (int p = gcS; p + 64 <= TrailingData.Length; p++) + { + int dn1 = BitConverter.ToUInt16(TrailingData, p); + int dn2 = BitConverter.ToUInt16(TrailingData, p + 2); + int ddeg = BitConverter.ToUInt16(TrailingData, p + 22); + if (dn1 > 5 && dn2 > 5 && ddeg >= 1 && ddeg <= 20) + { dBase = p; break; } + } + + // Count consecutive valid descriptors → N + // Only require n1>0, n2>0, deg in [1,20] — not all-zeros at +28..+63, + // because detail/junction nodes may have neighbour data there. + int gcN = 0; + while (true) + { + int bp = dBase + gcN * 64; + if (bp + 64 > TrailingData.Length) break; + int dn1 = BitConverter.ToUInt16(TrailingData, bp); + int dn2 = BitConverter.ToUInt16(TrailingData, bp + 2); + int ddeg = BitConverter.ToUInt16(TrailingData, bp + 22); + if (dn1 < 1 || dn2 < 1 || ddeg < 1 || ddeg > 20) break; + gcN++; + } + + if (gcN >= 2) + { + // Full node count for mod-N arithmetic: prefer the 4-byte count stored + // just before the descriptor array (or at the start of TrailingData). + // This includes detail/junction nodes that may not follow the sequential + // 64-byte layout. Fall back to gcN if no valid count is found. + int gcNFull = gcN; + { + // Try: 4 bytes just before dBase + if (dBase >= 4) + { + int tryN = (int)BitConverter.ToUInt32(TrailingData, dBase - 4); + if (tryN >= gcN && tryN < 4096) gcNFull = tryN; + } + // Try: first 4 bytes of TrailingData + if (gcNFull == gcN && TrailingData.Length >= 4) + { + int tryN = (int)BitConverter.ToUInt32(TrailingData, 0); + if (tryN >= gcN && tryN < 4096) gcNFull = tryN; + } + } + + // offA in the descriptor may be: + // ABSOLUTE (file offset) — tracks with magic header, offA > spawnEnd + nodeDataBase + // RELATIVE (to node data area = dBase + gcN×64) — tracks without magic + // Auto-detect by checking whether offA[0] exceeds the node-data-area file offset. + int nodeDataBase = dBase + gcN * 64; + uint offA0 = BitConverter.ToUInt32(TrailingData, dBase + 4); + bool absoluteOffA = (offA0 > (uint)(spawnEnd + nodeDataBase)); + + int sFwd = 0; + float minNodeDist = float.MaxValue; + int minVtxTd = TrailingData.Length; + + for (int n = 0; n < gcN; n++) + { + int bp = dBase + n * 64; + int dn1 = BitConverter.ToUInt16(TrailingData, bp); + uint offA = BitConverter.ToUInt32(TrailingData, bp + 4); + + int vtxTd = absoluteOffA + ? (int)(offA - spawnEnd) // file-absolute → TrailingData offset + : nodeDataBase + (int)offA; // section-relative → TrailingData offset + + if (vtxTd < minVtxTd) minVtxTd = vtxTd; + if (vtxTd < 0 || vtxTd + dn1 * 16 > TrailingData.Length) continue; + + float cx = 0f, cz = 0f; + for (int v = 0; v < dn1; v++) + { + cx += BitConverter.ToSingle(TrailingData, vtxTd + v * 16 + 0); + cz += BitConverter.ToSingle(TrailingData, vtxTd + v * 16 + 8); + } + cx /= dn1; cz /= dn1; + float d = (cx - sfMx) * (cx - sfMx) + (cz - sfMz) * (cz - sfMz); + if (d < minNodeDist) { minNodeDist = d; sFwd = n; } + } + + // Spatial grid: from end of descriptor array to start of first vertex block + int gridStart2 = nodeDataBase; + int gridEnd2 = (minVtxTd < TrailingData.Length) ? minVtxTd : TrailingData.Length; + if (gridEnd2 > TrailingData.Length) gridEnd2 = TrailingData.Length; + + // Remap every uint16 in [0, gcNFull) within the spatial grid. + // Use gcNFull (full node count) for the modular formula so that + // detail/junction nodes (indices >= gcN) are also correctly remapped. + for (int off = gridStart2; off + 2 <= gridEnd2; off += 2) + { + int k = BitConverter.ToUInt16(TrailingData, off); + if (k < gcNFull) + { + int newK = ((sFwd - k) % gcNFull + gcNFull) % gcNFull; + TrailingData[off] = (byte)(newK & 0xFF); + TrailingData[off + 1] = (byte)(newK >> 8); + } + } + } + } + } + } + + // ── Private helpers ────────────────────────────────────────────────────── + + private static float NormaliseAngle(float rad) + { + const float TwoPi = MathF.PI * 2f; + rad %= TwoPi; + if (rad > MathF.PI) rad -= TwoPi; + if (rad <= -MathF.PI) rad += TwoPi; + return rad; + } +} diff --git a/PDTools.Files/Courses/PS2/Runway/GT3RunwayReverser.cs b/PDTools.Files/Courses/PS2/Runway/GT3RunwayReverser.cs new file mode 100644 index 00000000..705473f6 --- /dev/null +++ b/PDTools.Files/Courses/PS2/Runway/GT3RunwayReverser.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace PDTools.Files.Courses.PS2.Runway; + +/// +/// Reverses the driving direction of a GTRW "CourseRunwayData" runway file +/// (full single-file, little-endian format). Validated byte-for-byte against +/// known-good forward/reverse pairs (smtsouth: 0 bytes off). +/// +/// Four transforms flip the lap — all other sections are left unchanged: +/// 1. Path spline — new[k] = T(fwd[(PC-2-k)%PC]); swap L/R, mirror dist, +/// negate tangent and aux. +/// 2. Start grid — rotated 180° about the start/finish point. +/// 3. Sectors — dist mirrored (L-d), re-sorted ascending; start/finish +/// markers (type≠2) stay at distance 0. +/// 4. Mesh — each node's g path-segment refs reflected by S=(PC-3)%PC +/// and re-sorted ascending. Geometry and adjacency unchanged. +/// +/// File size is preserved — header offsets and EOF need no changes. +/// +public static class GT3RunwayReverser +{ + // ── Helpers ────────────────────────────────────────────────────────────── + + private static ushort RU16(byte[] b, int o) => BitConverter.ToUInt16(b, o); + private static uint RU32(byte[] b, int o) => BitConverter.ToUInt32(b, o); + private static float RF32(byte[] b, int o) => BitConverter.ToSingle(b, o); + + private static void WU16(byte[] b, int o, int v) + { + b[o] = (byte)(v & 0xFF); + b[o + 1] = (byte)((v >> 8) & 0xFF); + } + private static void WF32(byte[] b, int o, float v) + => Array.Copy(BitConverter.GetBytes(v), 0, b, o, 4); + + // Floating-point modulo, always non-negative. + private static float FMod(float x, float m) + { + float r = x % m; + return r < 0f ? r + m : r; + } + + // ── Section descriptor ─────────────────────────────────────────────────── + + private readonly struct Info + { + public readonly int Size; + public readonly float L; // lap length + public readonly int SectorCount; + public readonly int GridCount, GridOff; // start grid (spawn slots) + public readonly int MeshCount, MeshOff; // collision mesh nodes + public readonly int PoolOff; // surface vertex pool + public readonly int PathCount, PathOff; // centreline path spline + public readonly int Grid1Off, Grid2Off; // broadphase grids + public readonly int TailOff; // staging / tail block + + public Info(byte[] d) + { + Size = d.Length; + L = RF32(d, 0x18); + SectorCount = RU16(d, 0x22); + GridCount = (int)RU32(d, 0x28); + GridOff = (int)RU32(d, 0x2C); + MeshCount = (int)RU32(d, 0x30); + MeshOff = (int)RU32(d, 0x34); + PoolOff = (int)RU32(d, 0x3C); + PathCount = (int)RU32(d, 0x40); + PathOff = (int)RU32(d, 0x44); + Grid1Off = (int)RU32(d, 0x48); + Grid2Off = (int)RU32(d, 0x4C); + TailOff = (int)RU32(d, 0x54); + } + } + + // ── Validation ─────────────────────────────────────────────────────────── + + private static Info Validate(byte[] d) + { + if (d.Length < 0x60 + || d[0] != (byte)'G' || d[1] != (byte)'T' + || d[2] != (byte)'R' || d[3] != (byte)'W') + throw new InvalidDataException("Not a GTRW runway file (missing GTRW magic)."); + + var info = new Info(d); + + if (RU32(d, 0x24) != 0x60) + throw new InvalidDataException("Unexpected sectors offset — layout not supported."); + if (info.L <= 10f || info.L >= 1e6f) + throw new InvalidDataException("Lap length out of range — aborting to avoid corruption."); + + int[] order = { info.MeshOff, info.PoolOff, info.PathOff, + info.Grid1Off, info.Grid2Off, info.TailOff }; + for (int i = 0; i < order.Length; i++) + if (order[i] < 0x60 || order[i] > info.Size) + throw new InvalidDataException("Section offsets inconsistent — file not supported."); + for (int i = 1; i < order.Length; i++) + if (order[i] < order[i - 1]) + throw new InvalidDataException("Section offsets not monotonically increasing — not supported."); + + if (info.MeshCount < 2 || info.MeshCount > 100_000) + throw new InvalidDataException("Mesh node count out of range — aborting."); + + return info; + } + + // ── Transform 1: reverse the centreline path spline ───────────────────── + // new[k] = T(fwd[(PC-2-k) % PC]) — swap L/R, mirror dist, negate tan+aux. + + private static void ReversePath(byte[] d, in Info i) + { + int po = i.PathOff, pc = i.PathCount; + float L = i.L; + + var snap = new float[pc, 8]; + for (int s = 0; s < pc; s++) + for (int f = 0; f < 8; f++) + snap[s, f] = RF32(d, po + s * 32 + f * 4); + + for (int k = 0; k < pc; k++) + { + int src = ((pc - 2 - k) % pc + pc) % pc; + float Lx = snap[src, 0], Lz = snap[src, 1]; + float Rx = snap[src, 2], Rz = snap[src, 3]; + float dist = snap[src, 4]; + float tx = snap[src, 5], tz = snap[src, 6], aux = snap[src, 7]; + int dst = po + k * 32; + WF32(d, dst + 0, Rx); + WF32(d, dst + 4, Rz); + WF32(d, dst + 8, Lx); + WF32(d, dst + 12, Lz); + WF32(d, dst + 16, FMod(L - dist, L)); + WF32(d, dst + 20, -tx); + WF32(d, dst + 24, -tz); + WF32(d, dst + 28, -aux); + } + } + + // ── Transform 2: rotate start grid 180° about the start/finish point ──── + // Must be called BEFORE ReversePath so it reads the forward centreline. + + private static void ReverseStartGrid(byte[] d, in Info i) + { + int go = i.GridOff, gc = i.GridCount; + int po = i.PathOff, pc = i.PathCount; + float L = i.L; + + // Read forward path stations (8 floats: Lx,Lz,Rx,Rz,dist,tx,tz,aux) + var midX = new float[pc]; var midZ = new float[pc]; + var dist = new float[pc]; + var tanX = new float[pc]; var tanZ = new float[pc]; + for (int s = 0; s < pc; s++) + { + int o = po + s * 32; + float lx = RF32(d, o), lz = RF32(d, o + 4), rx = RF32(d, o + 8), rz = RF32(d, o + 12); + midX[s] = (lx + rx) * 0.5f; + midZ[s] = (lz + rz) * 0.5f; + dist[s] = RF32(d, o + 16); + tanX[s] = RF32(d, o + 20); + tanZ[s] = RF32(d, o + 24); + } + + // Find the dist→0 wrap (largest drop between consecutive distances) + int w = 0; float maxGap = float.MinValue; + for (int s = 0; s < pc; s++) + { + float gap = dist[s] - dist[(s + 1) % pc]; + if (gap > maxGap) { maxGap = gap; w = s; } + } + int w2 = (w + 1) % pc; + float den = (L - dist[w]) + dist[w2]; + float f = den > 1e-6f ? (L - dist[w]) / den : 0f; + + float p0x = midX[w] + f * (midX[w2] - midX[w]); + float p0z = midZ[w] + f * (midZ[w2] - midZ[w]); + float tx0 = tanX[w] + f * (tanX[w2] - tanX[w]); + float tz0 = tanZ[w] + f * (tanZ[w2] - tanZ[w]); + float tl = MathF.Sqrt(tx0 * tx0 + tz0 * tz0); + if (tl < 1e-10f) tl = 1f; + float px = -tz0 / tl; // perpendicular to tangent = start-line direction + float pz = tx0 / tl; + + // Project grid centroid onto the start line to get the reflection centre + float cx = 0f, cz = 0f; + for (int s = 0; s < gc; s++) { cx += RF32(d, go + s * 16); cz += RF32(d, go + s * 16 + 8); } + cx /= gc; cz /= gc; + float dot = (cx - p0x) * px + (cz - p0z) * pz; + float rx2 = p0x + dot * px, rz2 = p0z + dot * pz; + + // Reflect each slot: x → 2rx-x, z → 2rz-z, heading → atan2(-sin,-cos) + for (int s = 0; s < gc; s++) + { + int o = go + s * 16; + float x = RF32(d, o), y = RF32(d, o + 4), z = RF32(d, o + 8), h = RF32(d, o + 12); + WF32(d, o + 0, 2f * rx2 - x); + WF32(d, o + 4, y); + WF32(d, o + 8, 2f * rz2 - z); + WF32(d, o + 12, MathF.Atan2(-MathF.Sin(h), -MathF.Cos(h))); + } + } + + // ── Transform 3: mirror sector / checkpoint distances ──────────────────── + // type≠2 markers stay at dist=0; type-2 records mirrored (L-d), re-sorted. + + private static void ReverseSectors(byte[] d, in Info i) + { + int sc = i.SectorCount; + float L = i.L; + + var markers = new List<(int t, int fl, float dist)>(); + var feats = new List<(int t, int fl, float dist)>(); + for (int s = 0; s < sc; s++) + { + int o = 0x60 + s * 8; + int type = RU16(d, o), flag = RU16(d, o + 2); + float dt = RF32(d, o + 4); + (type != 2 ? markers : feats).Add((type, flag, dt)); + } + + for (int s = 0; s < feats.Count; s++) + { + var (t, fl, dt) = feats[s]; + feats[s] = (t, fl, FMod(L - dt, L)); + } + feats.Sort((a, b) => a.dist.CompareTo(b.dist)); + + int idx = 0; + foreach (var (t, fl, dt) in markers) { int o = 0x60 + idx++ * 8; WU16(d, o, t); WU16(d, o + 2, fl); WF32(d, o + 4, dt); } + foreach (var (t, fl, dt) in feats) { int o = 0x60 + idx++ * 8; WU16(d, o, t); WU16(d, o + 2, fl); WF32(d, o + 4, dt); } + } + + // ── Transform 4: reflect ground-collision path-segment references ───────── + // Each node carries g segment indices (at its offC record); S = (PC-3)%PC + // derived from the path reversal formula. Re-sorted ascending per node. + // Geometry, adjacency and all other node data are left unchanged. + + private static void ReverseMesh(byte[] d, in Info i) + { + int mo = i.MeshOff, N = i.MeshCount, PC = i.PathCount; + int S = (PC - 3 + PC) % PC; + + for (int n = 0; n < N; n++) + { + int o = mo + n * 64; + int g = RU16(d, o + 20); // geom-count = path-segment ref count + int offC = (int)RU32(d, o + 16); // absolute offset of node's 16-byte record + + var refs = new int[g]; + for (int j = 0; j < g; j++) + refs[j] = ((S - RU16(d, offC + j * 2)) % PC + PC) % PC; + Array.Sort(refs); + + for (int j = 0; j < g; j++) + WU16(d, offC + j * 2, refs[j]); + } + } + + // ── Public API ─────────────────────────────────────────────────────────── + + /// + /// Returns a new byte array containing the reversed GTRW runway. + /// Throws if the file format is not supported. + /// + public static byte[] Reverse(byte[] data) + { + var d = (byte[])data.Clone(); + var info = Validate(d); + + ReverseStartGrid(d, info); // reads forward path — must precede ReversePath + ReversePath(d, info); + ReverseSectors(d, info); + ReverseMesh(d, info); + + return d; + } + + /// + /// Read , reverse it, write to . + /// Returns a summary line suitable for logging. + /// + public static string ReverseFile(string inputPath, string outputPath) + { + if (Path.GetFullPath(inputPath).Equals( + Path.GetFullPath(outputPath), StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("Output path must differ from the input file."); + + byte[] data = File.ReadAllBytes(inputPath); + var info = Validate(data); // validate before cloning + byte[] result = Reverse(data); + + string dir = Path.GetDirectoryName(outputPath); + if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir); + File.WriteAllBytes(outputPath, result); + + return $"{result.Length} bytes | lap {info.L:F2} m | " + + $"{info.MeshCount} mesh nodes | {info.SectorCount} sectors | " + + $"{info.PathCount} path stations"; + } +} diff --git a/PDTools.Files/Courses/PS2/Runway/RunwayCluster.cs b/PDTools.Files/Courses/PS2/Runway/RunwayCluster.cs index f47e461c..19daff66 100644 --- a/PDTools.Files/Courses/PS2/Runway/RunwayCluster.cs +++ b/PDTools.Files/Courses/PS2/Runway/RunwayCluster.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -12,26 +12,48 @@ namespace PDTools.Files.Courses.PS2.Runway; public class RunwayCluster { public ushort CheckpointLookupIndexStart { get; set; } - public ushort CheckpointLookupLength { get; set; } - public short[] TriIndices { get; set; } + public ushort CheckpointLookupLength { get; set; } + public short[] TriIndices { get; set; } public static RunwayCluster FromStream(BinaryStream bs) { RunwayCluster cluster = new RunwayCluster(); - ushort triIndexCount = bs.ReadUInt16(); - cluster.CheckpointLookupLength = bs.ReadUInt16(); + ushort triIndexCount = bs.ReadUInt16(); + cluster.CheckpointLookupLength = bs.ReadUInt16(); cluster.CheckpointLookupIndexStart = bs.ReadUInt16(); - bs.ReadUInt16(); - int triIndicesOffset = bs.ReadInt32(); - bs.ReadInt32(); + bs.ReadUInt16(); // padding + int triIndicesOffset = bs.ReadInt32(); + bs.ReadInt32(); // padding + long savedPos = bs.Position; bs.Position = triIndicesOffset; cluster.TriIndices = bs.ReadInt16s((int)triIndexCount); + bs.Position = savedPos; return cluster; } - public static int GetSize() + /// + /// Writes the 0x10-byte cluster header. + /// is the file-absolute byte offset of the + /// first tri-index short for this cluster (written into the header's triIndicesOffset field). + /// The actual tri-index data is written separately by . + /// + public void WriteHeader(BinaryStream bs, int absoluteTriDataOffset) { - return 0x10; + bs.WriteUInt16((ushort)TriIndices.Length); + bs.WriteUInt16(CheckpointLookupLength); + bs.WriteUInt16(CheckpointLookupIndexStart); + bs.WriteUInt16(0); // padding + bs.WriteInt32(absoluteTriDataOffset); + bs.WriteInt32(0); // padding } + + /// Writes the tri-index array (sequentially; call after all cluster headers). + public void WriteTriData(BinaryStream bs) + { + foreach (short idx in TriIndices) + bs.WriteInt16(idx); + } + + public static int GetSize() => 0x10; } diff --git a/PDTools.Files/Courses/PS2/Runway/RunwayData.cs b/PDTools.Files/Courses/PS2/Runway/RunwayData.cs index 7aed74e7..0ccf3762 100644 --- a/PDTools.Files/Courses/PS2/Runway/RunwayData.cs +++ b/PDTools.Files/Courses/PS2/Runway/RunwayData.cs @@ -1,12 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Numerics; using System.IO; -using PDTools.Files; using PDTools.Utils; using Syroot.BinaryData; @@ -15,225 +12,915 @@ namespace PDTools.Files.Courses.PS2.Runway; public class RunwayData { - /// - /// Version major of the runway. - /// - public ushort VersionMajor { get; set; } + // ── File magic ─────────────────────────────────────────────────────────── - /// - /// Version minor of the runway. - /// - public ushort VersionMinor { get; set; } + /// '4WNR' big-endian magic. + public const uint MAGIC_BE = 0x524E5734u; - /// - /// Unknown - /// - public uint Flags { get; set; } + /// 'RNW4' little-endian magic. + public const uint MAGIC_LE = 0x34574E52u; - /// - /// Min model bound of the runway. - /// - public Vector3[] Bounds = default; + // ── Header fields ──────────────────────────────────────────────────────── - /// - /// Increases depending on the track - /// - public uint UnkVal1 { get; set; } + public uint Magic { get; set; } /// - /// Unknown + /// Value stored at header offset 0x08 (RelocSize / relocation data size). + /// Preserved here so the builder can write back the original value rather than + /// the actual computed file length. 0 = use computed size (safe default). /// - public uint UnkVal2 { get; set; } + public uint OriginalRelocSize { get; set; } - /// - /// Unknown (GT6 Apricot Hill has it set) - /// - public uint UnkVal3 { get; set; } + /// Version word from offset 0x10 (high 16 = major, low 16 = minor). + public uint Version { get; set; } + public ushort VersionMajor => (ushort)(Version >> 16); + public ushort VersionMinor => (ushort)(Version & 0xFFFF); - /// - /// Track length in meters. - /// + public uint Flags { get; set; } + + /// Total track length in metres (the VCoord at wrap). public float TrackV { get; set; } public float StartVCoord { get; set; } - public float GoalVCoord { get; set; } + public float GoalVCoord { get; set; } - public List Checkpoints { get; set; } = []; - public List CheckpointLookupIndices { get; set; } = []; - public List Vertices { get; set; } = []; - public List Tris { get; set; } = []; - public List Clusters { get; set; } = []; + /// World-space AABB: [0] = min, [1] = max. + public Vector3[] Bounds { get; set; } = new Vector3[2]; + // ── Counts stored verbatim from the header ─────────────────────────────── + public short CheckpointListCount { get; set; } // 0x38 + public short UnkCount { get; set; } // 0x3A + public int Unk0x40 { get; set; } // 0x40 (int32 after the first four counts) + public short GadgetsCount { get; set; } // 0x44 + + /// BSP tree depth (number of levels including root). public byte TreeMaxDepth { get; set; } - public Node Root { get; set; } + + // ── Opaque header blobs ────────────────────────────────────────────────── + + /// 19 raw bytes from header offset 0x4D to 0x5F (unknown count / flag fields). + public byte[] UnknownHeader0x4D { get; set; } /// - /// '4WNR' + /// 60 raw bytes from header offset 0x84 to 0xBF. + /// The first four int32 words are absolute file offsets into the trailing-data region. + /// These are patched on write so they reflect the new layout; the stored copy always + /// contains the values as read from the source file. /// - public const int MAGIC_BE = 0x524E5734; + public byte[] ExtraHeaderBytes { get; set; } + + // ── Spawn positions ────────────────────────────────────────────────────── /// - /// 'RNW4' + /// Six starting-grid positions at file offset 0xC0 (6 × 0x10 bytes). + /// Rotation is in radians; to reverse the course, add π (≈ 3.14159) to each angle. /// - public const int MAGIC_LE = 0x34574E52; + public List SpawnPositions { get; set; } = []; - public uint Magic { get; set; } + // ── Main data lists ────────────────────────────────────────────────────── + + public List Checkpoints { get; set; } = []; + public List CheckpointLookupIndices { get; set; } = []; + public List Vertices { get; set; } = []; + public List Tris { get; set; } = []; + public List Clusters { get; set; } = []; + + // ── Opaque section blobs (raw bytes including any trailing padding) ─────── + + public byte[] LightSetsRawBytes { get; set; } + public byte[] UnkOffset3RawBytes { get; set; } + public byte[] GadgetsRawBytes { get; set; } + + /// + /// Raw bytes that follow the cluster tri-index arrays, referenced by the offset + /// words in . Contains physics/gadget/lighting data + /// whose internal structure is unknown; it is preserved verbatim on write. + /// This section is the cause of wall-collision failure when absent. + /// + public byte[] TrailingRawBytes { get; set; } + + // ── BSP tree ───────────────────────────────────────────────────────────── + + public Node Root { get; set; } + + // ── Original-offset tracking (set by FromStream, used by Write) ────────── + // These let Write() detect which blob sections shared the same file position + // in the source file (so they're written only once) and correctly relocate the + // trailing data referenced by ExtraHeaderBytes. + + private int _originalLightSetsOffset; + private int _originalUnkOffset3; + private int _originalGadgetsOffset; + private int _originalTrailingDataOffset; // absolute from file base; 0 = absent + + // When a blob section's offset equals roadVertsOffset in the source file the + // blob is absent (the game just finds road verts there). Track these flags so + // Write() can reproduce the same header layout without mis-reading road-vert + // bytes as blob data. + private bool _lightSetsSharesWithRoadVerts; + private bool _unk3SharesWithRoadVerts; + private bool _gadgetsSharesWithRoadVerts; + + // ── Reading ────────────────────────────────────────────────────────────── public RunwayData FromStream(Stream stream) { BinaryStream bs = new BinaryStream(stream); long basePos = bs.Position; - RunwayData rwy = new RunwayData(); - rwy.Magic = bs.ReadUInt32(); + Magic = bs.ReadUInt32(); // 0x00 - if (rwy.Magic == MAGIC_BE) + if (Magic == MAGIC_BE) bs.ByteConverter = ByteConverter.Big; - else if (rwy.Magic == MAGIC_LE) + else if (Magic == MAGIC_LE) bs.ByteConverter = ByteConverter.Little; else - throw new InvalidDataException("Unsupported runway format."); - - bs.ReadInt32(); // Reloc Pointer - bs.ReadUInt32(); // Reloc Size - rwy.Flags = bs.ReadUInt32(); - uint version = bs.ReadUInt32(); // version - rwy.TrackV = bs.ReadSingle(); - rwy.StartVCoord = bs.ReadSingle(); - rwy.GoalVCoord = bs.ReadSingle(); - - rwy.Bounds = - [ - new Vector3(bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle()), - new Vector3(bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle()) - ]; - - short checkpointListCount = bs.ReadInt16(); - short unkCount = bs.ReadInt16(); - short checkpointCount = bs.ReadInt16(); - short checkpointLookupIndicesCount = bs.ReadInt16(); - bs.ReadInt32(); - short gadgetsCount = bs.ReadInt16(); - short roadVerticesCount = bs.ReadInt16(); - short roadTriCount = bs.ReadInt16(); - short clusterCount = bs.ReadInt16(); - rwy.TreeMaxDepth = bs.Read1Byte(); - - bs.Position = 0x60; - int checkpointsOffset = bs.ReadInt32(); - int checkpointLookupIndicesOffset = bs.ReadInt32(); - int lightSetsOffset = bs.ReadInt32(); - int unkOffset3 = bs.ReadInt32(); - int gadgetsOffset = bs.ReadInt32(); - int roadVertsOffset = bs.ReadInt32(); - int roadTrisOffset = bs.ReadInt32(); - int clustersOffset = bs.ReadInt32(); - int traversalDataOffset = bs.ReadInt32(); + throw new InvalidDataException($"Not a RNW4 file (magic 0x{Magic:X8})."); + + bs.ReadInt32(); // 0x04 RelocPtr (runtime) + OriginalRelocSize = bs.ReadUInt32(); // 0x08 RelocSize (preserve for exact round-trip) + Flags = bs.ReadUInt32(); // 0x0C + Version = bs.ReadUInt32(); // 0x10 + TrackV = bs.ReadSingle(); // 0x14 + StartVCoord = bs.ReadSingle(); // 0x18 + GoalVCoord = bs.ReadSingle(); // 0x1C + Bounds = new Vector3[] + { + new Vector3(bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle()), // 0x20 + new Vector3(bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle()), // 0x2C + }; + + // ── Counts @ 0x38 ───────────────────────────────────────────────── + CheckpointListCount = bs.ReadInt16(); // 0x38 + UnkCount = bs.ReadInt16(); // 0x3A + short checkpointCount = bs.ReadInt16(); // 0x3C + short checkpointLookupIndicesCount = bs.ReadInt16(); // 0x3E + Unk0x40 = bs.ReadInt32(); // 0x40 + GadgetsCount = bs.ReadInt16(); // 0x44 + ushort roadVerticesCount = bs.ReadUInt16(); // 0x46 — ushort: Suzuka has >32767 verts + ushort roadTriCount = bs.ReadUInt16(); // 0x48 — ushort: Suzuka has >32767 tris + ushort clusterCount = bs.ReadUInt16(); // 0x4A + TreeMaxDepth = bs.Read1Byte(); // 0x4C + + // 0x4D – 0x5F: 19 unknown bytes + UnknownHeader0x4D = bs.ReadBytes(0x13); + + // ── Offset table @ 0x60 ─────────────────────────────────────────── + bs.Position = basePos + 0x60; + int checkpointsOffset = bs.ReadInt32(); // 0x60 + int checkpointLookupOffset = bs.ReadInt32(); // 0x64 + int lightSetsOffset = bs.ReadInt32(); // 0x68 + int unkOffset3 = bs.ReadInt32(); // 0x6C + int gadgetsOffset = bs.ReadInt32(); // 0x70 + int roadVertsOffset = bs.ReadInt32(); // 0x74 + int roadTrisOffset = bs.ReadInt32(); // 0x78 + int clustersOffset = bs.ReadInt32(); // 0x7C + int traversalDataOffset = bs.ReadInt32(); // 0x80 + + // Store for use in Write() (duplicate-section detection) + _originalLightSetsOffset = lightSetsOffset; + _originalUnkOffset3 = unkOffset3; + _originalGadgetsOffset = gadgetsOffset; + + // 0x84 – 0xBF: 60 bytes of extra header (4 offset words + constant words) + ExtraHeaderBytes = bs.ReadBytes(0x3C); + + // Extract the first non-zero trailing-data pointer from ExtraHeaderBytes + // (bytes 0–3 of the array = file offset 0x84). + _originalTrailingDataOffset = 0; + if (ExtraHeaderBytes.Length >= 4) + { + _originalTrailingDataOffset = Magic == MAGIC_BE + ? (ExtraHeaderBytes[0] << 24) | (ExtraHeaderBytes[1] << 16) | + (ExtraHeaderBytes[2] << 8) | ExtraHeaderBytes[3] + : ExtraHeaderBytes[0] | (ExtraHeaderBytes[1] << 8) | + (ExtraHeaderBytes[2] << 16) | (ExtraHeaderBytes[3] << 24); + } + + // ── Spawn positions @ 0xC0 (count derived from distance to checkpoints) ── + // The spawn block always starts at 0xC0 and ends where checkpoints begin. + // Most tracks have 6 entries; some have more (e.g. 10). + bs.Position = basePos + 0xC0; + int spawnCount = (checkpointsOffset - 0xC0) / RunwayStartingPosition.GetSize(); + for (int i = 0; i < spawnCount; i++) + SpawnPositions.Add(RunwayStartingPosition.FromStream(bs)); + + // ── Section helpers ──────────────────────────────────────────────── + int fileSize = (int)(stream.Length - basePos); + + // Use Distinct so that sections sharing the same offset only appear once; + // this ensures SectionEnd() returns the correct next boundary. + int[] allOffsets = new[] + { + checkpointsOffset, checkpointLookupOffset, + lightSetsOffset, unkOffset3, gadgetsOffset, + roadVertsOffset, roadTrisOffset, + clustersOffset, traversalDataOffset + }.Where(o => o > 0).Distinct().OrderBy(o => o).ToArray(); + + int SectionEnd(int off) + { + foreach (int o in allOffsets) + if (o > off) return o; + return fileSize; + } + + // ── Checkpoints ─────────────────────────────────────────────────── for (int i = 0; i < checkpointCount; i++) { - bs.Position = basePos + checkpointsOffset + (i * RunwayCheckpoint.GetSize()); - var vert = RunwayCheckpoint.FromStream(bs); - rwy.Checkpoints.Add(vert); + bs.Position = basePos + checkpointsOffset + i * RunwayCheckpoint.GetSize(); + Checkpoints.Add(RunwayCheckpoint.FromStream(bs)); } + // ── Checkpoint lookup indices ───────────────────────────────────── for (int i = 0; i < checkpointLookupIndicesCount; i++) { - bs.Position = basePos + checkpointLookupIndicesOffset + (i * sizeof(short)); - rwy.CheckpointLookupIndices.Add(bs.ReadInt16()); + bs.Position = basePos + checkpointLookupOffset + i * sizeof(short); + CheckpointLookupIndices.Add(bs.ReadInt16()); + } + + // ── Opaque blobs (light sets, unk3, gadgets) ────────────────────── + // Several of these pointers may share the same file offset (common in GT4 + // tracks that have no light-set or gadget data at that position). + // Each unique offset is read exactly once to avoid duplicating data on write. + // + // When a blob offset equals roadVertsOffset the blob is absent: the header + // simply points to the start of the road-vert data as a sentinel. Guard + // against this to avoid reading road-vert bytes as blob data (which would + // then be written twice in Write(), doubling the file size). + + _lightSetsSharesWithRoadVerts = lightSetsOffset > 0 && lightSetsOffset == roadVertsOffset; + _unk3SharesWithRoadVerts = unkOffset3 > 0 && unkOffset3 == roadVertsOffset; + _gadgetsSharesWithRoadVerts = gadgetsOffset > 0 && gadgetsOffset == roadVertsOffset; + + if (lightSetsOffset > 0 && !_lightSetsSharesWithRoadVerts) + { + bs.Position = basePos + lightSetsOffset; + LightSetsRawBytes = bs.ReadBytes(SectionEnd(lightSetsOffset) - lightSetsOffset); } + if (unkOffset3 > 0 && unkOffset3 != lightSetsOffset && !_unk3SharesWithRoadVerts) + { + bs.Position = basePos + unkOffset3; + UnkOffset3RawBytes = bs.ReadBytes(SectionEnd(unkOffset3) - unkOffset3); + } + + if (gadgetsOffset > 0 && gadgetsOffset != lightSetsOffset && gadgetsOffset != unkOffset3 && !_gadgetsSharesWithRoadVerts) + { + bs.Position = basePos + gadgetsOffset; + GadgetsRawBytes = bs.ReadBytes(SectionEnd(gadgetsOffset) - gadgetsOffset); + } + + // ── Road vertices ───────────────────────────────────────────────── for (int i = 0; i < roadVerticesCount; i++) { - bs.Position = basePos + roadVertsOffset + (i * RunwayRoadVert.GetSize()); - var vert = RunwayRoadVert.FromStream(bs); - rwy.Vertices.Add(vert); + bs.Position = basePos + roadVertsOffset + i * RunwayRoadVert.GetSize(); + Vertices.Add(RunwayRoadVert.FromStream(bs)); } + // ── Road tris ───────────────────────────────────────────────────── for (int i = 0; i < roadTriCount; i++) { - bs.Position = basePos + roadTrisOffset + (i * RunwayRoadTri.GetSize()); - var tri = RunwayRoadTri.FromStream(bs); - rwy.Tris.Add(tri); + bs.Position = basePos + roadTrisOffset + i * RunwayRoadTri.GetSize(); + Tris.Add(RunwayRoadTri.FromStream(bs)); } + // ── BSP traversal tree ──────────────────────────────────────────── + bs.Position = basePos + traversalDataOffset; + Root = TraverseRead(bs, TreeMaxDepth - 1); + + // ── Clusters (header + per-cluster tri-index list) ──────────────── for (int i = 0; i < clusterCount; i++) { - bs.Position = basePos + clustersOffset + (i * RunwayCluster.GetSize()); - var cluster = RunwayCluster.FromStream(bs); - rwy.Clusters.Add(cluster); + bs.Position = basePos + clustersOffset + i * RunwayCluster.GetSize(); + Clusters.Add(RunwayCluster.FromStream(bs)); } - bs.Position = traversalDataOffset; - rwy.Root = TraverseRead(bs, rwy.TreeMaxDepth - 1); + // ── Trailing data (after cluster tri-index arrays) ──────────────── + // The four offset words in ExtraHeaderBytes (0x84–0x93) point into this + // region, which holds physics/gadget/lighting data used for wall collision. + // It must be read and preserved verbatim; omitting it breaks wall collision. + if (_originalTrailingDataOffset > 0 && _originalTrailingDataOffset < fileSize) + { + bs.Position = basePos + _originalTrailingDataOffset; + TrailingRawBytes = bs.ReadBytes(fileSize - _originalTrailingDataOffset); + } - return rwy; + return this; } - private static Node TraverseRead(BinaryStream stream, int depthLeft) + // ── BSP tree helpers ───────────────────────────────────────────────────── + + private static Node TraverseRead(BinaryStream bs, int depthLeft) { if (depthLeft < 0) return null; var node = new Node(); - byte data = stream.Read1Byte(); - node.Axis = (byte)(data >> 6); + byte data = bs.Read1Byte(); + node.Axis = (byte)(data >> 6); node.Value = ((float)(data & 0b111111) + 0.5f) * 0.015625f; - node.Left = TraverseRead(stream, depthLeft - 1); - node.Right = TraverseRead(stream, depthLeft - 1); - + node.Left = TraverseRead(bs, depthLeft - 1); + node.Right = TraverseRead(bs, depthLeft - 1); return node; } + private static void TraverseWrite(BinaryStream bs, Node node, int depthLeft) + { + if (depthLeft < 0) + return; + + if (node == null) + { + // Write zeroed bytes for entire missing subtree (pre-order: root+left+right) + int nodeCount = (1 << (depthLeft + 1)) - 1; + for (int i = 0; i < nodeCount; i++) + bs.WriteByte(0); + return; + } + + // Reverse of read: valueBits = round(value / 0.015625) - 0.5 clamped to [0,63] + byte valueBits = (byte)Math.Clamp( + (int)MathF.Round(node.Value / 0.015625f - 0.5f), 0, 63); + bs.WriteByte((byte)((node.Axis << 6) | valueBits)); + + TraverseWrite(bs, node.Left, depthLeft - 1); + TraverseWrite(bs, node.Right, depthLeft - 1); + } + + // ── Writing ────────────────────────────────────────────────────────────── + /// - /// Searches the runway for a possible collision at the provided coordinates (Y will be within bounds) + /// Serialises the runway to starting at its current position. + /// All section offsets are recomputed from scratch so the file is self-consistent + /// after changes the checkpoint-lookup table size. + /// + /// Alignment: + /// The PS2 DMA engine requires 16-byte (quadword) alignment for the sections it + /// fetches directly. The writer inserts alignment padding before: + /// • the opaque-blob region (light-sets / unk3 / gadgets), + /// • road vertices, + /// • the BSP traversal tree, + /// • the trailing-data region. + /// Sections that follow naturally-aligned predecessors (road tris after road verts, + /// clusters after BSP) need no explicit padding. + /// + public void Write(Stream stream) + { + long basePos = stream.Position; + var bs = new BinaryStream(stream, + Magic == MAGIC_BE ? ByteConverter.Big : ByteConverter.Little); + + // Local helper: write a raw blob and return its offset from basePos, + // or 0 if the blob is absent. + int WriteBlob(byte[] data) + { + if (data == null || data.Length == 0) return 0; + int off = (int)(stream.Position - basePos); + stream.Write(data); + return off; + } + + // ── Spawn positions @ 0xC0 ─────────────────────────────────────── + stream.Position = basePos + 0xC0; + foreach (var sp in SpawnPositions) + sp.ToStream(bs); + // 6 × 0x10 = 0x60 bytes → stream now at basePos + 0x120 + + // ── Checkpoints ────────────────────────────────────────────────── + int checkpointsOffset = (int)(stream.Position - basePos); // = 0x120 + foreach (var cp in Checkpoints) + cp.ToStream(bs); + + // ── Checkpoint lookup indices ───────────────────────────────────── + int checkpointLookupOffset = (int)(stream.Position - basePos); + foreach (short idx in CheckpointLookupIndices) + bs.WriteInt16(idx); + + // ── 16-byte alignment before opaque blobs ───────────────────────── + // The checkpoint-lookup table's byte count may not be a multiple of 16 + // (e.g. after ReverseDirection adds entries). Pad to a quadword boundary + // so that the blob section and all subsequent sections remain PS2-aligned. + AlignStream(stream, 16); + + // ── Opaque blobs (light-sets, unk3, gadgets) ───────────────────── + // If two or three of these pointers shared the same file offset in the + // source file, write the blob only once and reuse the offset for the + // others. Writing duplicates shifts every section that follows. + // + // Blobs that originally pointed at roadVertsOffset (absent/sentinel) are + // not written here; their header offset is patched to roadVertsOffset after + // the road-vert section is placed. Use -1 as a sentinel meaning "copy from + // roadVertsOffset once that is known". + + const int kSharesWithRoadVerts = -1; + + int lightSetsWriteOff = _lightSetsSharesWithRoadVerts ? kSharesWithRoadVerts : WriteBlob(LightSetsRawBytes); + + int unkOffset3WriteOff; + if (_unk3SharesWithRoadVerts) + unkOffset3WriteOff = kSharesWithRoadVerts; + else if (_originalUnkOffset3 > 0 && _originalUnkOffset3 == _originalLightSetsOffset) + unkOffset3WriteOff = lightSetsWriteOff; // shared — reuse + else + unkOffset3WriteOff = WriteBlob(UnkOffset3RawBytes); + + int gadgetsWriteOff; + if (_gadgetsSharesWithRoadVerts) + gadgetsWriteOff = kSharesWithRoadVerts; + else if (_originalGadgetsOffset > 0 && _originalGadgetsOffset == _originalLightSetsOffset) + gadgetsWriteOff = lightSetsWriteOff; + else if (_originalGadgetsOffset > 0 && _originalGadgetsOffset == _originalUnkOffset3) + gadgetsWriteOff = unkOffset3WriteOff; + else + gadgetsWriteOff = WriteBlob(GadgetsRawBytes); + + // ── Road vertices (16-byte aligned) ────────────────────────────── + AlignStream(stream, 16); + int roadVertsOffset = (int)(stream.Position - basePos); + foreach (var v in Vertices) + v.ToStream(bs); + + // Patch any blob offsets that were deferred because they share roadVertsOffset. + if (lightSetsWriteOff == kSharesWithRoadVerts) lightSetsWriteOff = roadVertsOffset; + if (unkOffset3WriteOff == kSharesWithRoadVerts) unkOffset3WriteOff = roadVertsOffset; + if (gadgetsWriteOff == kSharesWithRoadVerts) gadgetsWriteOff = roadVertsOffset; + + // ── Road tris ───────────────────────────────────────────────────── + // Each RunwayRoadVert is 0x10 bytes, so tris always follow at a + // 16-byte-aligned address — no explicit padding needed here. + int roadTrisOffset = (int)(stream.Position - basePos); + foreach (var t in Tris) + t.ToStream(bs); + + // ── BSP traversal tree (16-byte aligned, 1< 0) + { + AlignStream(stream, 16); + int newTrailingOffset = (int)(stream.Position - basePos); + trailingDelta = newTrailingOffset - _originalTrailingDataOffset; + stream.Write(TrailingRawBytes); + } + + long endPos = stream.Position; + + // ── Header @ basePos ────────────────────────────────────────────── + stream.Position = basePos; + bs.WriteUInt32(Magic); // 0x00 + bs.WriteInt32(0); // 0x04 RelocPtr (runtime; always 0 on disk) + // Always use the actual file size so the game loads the complete file including + // trailing physics/collision data. If the reversed file is larger than the + // source (due to extra non-contiguous lookup entries), preserving the original + // RelocSize would cause the game to stop reading before the trailing data. + bs.WriteUInt32((uint)(endPos - basePos)); // 0x08 RelocSize + bs.WriteUInt32(Flags); // 0x0C + bs.WriteUInt32(Version); // 0x10 + bs.WriteSingle(TrackV); // 0x14 + bs.WriteSingle(StartVCoord); // 0x18 + bs.WriteSingle(GoalVCoord); // 0x1C + bs.WriteSingle(Bounds[0].X); // 0x20 + bs.WriteSingle(Bounds[0].Y); + bs.WriteSingle(Bounds[0].Z); + bs.WriteSingle(Bounds[1].X); // 0x2C + bs.WriteSingle(Bounds[1].Y); + bs.WriteSingle(Bounds[1].Z); + + // Counts @ 0x38 + bs.WriteInt16(CheckpointListCount); // 0x38 + bs.WriteInt16(UnkCount); // 0x3A + bs.WriteInt16((short)Checkpoints.Count); // 0x3C + bs.WriteInt16((short)CheckpointLookupIndices.Count); // 0x3E + bs.WriteInt32(Unk0x40); // 0x40 + bs.WriteInt16(GadgetsCount); // 0x44 + bs.WriteUInt16((ushort)Vertices.Count); // 0x46 + bs.WriteUInt16((ushort)Tris.Count); // 0x48 + bs.WriteUInt16((ushort)Clusters.Count); // 0x4A + bs.WriteByte(TreeMaxDepth); // 0x4C + + // 0x4D–0x5F unknown + if (UnknownHeader0x4D != null) + stream.Write(UnknownHeader0x4D, 0, Math.Min(UnknownHeader0x4D.Length, 0x13)); + else + stream.Write(new byte[0x13]); + + // Offset table @ 0x60 + stream.Position = basePos + 0x60; + bs.WriteInt32(checkpointsOffset); // 0x60 + bs.WriteInt32(checkpointLookupOffset); // 0x64 + bs.WriteInt32(lightSetsWriteOff); // 0x68 + bs.WriteInt32(unkOffset3WriteOff); // 0x6C + bs.WriteInt32(gadgetsWriteOff); // 0x70 + bs.WriteInt32(roadVertsOffset); // 0x74 + bs.WriteInt32(roadTrisOffset); // 0x78 + bs.WriteInt32(clustersOffset); // 0x7C + bs.WriteInt32(traversalDataOffset); // 0x80 + + // Extra header @ 0x84–0xBF + // Clone the stored bytes so we can patch without mutating the source data. + // The four pointer words (bytes 0–15 of the array = file offsets 0x84–0x93) + // are adjusted by the net delta between where the trailing data now lands + // versus where it was in the original file. + byte[] patchedExtra = ExtraHeaderBytes != null + ? (byte[])ExtraHeaderBytes.Clone() + : new byte[0x3C]; + + if (trailingDelta != 0) + PatchExtraHeaderOffsets(patchedExtra, trailingDelta); + + stream.Write(patchedExtra, 0, Math.Min(patchedExtra.Length, 0x3C)); + + stream.Position = endPos; + } + + // ── Direction reversal ─────────────────────────────────────────────────── + + /// + /// Transforms this runway so that it runs in the opposite direction: + /// + /// • Checkpoint order reversed (last→first); Left↔Right positions swapped on each gate; + /// V = (TrackV − oldV) mod TrackV. + /// • Checkpoint-pair indices in each cluster's lookup entry are remapped: + /// pair j → (N−2−j) for j < N−1; pair (N−1) stays as (N−1) (wrap pair). + /// • Non-contiguous transformed pair sets are pushed into new lookup entries. + /// • StartVCoord/GoalVCoord are transformed by the same V formula. + /// • Spawn-position rotations are flipped by π radians so cars face the new direction. + /// + /// The Left↔Right swap is essential: QuadSTCompute performs signed-area (cross-product) + /// tests that depend on correct winding order. Reversing traversal direction flips the + /// winding; swapping Left↔Right restores it, exactly equivalent to rotating each gate + /// 180° about its Middle point. + /// + /// Triangle UnkBits (SectorId / CpSubIndex) are NOT modified. Comparison against GT4's + /// own reverse-variant files confirms Polyphony leaves them unchanged: SectorId encodes + /// the physical mesh region, not the traversal order. + /// + /// Geometry (vertices, tris, clusters, BSP tree, bounds) and the collision/physics + /// trailing data are otherwise unchanged. + /// The ExtraHeaderBytes offset words are patched by Write() when it knows the + /// final layout, so no adjustment is needed here. + /// + public void ReverseDirection() + { + int N = Checkpoints.Count; + float totalTrackV = TrackV; + + // ── Step 1: Reverse checkpoint order, mirror V, swap Left↔Right ───── + // Left↔Right swap is required: QuadSTCompute uses signed cross-products + // whose winding depends on which side is "left" vs "right" relative to the + // travel direction. Reversing the traversal order flips the winding; + // swapping Left↔Right corrects it. Geometrically this is identical to + // rotating each gate 180° about its Middle point. + var reversed = new List(N); + for (int i = 0; i < N; i++) + { + var src = Checkpoints[N - 1 - i]; + reversed.Add(new RunwayCheckpoint + { + Left = src.Right, // swap: corrects QuadSTCompute winding + Middle = src.Middle, + Right = src.Left, // swap: corrects QuadSTCompute winding + TrackV = (totalTrackV - src.TrackV) % totalTrackV, + }); + } + Checkpoints = reversed; + + // ── Step 2: Transform StartVCoord / GoalVCoord ─────────────────── + StartVCoord = (totalTrackV - StartVCoord) % totalTrackV; + GoalVCoord = (totalTrackV - GoalVCoord) % totalTrackV; + + // ── Step 3: Flip spawn rotations by π; transform config gate records ─ + // The spawn block begins with zero or more configuration records followed by + // real starting-grid slots. A slot is a config record if it appears before + // the first slot with a genuine heading angle (0 < |Rot| ≤ 2π). + // + // Config records pack checkpoint-gate V-coordinates into their struct fields: + // |Rot| > 2π : Z and Rotation both hold gate V-coords (two gates per slot). + // Rot == 0 : X (and Z if nonzero) hold additional gate V-coords. + // + // The forward file stores gate V-coords in ascending order (traversal order) + // across the config slots, in field sequence: slot0.Z, slot0.Rot, slot1.X, … + // Reversal must: + // 1. Collect all gate values from all config slots in field order. + // 2. V-mirror each: new_v = TrackV - old_v + // 3. Reverse the list — so the smallest reversed-V (first gate reached in + // the reversed lap) goes into the first field position, preserving the + // ascending-V invariant that the game requires. + // 4. Write the reordered, mirrored values back into the same field slots. + // + // Real spawn slots: flip heading by π. + const float TwoPiF = MathF.PI * 2f; + + // Find the index of the first real spawn slot. + int firstRealSpawn = SpawnPositions.Count; + for (int i = 0; i < SpawnPositions.Count; i++) + { + float absRot = MathF.Abs(SpawnPositions[i].Rotation); + if (absRot > 0f && absRot <= TwoPiF) + { + firstRealSpawn = i; + break; + } + } + + // ── Collect all gate V-coords from config slots in field order ────── + // Each entry: (slot index, field name, forward V-value) + var gateFields = new List<(int slot, string field)>(); + var gateValues = new List(); + + for (int i = 0; i < firstRealSpawn; i++) + { + var sp = SpawnPositions[i]; + if (MathF.Abs(sp.Rotation) > TwoPiF) + { + gateFields.Add((i, "Z")); gateValues.Add(sp.Z); + gateFields.Add((i, "Rot")); gateValues.Add(sp.Rotation); + } + else // Rot == 0 + { + if (sp.X != 0f) { gateFields.Add((i, "X")); gateValues.Add(sp.X); } + if (sp.Z != 0f) { gateFields.Add((i, "Z")); gateValues.Add(sp.Z); } + } + } + + // ── V-mirror all values then reverse the list ───────────────────── + var newGateValues = new float[gateValues.Count]; + for (int g = 0; g < gateValues.Count; g++) + newGateValues[gateValues.Count - 1 - g] = totalTrackV - gateValues[g]; + + // ── Write back in the same field positions ───────────────────────── + for (int g = 0; g < gateFields.Count; g++) + { + var (idx, field) = gateFields[g]; + var sp = SpawnPositions[idx]; + switch (field) + { + case "Z": sp.Z = newGateValues[g]; break; + case "Rot": sp.Rotation = newGateValues[g]; break; + case "X": sp.X = newGateValues[g]; break; + } + } + + // ── Flip real spawn headings by π ───────────────────────────────── + for (int i = firstRealSpawn; i < SpawnPositions.Count; i++) + SpawnPositions[i].Rotation = NormaliseAngle(SpawnPositions[i].Rotation + MathF.PI); + + // ── Step 4: Remap checkpoint-pair indices in-place ─────────────────── + // Transform each lookup entry j: j < N-1 → N-2-j, j = N-1 → N-1. + // Cluster CheckpointLookupIndexStart / CheckpointLookupLength are left + // unchanged — the same windows into the table cover the correct reversed + // pairs. No entries are added, so the file size stays identical to the + // source. The source file's per-cluster ordering (including N-1 first + // in custom entries that span the start/finish boundary) is preserved, + // which is what getVCoord requires for correct V-coordinate tie-breaking. + for (int i = 0; i < CheckpointLookupIndices.Count; i++) + { + short j = CheckpointLookupIndices[i]; + CheckpointLookupIndices[i] = j < N - 1 ? (short)(N - 2 - j) : (short)(N - 1); + } + + // Triangle UnkBits (SectorId / CpSubIndex) are intentionally NOT modified. + // Empirical comparison against GT4's own reverse-variant files confirms that + // Polyphony leaves the triangle sector IDs unchanged when reversing a track: + // the SectorId encodes the physical timing-sector region on the mesh, not + // the traversal order. Inverting them shifts the T1/T2/T3 boundary by one + // checkpoint in the blueprint sector-map and does not match the originals. + } + + // ── Private helpers ────────────────────────────────────────────────────── + + private static float NormaliseAngle(float rad) + { + const float TwoPi = MathF.PI * 2f; + rad %= TwoPi; + if (rad > MathF.PI) rad -= TwoPi; + if (rad <= -MathF.PI) rad += TwoPi; + return rad; + } + + /// + /// Adjusts offset words in a clone of by + /// bytes. Only words whose stored value is + /// ≥ are touched — those are + /// confirmed pointers into the trailing-data region. Words that are zero + /// (absent) or smaller than that threshold are counts/constants (e.g. the + /// 3751-entry count at ExtraHdr[1] in 20r60r) and must not be modified. + /// Works in-place on ; call with a clone to avoid + /// mutating the stored source data. + /// + private void PatchExtraHeaderOffsets(byte[] bytes, int delta) + { + bool bigEndian = (Magic == MAGIC_BE); + for (int i = 0; i < 4; i++) + { + int bytePos = i * 4; + if (bytePos + 4 > bytes.Length) break; + + int val = bigEndian + ? (bytes[bytePos ] << 24) | (bytes[bytePos + 1] << 16) | + (bytes[bytePos + 2] << 8) | bytes[bytePos + 3] + : bytes[bytePos ] | (bytes[bytePos + 1] << 8) | + (bytes[bytePos + 2] << 16) | (bytes[bytePos + 3] << 24); + + // Skip zero (absent) and any word smaller than the start of the + // trailing-data region — those are counts/constants, not pointers. + if (val == 0 || val < _originalTrailingDataOffset) continue; + + val += delta; + + if (bigEndian) + { + bytes[bytePos ] = (byte)(val >> 24); + bytes[bytePos + 1] = (byte)(val >> 16); + bytes[bytePos + 2] = (byte)(val >> 8); + bytes[bytePos + 3] = (byte) val; + } + else + { + bytes[bytePos ] = (byte) val; + bytes[bytePos + 1] = (byte)(val >> 8); + bytes[bytePos + 2] = (byte)(val >> 16); + bytes[bytePos + 3] = (byte)(val >> 24); + } + } + } + + private static void AlignStream(Stream stream, int alignment) + { + long pos = stream.Position; + long aligned = (pos + alignment - 1) & ~(long)(alignment - 1); + long pad = aligned - pos; + if (pad > 0) + { + Span zeros = stackalloc byte[(int)pad]; + zeros.Clear(); + stream.Write(zeros); + } + } + + // ── Blueprint helpers ──────────────────────────────────────────────────── + + /// + /// Whether shares the same file offset as + /// in the source file. Used by the blueprint + /// exporter to generate the correct sharing flags in blueprint.yaml. + /// + public bool Unk3SharesOffsetWithLightsets => + _originalUnkOffset3 > 0 && _originalUnkOffset3 == _originalLightSetsOffset; + + /// + /// Whether shares the same file offset as + /// in the source file. + /// + public bool GadgetsSharesOffsetWithLightsets => + _originalGadgetsOffset > 0 && _originalGadgetsOffset == _originalLightSetsOffset; + + /// + /// Whether shares the same file offset as + /// (and not with lightsets) in the source file. + /// + public bool GadgetsSharesOffsetWithUnk3 => + _originalGadgetsOffset > 0 && + _originalGadgetsOffset == _originalUnkOffset3 && + _originalGadgetsOffset != _originalLightSetsOffset; + + /// + /// Configures original-offset tracking for blueprint round-trips. + /// Must be called after building a from a blueprint + /// (instead of ) so that can + /// correctly deduplicate blob sections and patch ExtraHeaderBytes offset words. + /// + /// Fake original offset for lightsets (nonzero = present). + /// Same value as lightSetsOffset if shared, otherwise distinct nonzero. + /// Same as lightsets/unk3 if shared, otherwise distinct nonzero. + /// + /// Value of the trailing-data pointer as it appears in ExtraHeaderBytes[0:4]. + /// Used by to distinguish pointer words + /// from count words (any word < this threshold is treated as a count). + /// + public void SetOriginalOffsets( + int lightSetsOffset, int unkOffset3, int gadgetsOffset, int trailingDataOffset) + { + _originalLightSetsOffset = lightSetsOffset; + _originalUnkOffset3 = unkOffset3; + _originalGadgetsOffset = gadgetsOffset; + _originalTrailingDataOffset = trailingDataOffset; + } + + /// + /// Returns raw bytes of the BSP traversal tree written in pre-order. + /// The length equals 2^TreeMaxDepth − 1 bytes (one byte per node). + /// Used by the blueprint exporter to produce bsp_tree.bin. + /// + public byte[] SaveBspToBytes() + { + using var ms = new MemoryStream(); + var bs = new BinaryStream(ms, + Magic == MAGIC_BE ? ByteConverter.Big : ByteConverter.Little); + TraverseWrite(bs, Root, TreeMaxDepth - 1); + return ms.ToArray(); + } + + /// + /// Loads the BSP traversal tree from raw bytes produced by . + /// must be set before calling. + /// Used by the blueprint builder to restore bsp_tree.bin. + /// + public void LoadBspFromBytes(byte[] bspBytes) + { + using var ms = new MemoryStream(bspBytes); + var bs = new BinaryStream(ms, + Magic == MAGIC_BE ? ByteConverter.Big : ByteConverter.Little); + Root = TraverseRead(bs, TreeMaxDepth - 1); + } + + // ── Collision / track-position query methods ───────────────────────────── + + /// + /// Searches the runway for a possible collision at the provided coordinates (Y will be within bounds). /// - /// - /// // GT4O US 0x293F90 public bool search(out RunwayResult result, Vector3 pos) { if (pos.Y >= Bounds[0].Y) { - Vector3 startPoint; - startPoint.X = pos.X; - startPoint.Y = Bounds[1].Y >= pos.Y ? pos.Y : Bounds[1].Y; - startPoint.Z = pos.Z; + Vector3 startPoint = new Vector3( + pos.X, + Bounds[1].Y >= pos.Y ? pos.Y : Bounds[1].Y, + pos.Z); - Vector3 endPoint; - endPoint.X = pos.X; - endPoint.Y = Bounds[0].Y; - endPoint.Z = pos.Z; + Vector3 endPoint = new Vector3(pos.X, Bounds[0].Y, pos.Z); int depth = TreeMaxDepth - 1; return traverse(out result, Bounds, startPoint, endPoint, Root, depth, 0); } - else - result = null; + result = null; return false; } // GT4O US 0x294128 - // Finds the cluster linked to provided positions? - public bool traverse(out RunwayResult result, Span bounds, Vector3 startPoint, Vector3 endPoint, Node node, int depth, short clusterIndex) + public bool traverse(out RunwayResult result, Span bounds, + Vector3 startPoint, Vector3 endPoint, + Node node, int depth, short clusterIndex) { if (depth < 0) return checkHit(out result, startPoint, endPoint, clusterIndex); - int axis = node.Axis; + int axis = node.Axis; float axisBoundsMin = bounds[0].GetAxis(axis); float axisBoundsMax = bounds[1].GetAxis(axis); float axisV1 = startPoint.GetAxis(axis); float axisV2 = endPoint.GetAxis(axis); - float pos = MathUtils.Lerp(axisBoundsMin, axisBoundsMax, node.Value); // (axisBoundsMin * (1.0f - node.Value)) + (axisBoundsMax * node.Value); - float v20 = axisV1 - pos; + float pos = MathUtils.Lerp(axisBoundsMin, axisBoundsMax, node.Value); + float v20 = axisV1 - pos; Node nextNode; - clusterIndex *= 2; Span nextBounds = stackalloc Vector3[2]; @@ -246,14 +933,12 @@ public bool traverse(out RunwayResult result, Span bounds, Vector3 star { nextNode = node.Left; depth--; - nextBounds[1].SetAxis(axis, pos); } else { nextNode = node.Right; depth--; - nextBounds[0].SetAxis(axis, pos); clusterIndex++; } @@ -266,52 +951,35 @@ public bool traverse(out RunwayResult result, Span bounds, Vector3 star return traverse(out result, nextBounds, startPoint, endPoint, nextNode, depth, clusterIndex); } - /// - /// Returns whether a vector hits collision within a specific cluster, and returns additional information about the point that was hit - /// - /// - /// - /// - /// - /// - /// // GT4O US 0x294508 public bool checkHit(out RunwayResult result, Vector3 startPoint, Vector3 endPoint, short clusterIndex) { RunwayCluster cluster = Clusters[clusterIndex]; - Vector3 vecDiff = endPoint - startPoint; - // Results Vector3 closest = Vector3.Zero; - short resTri = -1; - float v20 = float.NaN; - - float val1 = 0.0f, val2 = 0.0f, val3 = 0.0f; + short resTri = -1; + float v20 = float.NaN; + float val1 = 0, val2 = 0, val3 = 0; - // Iterate through all tris/faces for this cluster, check if we are colliding against any of them for (int i = 0; i < cluster.TriIndices.Length; i++) { - short currentTriIndex = cluster.TriIndices[i]; - RunwayRoadTri tri = Tris[currentTriIndex]; - + short currentTriIndex = cluster.TriIndices[i]; + RunwayRoadTri tri = Tris[currentTriIndex]; RunwayRoadVert p1 = Vertices[tri.Vert1]; RunwayRoadVert p2 = Vertices[tri.Vert2]; RunwayRoadVert p3 = Vertices[tri.Vert3]; - // Check 1 to 2 Vector3 a = p1.Vertex - startPoint; Vector3 b = p2.Vertex - startPoint; Vector3 c = p3.Vertex - startPoint; - Vector3 crossed = Vector3.Cross(vecDiff, a); // GT4Course::'anonymous_namespace'::outerProduct - float dot1 = Vector3.Dot(b, crossed); // GT4Course::'anonymous_namespace'::innerProduct + Vector3 crossed = Vector3.Cross(vecDiff, a); + float dot1 = Vector3.Dot(b, crossed); if (dot1 <= 0.0f) { - // Check 2 to 3 float dot2 = Vector3.Dot(c, crossed); - if (0.0f <= dot2) { crossed = Vector3.Cross(b, c); @@ -328,21 +996,18 @@ public bool checkHit(out RunwayResult result, Vector3 startPoint, Vector3 endPoi if (dot4 != 0.0f) { var last = Vector3.Dot(crossed, a); - if (0.0f < last || last < dot4) - continue; - + if (0.0f < last || last < dot4) continue; val = last / dot4; } if (float.IsNaN(v20) || val <= v20) { closest = crossed; - resTri = currentTriIndex; - v20 = val; - - val1 = dot1; - val2 = -dot2; - val3 = dot3; + resTri = currentTriIndex; + v20 = val; + val1 = dot1; + val2 = -dot2; + val3 = dot3; } } } @@ -352,19 +1017,17 @@ public bool checkHit(out RunwayResult result, Vector3 startPoint, Vector3 endPoi if (resTri >= 0) { result = new RunwayResult(); - result.HitPoint = new Vector3( - ((endPoint.X - startPoint.X) * v20) + startPoint.X, - ((endPoint.Y - startPoint.Y) * v20) + startPoint.Y, - ((endPoint.Z - startPoint.Z) * v20) + startPoint.Z - ); - result.Cluster = clusterIndex; + (endPoint.X - startPoint.X) * v20 + startPoint.X, + (endPoint.Y - startPoint.Y) * v20 + startPoint.Y, + (endPoint.Z - startPoint.Z) * v20 + startPoint.Z); + result.Cluster = clusterIndex; result.TriIndex = resTri; - var tri = Tris[resTri]; - RunwayRoadVert p1 = Vertices[tri.Vert1]; - RunwayRoadVert p2 = Vertices[tri.Vert2]; - RunwayRoadVert p3 = Vertices[tri.Vert3]; + var tri = Tris[resTri]; + RunwayRoadVert p1 = Vertices[tri.Vert1]; + RunwayRoadVert p2 = Vertices[tri.Vert2]; + RunwayRoadVert p3 = Vertices[tri.Vert3]; Vector3 adjusted = result.HitPoint; for (int axis = 0; axis < 3; axis++) @@ -373,36 +1036,25 @@ public bool checkHit(out RunwayResult result, Vector3 startPoint, Vector3 endPoi float f2 = p2.Vertex.GetAxis(axis); float f3 = p3.Vertex.GetAxis(axis); float res = result.HitPoint.GetAxis(axis); - float min, max; - if (f1 >= f2) - { - max = MathF.Max(f1, f3); - min = MathF.Min(f2, f3); - } - else - { - max = MathF.Max(f2, f3); - min = MathF.Min(f1, f3); - } - - float axisVal = MathF.Max(min, Math.Min(max, res)); - adjusted.SetAxis(axis, axisVal); + if (f1 >= f2) { max = MathF.Max(f1, f3); min = MathF.Min(f2, f3); } + else { max = MathF.Max(f2, f3); min = MathF.Min(f1, f3); } + adjusted.SetAxis(axis, MathF.Max(min, Math.Min(max, res))); } result.HitPoint = adjusted; float combined = val1 + val2 + val3; - float rsqrt = 1.0f / closest.Length(); // SQRT(closest.X * closest.X + closest.Y * closest.Y + closest.Z * closest.Z); + float rsqrt = 1.0f / closest.Length(); - float val = (float)p1.Unk2 / 255.0f; - val = val + (((float)p2.Unk2 / 255.0f - val) * val2 + ((float)p3.Unk2 / 255.0f - val) * val1) / combined; - result.Unk = Math.Clamp(val, 0.0f, 1.0f); // min.s + max.s + float v = (float)p1.Unk2 / 255.0f; + v += (((float)p2.Unk2 / 255.0f - v) * val2 + ((float)p3.Unk2 / 255.0f - v) * val1) / combined; + result.Unk = Math.Clamp(v, 0.0f, 1.0f); result.UnkVec = closest * rsqrt; - val = (float)(p1.Unk3 & 0x7F) / 127.0f; - val = val + (((float)(p2.Unk3 & 0x7F) / 127.0f - val) * val2 + ((float)(p3.Unk3 & 0x7F) / 127.0f - val) * val1) / combined; - result.Unk2 = Math.Clamp(val, 0.0f, 1.0f); // min.s + max.s + v = (float)(p1.Unk3 & 0x7F) / 127.0f; + v += (((float)(p2.Unk3 & 0x7F) / 127.0f - v) * val2 + ((float)(p3.Unk3 & 0x7F) / 127.0f - v) * val1) / combined; + result.Unk2 = Math.Clamp(v, 0.0f, 1.0f); result.Unk3 = v20; result.Unk4 = -Vector3.Dot(result.UnkVec, p1.Vertex); return true; @@ -412,55 +1064,41 @@ public bool checkHit(out RunwayResult result, Vector3 startPoint, Vector3 endPoi return false; } - /// - /// Gets the vcoord (meter) given a position. - /// - /// Position on the track. - /// "Hint" for searching with the cluster and tri index. If default, will search for that first. - /// + /// Gets the VCoord (metres from start) for a world position. public float getVCoord(Vector3 position, RunwayHint hint) { if ((hint.Cluster & 0x8000) == -1 || hint.TriIndex == -1) { search(out RunwayResult result, position); - if (result.Cluster == -1 || result.TriIndex == -1) - return 0.0f; - + if (result.Cluster == -1 || result.TriIndex == -1) return 0.0f; hint.TriIndex = result.TriIndex; - hint.Cluster = result.Cluster; + hint.Cluster = result.Cluster; } float lowest = float.NaN; float vcoord = 0.0f; - // We have a cluster, loop through all the checkpoints linked to it - // We'll use them to calculate the vcoord RunwayCluster cluster = Clusters[hint.Cluster]; - for (int i = cluster.CheckpointLookupIndexStart; i < cluster.CheckpointLookupIndexStart + cluster.CheckpointLookupLength; i++) + for (int i = cluster.CheckpointLookupIndexStart; + i < cluster.CheckpointLookupIndexStart + cluster.CheckpointLookupLength; i++) { - var index = CheckpointLookupIndices[i]; - RunwayCheckpoint cp = Checkpoints[index]; - RunwayCheckpoint nextcp = index + 1 != Checkpoints.Count ? Checkpoints[index + 1] : Checkpoints[0]; + var index = CheckpointLookupIndices[i]; + RunwayCheckpoint cp = Checkpoints[index]; + RunwayCheckpoint nextcp = index + 1 != Checkpoints.Count + ? Checkpoints[index + 1] : Checkpoints[0]; - // Check each side (aka each rectangle) for a possible hit - if (QuadSTCompute(out Vector3 result, position, cp.Left, nextcp.Left, cp.Middle, nextcp.Middle) || - QuadSTCompute(out result, position, cp.Middle, nextcp.Middle, cp.Right, nextcp.Right)) + if (QuadSTCompute(out Vector3 result, position, cp.Left, nextcp.Left, cp.Middle, nextcp.Middle) || + QuadSTCompute(out result, position, cp.Middle, nextcp.Middle, cp.Right, nextcp.Right)) { float current = MathF.Abs(result.Z); if (float.IsNaN(lowest) || current < lowest) { vcoord = cp.TrackV; float nextV = nextcp.TrackV; - - if (nextcp.TrackV < cp.TrackV) - nextV = this.TrackV; - + if (nextcp.TrackV < cp.TrackV) nextV = this.TrackV; vcoord += (nextV - vcoord) * result.X; - lowest = current; - - // Loop back - if (vcoord > this.TrackV) - vcoord -= this.TrackV; + lowest = current; + if (vcoord > this.TrackV) vcoord -= this.TrackV; } } } @@ -468,17 +1106,8 @@ public float getVCoord(Vector3 position, RunwayHint hint) return vcoord; } - /// - /// - /// - /// - /// Position - /// Rect P1 - /// Rect P2 - /// Rect P3 - /// Rect P4 - /// - public static bool QuadSTCompute(out Vector3 result, Vector3 pos, Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4) + public static bool QuadSTCompute(out Vector3 result, Vector3 pos, + Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4) { float p3p1XDiff = p3.X - p1.X; float p3p1ZDiff = p3.Z - p1.Z; @@ -486,7 +1115,6 @@ public static bool QuadSTCompute(out Vector3 result, Vector3 pos, Vector3 p1, Ve float p2p4ZDiff = p2.Z - p4.Z; result = default; - if (p3p1XDiff == 0.0 && p3p1ZDiff == 0.0 && p2p4XDiff == 0.0 && p2p4ZDiff == 0.0f) return false; @@ -529,15 +1157,14 @@ public static bool QuadSTCompute(out Vector3 result, Vector3 pos, Vector3 p1, Ve float unk5 = p2p1ZDiff * p3p1XDiff - p2p1XDiff * p3p1ZDiff; float unk6 = aa + unk5; float unk7 = MathF.Sqrt(unk6 * unk6 + unk2 * 4.0f * (p3p1ZDiff * v1 - p3p1XDiff - v2)); - result.X = (unk + unk) / ((unk5 - aa) - unk7); // Progress between previous and next cp (0.0 to 1.0) + result.X = (unk + unk) / ((unk5 - aa) - unk7); result.Y = (unk2 * -2.0f) / (unk6 - unk7); } - result.Z = pos.Y - ((1.0f - result.X) * (1.0f - result.Y) * p1.Y - + result.X * (1.0f - result.Y) * p2.Y - + (1.0f - result.X) * result.Y * p3.Y - + result.X * result.Y * p4.Y); - + result.Z = pos.Y - ((1.0f - result.X) * (1.0f - result.Y) * p1.Y + + result.X * (1.0f - result.Y) * p2.Y + + (1.0f - result.X) * result.Y * p3.Y + + result.X * result.Y * p4.Y); return true; } } @@ -547,27 +1174,28 @@ public static bool QuadSTCompute(out Vector3 result, Vector3 pos, Vector3 p1, Ve return false; } + // ── Nested types ───────────────────────────────────────────────────────── + public class Node { - public byte Axis; + public byte Axis; public float Value; - public Node Left; - public Node Right; + public Node Left; + public Node Right; } public class RunwayResult { - public Vector3 HitPoint { get; set; } // 0x00 - public byte TriUnk { get; set; } // 0x0C - public byte TriUnk2 { get; set; } // 0x10 - public float Unk { get; set; } // 0x14 - public float Unk2 { get; set; } // 0x18 - public Vector3 UnkVec { get; set; } // 0x1C - public float Unk4 { get; set; } // 0x28 - public float Unk3 { get; set; } // 0x2C - - public short TriIndex { get; set; } = -1; // 0x30 - public short Cluster { get; set; } = -1; // 0x32 + public Vector3 HitPoint { get; set; } // 0x00 + public byte TriUnk { get; set; } // 0x0C + public byte TriUnk2 { get; set; } // 0x10 + public float Unk { get; set; } // 0x14 + public float Unk2 { get; set; } // 0x18 + public Vector3 UnkVec { get; set; } // 0x1C + public float Unk4 { get; set; } // 0x28 + public float Unk3 { get; set; } // 0x2C + public short TriIndex { get; set; } = -1; // 0x30 + public short Cluster { get; set; } = -1; // 0x32 } public struct RunwayHint @@ -578,14 +1206,13 @@ public struct RunwayHint public RunwayHint(short triIndex, short cluster) { TriIndex = triIndex; - Cluster = cluster; + Cluster = cluster; } public RunwayHint() { TriIndex = -1; - Cluster = -1; + Cluster = -1; } } } - diff --git a/PDTools.Files/Courses/PS2/Runway/RunwayRoadTri.cs b/PDTools.Files/Courses/PS2/Runway/RunwayRoadTri.cs index 934d3135..0e100899 100644 --- a/PDTools.Files/Courses/PS2/Runway/RunwayRoadTri.cs +++ b/PDTools.Files/Courses/PS2/Runway/RunwayRoadTri.cs @@ -14,7 +14,20 @@ public class RunwayRoadTri public ushort Vert1 { get; set; } public ushort Vert2 { get; set; } public ushort Vert3 { get; set; } - public byte UnkBits { get; set; } // Returned from runway search - 5 bits and 3 bits + /// + /// Byte packed as [7:5] SectorId (3 bits, 0–7) | [4:0] CpSubIndex (5 bits, 0–31). + /// Returned in the runway-search result as TriUnk (sector) / TriUnk2 (sub-index). + /// SectorId identifies the in-game timing sector (0 = T1, 1 = T2, …) for this triangle. + /// CpSubIndex is the local checkpoint sub-index used for V-coord interpolation. + /// + public byte UnkBits { get; set; } + + /// High 3 bits of : in-game timing sector ID (0–7). + public byte SectorId => (byte)(UnkBits >> 5); + + /// Low 5 bits of : checkpoint sub-index within the sector (0–31). + public byte CpSubIndex => (byte)(UnkBits & 0x1F); + public byte Unk { get; set; } // Returned from runway search public uint Flags { get; set; } @@ -30,6 +43,16 @@ public static RunwayRoadTri FromStream(BinaryStream bs) return tri; } + public void ToStream(BinaryStream bs) + { + bs.WriteUInt16(Vert1); + bs.WriteUInt16(Vert2); + bs.WriteUInt16(Vert3); + bs.WriteByte(UnkBits); + bs.WriteByte(Unk); + bs.WriteUInt32(Flags); + } + public static int GetSize() { return 0x0C; diff --git a/PDTools.Files/Courses/PS2/Runway/RunwayRoadVert.cs b/PDTools.Files/Courses/PS2/Runway/RunwayRoadVert.cs index 93d000ab..0c4e2391 100644 --- a/PDTools.Files/Courses/PS2/Runway/RunwayRoadVert.cs +++ b/PDTools.Files/Courses/PS2/Runway/RunwayRoadVert.cs @@ -26,6 +26,16 @@ public static RunwayRoadVert FromStream(BinaryStream bs) return vert; } + public void ToStream(BinaryStream bs) + { + bs.WriteSingle(Vertex.X); + bs.WriteSingle(Vertex.Y); + bs.WriteSingle(Vertex.Z); + bs.WriteInt16(Unk); + bs.WriteByte(Unk2); + bs.WriteByte(Unk3); + } + public static int GetSize() { return 0x10; diff --git a/PDTools.Files/Courses/PS2/Runway/RunwayStartingPosition.cs b/PDTools.Files/Courses/PS2/Runway/RunwayStartingPosition.cs new file mode 100644 index 00000000..ba843547 --- /dev/null +++ b/PDTools.Files/Courses/PS2/Runway/RunwayStartingPosition.cs @@ -0,0 +1,41 @@ +using Syroot.BinaryData; + +namespace PDTools.Files.Courses.PS2.Runway; + +/// +/// One spawn / starting-grid position inside a RNW4 file. +/// Six of these are stored contiguously at file offset 0xC0 (each 16 bytes = 4 floats). +/// +public class RunwayStartingPosition +{ + public float X { get; set; } + public float Y { get; set; } + public float Z { get; set; } + + /// + /// Heading angle of the spawned car, in radians (positive = counter-clockwise from +Z). + /// + public float Rotation { get; set; } + + public static RunwayStartingPosition FromStream(BinaryStream bs) + { + return new RunwayStartingPosition + { + X = bs.ReadSingle(), + Y = bs.ReadSingle(), + Z = bs.ReadSingle(), + Rotation = bs.ReadSingle(), + }; + } + + public void ToStream(BinaryStream bs) + { + bs.WriteSingle(X); + bs.WriteSingle(Y); + bs.WriteSingle(Z); + bs.WriteSingle(Rotation); + } + + /// Size of one record in bytes (always 0x10). + public static int GetSize() => 0x10; +} diff --git a/PDTools.Files/Courses/PS2/console-example-internal.log b/PDTools.Files/Courses/PS2/console-example-internal.log new file mode 100644 index 00000000..e35a7092 --- /dev/null +++ b/PDTools.Files/Courses/PS2/console-example-internal.log @@ -0,0 +1,14 @@ +2026-06-06 17:47:42.6214 Info Registered target NLog.Targets.ColoredConsoleTarget(Name=logconsole) +2026-06-06 17:47:42.6214 Warn Error has been raised. Exception: NLog.NLogConfigurationException: Target 'logfile' not found for logging rule: *. +2026-06-06 17:47:42.6343 Info NLog, Version=5.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c. File version: 5.2.5.2160. Product version: 5.2.5+7c014325443d65541200b698a50b9dd4ec30c7f0. GlobalAssemblyCache: False +2026-06-06 17:47:42.6343 Info Validating config: TargetNames=logconsole, ConfigItems=8, FilePath=C:\Users\User\Desktop\gtps2modeltool2\GTPS2ModelTool\bin\Debug\net9.0\NLog.config +2026-06-06 17:47:42.6508 Info Configuration initialized. +2026-06-06 17:47:43.2609 Info AppDomain Shutting down. LogFactory closing... +2026-06-06 17:47:43.2609 Info LogFactory has been closed. +2026-06-06 17:47:43.3892 Info Registered target NLog.Targets.ColoredConsoleTarget(Name=logconsole) +2026-06-06 17:47:43.3999 Warn Error has been raised. Exception: NLog.NLogConfigurationException: Target 'logfile' not found for logging rule: *. +2026-06-06 17:47:43.3999 Info NLog, Version=5.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c. File version: 5.2.5.2160. Product version: 5.2.5+7c014325443d65541200b698a50b9dd4ec30c7f0. GlobalAssemblyCache: False +2026-06-06 17:47:43.3999 Info Validating config: TargetNames=logconsole, ConfigItems=8, FilePath=C:\Users\User\Desktop\gtps2modeltool2\GTPS2ModelTool\bin\Debug\net9.0\NLog.config +2026-06-06 17:47:43.4190 Info Configuration initialized. +2026-06-06 17:47:43.4691 Info AppDomain Shutting down. LogFactory closing... +2026-06-06 17:47:43.4691 Info LogFactory has been closed. diff --git a/PDTools.Files/Models/PS2/CarModel1/CarModel4.cs b/PDTools.Files/Models/PS2/CarModel1/CarModel4.cs new file mode 100644 index 00000000..a94c5314 --- /dev/null +++ b/PDTools.Files/Models/PS2/CarModel1/CarModel4.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using PDTools.Files.Models.PS2.ModelSet; + +namespace PDTools.Files.Models.PS2.CarModel1; + +/// +/// GT4 Car Model container (CAR4 / "CAR4" magic). +/// +/// The file is a 0x40-byte header followed by a variable number of sub-sections. +/// Each field in the header is an absolute file offset; zero means the section is absent. +/// +/// Sub-sections are stored here as raw byte arrays so the container can be round-tripped +/// losslessly even when a particular sub-format is not yet fully understood. +/// Helper Get*() methods parse the byte arrays on demand for callers that need the +/// object-model view (e.g. the model dumper). +/// +public class CarModel4 +{ + /// "CAR4" little-endian magic. + public const uint MAGIC = 0x34524143u; + + /// Size of the CAR4 file header. + public const uint HeaderSize = 0x40u; + + // ── Sub-section raw bytes (null = section absent) ─────────────────────── + + /// 0x10 — GT4 car-info block (contains lights, cameras, exhaust positions, etc.). + public byte[] CarInfoBytes { get; set; } + + /// 0x14 — Collision boundary data. + public byte[] CollisionModelBytes { get; set; } + + /// 0x18 — Main body ModelSet2 (MDLS). + public byte[] MainModelSetBytes { get; set; } + + /// 0x1C — Color-patch table for the main model set (internal format unknown). + public byte[] MainModelColorPatchBytes { get; set; } + + /// 0x20 — Wheel ModelSet2 (MDLS). + public byte[] WheelModelSetBytes { get; set; } + + /// 0x24 — Color-patch table for the wheel model set. + public byte[] WheelColorPatchBytes { get; set; } + + /// 0x28 — Wing / spoiler ModelSet2 (MDLS, optional). + public byte[] WingModelSetBytes { get; set; } + + /// 0x2C — Tire ModelSet2 slot 0 (optional). + public byte[] TireModelSet0Bytes { get; set; } + + /// 0x30 — Tire ModelSet2 slot 1 (optional). + public byte[] TireModelSet1Bytes { get; set; } + + /// 0x34 — Built-in driver model (optional). + public byte[] BuiltinDriverModelBytes { get; set; } + + /// 0x38 — Extra texture set (optional). + public byte[] TexSetBytes { get; set; } + + // ── Reading ────────────────────────────────────────────────────────────── + + /// + /// Parses a CAR4 from starting at its current position. + /// Each sub-section is stored as a raw byte array (offset → next_offset). + /// + public void FromStream(Stream stream) + { + long basePos = stream.Position; + + using var br = new BinaryReader(stream, System.Text.Encoding.UTF8, leaveOpen: true); + + uint magic = br.ReadUInt32(); + if (magic != MAGIC) + throw new InvalidDataException($"Not a CAR4 file (magic 0x{magic:X8}, expected 0x{MAGIC:X8})."); + + br.ReadUInt32(); // 0x04 RelocatePtr (runtime) + br.ReadUInt32(); // 0x08 FileSizeForReloc (runtime) + br.ReadUInt32(); // 0x0C Empty + + uint carInfoOff = br.ReadUInt32(); // 0x10 + uint collisionModelOff = br.ReadUInt32(); // 0x14 + uint mainModelSetOff = br.ReadUInt32(); // 0x18 + uint mainColorPatchOff = br.ReadUInt32(); // 0x1C + uint wheelModelSetOff = br.ReadUInt32(); // 0x20 + uint wheelColorPatchOff = br.ReadUInt32(); // 0x24 + uint wingModelSetOff = br.ReadUInt32(); // 0x28 + uint tire0Off = br.ReadUInt32(); // 0x2C + uint tire1Off = br.ReadUInt32(); // 0x30 + uint builtinDriverOff = br.ReadUInt32(); // 0x34 + uint texSetOff = br.ReadUInt32(); // 0x38 + // 0x3C: padding — header ends at 0x40 + + uint fileSize = (uint)(stream.Length - basePos); + + // Build the sorted set of all present section start offsets so we can + // derive each section's byte range as [start, nextStart). + uint[] allOffsets = + [ + carInfoOff, collisionModelOff, mainModelSetOff, mainColorPatchOff, + wheelModelSetOff, wheelColorPatchOff, wingModelSetOff, + tire0Off, tire1Off, builtinDriverOff, texSetOff + ]; + + CarInfoBytes = ReadSection(stream, basePos, carInfoOff, allOffsets, fileSize); + CollisionModelBytes = ReadSection(stream, basePos, collisionModelOff, allOffsets, fileSize); + MainModelSetBytes = ReadSection(stream, basePos, mainModelSetOff, allOffsets, fileSize); + MainModelColorPatchBytes = ReadSection(stream, basePos, mainColorPatchOff, allOffsets, fileSize); + WheelModelSetBytes = ReadSection(stream, basePos, wheelModelSetOff, allOffsets, fileSize); + WheelColorPatchBytes = ReadSection(stream, basePos, wheelColorPatchOff, allOffsets, fileSize); + WingModelSetBytes = ReadSection(stream, basePos, wingModelSetOff, allOffsets, fileSize); + TireModelSet0Bytes = ReadSection(stream, basePos, tire0Off, allOffsets, fileSize); + TireModelSet1Bytes = ReadSection(stream, basePos, tire1Off, allOffsets, fileSize); + BuiltinDriverModelBytes = ReadSection(stream, basePos, builtinDriverOff, allOffsets, fileSize); + TexSetBytes = ReadSection(stream, basePos, texSetOff, allOffsets, fileSize); + } + + /// + /// Reads the bytes for one section. The range is [sectionOffset, nextLargerOffset). + /// Trailing alignment padding between sections is included in the extracted bytes, + /// ensuring that writing the bytes back produces an identical file layout. + /// + private static byte[] ReadSection(Stream stream, long basePos, + uint sectionOffset, uint[] allOffsets, uint fileSize) + { + if (sectionOffset == 0) + return null; + + // Next section start = smallest offset that is strictly greater than this one + uint nextOffset = allOffsets + .Where(o => o > sectionOffset) + .DefaultIfEmpty(fileSize) // fall back to end-of-file + .Min(); + + int size = (int)(nextOffset - sectionOffset); + if (size <= 0) + return null; + + stream.Position = basePos + sectionOffset; + byte[] data = new byte[size]; + int read = 0; + while (read < size) + { + int n = stream.Read(data, read, size - read); + if (n == 0) + break; + read += n; + } + return data; + } + + // ── Writing ────────────────────────────────────────────────────────────── + + /// + /// Serialises the CAR4 to . + /// Sub-sections are written sequentially starting at offset 0x40; the 0x40-byte + /// header is written last (so offsets can be determined during the write pass). + /// + /// Sections are aligned to 0x40 bytes. When sub-sections were extracted from an + /// original file (and therefore already contain trailing alignment padding), the + /// alignment step is a no-op and the output is bit-identical to the source. + /// When sections are freshly built (no trailing padding), alignment bytes are + /// inserted automatically. + /// + public void Write(Stream stream) + { + long basePos = stream.Position; + stream.Position = basePos + HeaderSize; // leave header space + + uint carInfoOff = WriteSectionAligned(stream, CarInfoBytes, basePos, 0x80); + uint collisionModelOff = WriteSectionAligned(stream, CollisionModelBytes, basePos, 0x40); + uint mainModelSetOff = WriteSectionAligned(stream, MainModelSetBytes, basePos, 0x40); + uint mainColorPatchOff = WriteSectionAligned(stream, MainModelColorPatchBytes, basePos, 0x40); + uint wheelModelSetOff = WriteSectionAligned(stream, WheelModelSetBytes, basePos, 0x40); + uint wheelColorPatchOff = WriteSectionAligned(stream, WheelColorPatchBytes, basePos, 0x40); + uint wingModelSetOff = WriteSectionAligned(stream, WingModelSetBytes, basePos, 0x40); + uint tire0Off = WriteSectionAligned(stream, TireModelSet0Bytes, basePos, 0x40); + uint tire1Off = WriteSectionAligned(stream, TireModelSet1Bytes, basePos, 0x40); + uint builtinDriverOff = WriteSectionAligned(stream, BuiltinDriverModelBytes, basePos, 0x40); + uint texSetOff = WriteSectionAligned(stream, TexSetBytes, basePos, 0x40); + + long endPos = stream.Position; + + // Write the 0x40-byte header + stream.Position = basePos; + using var bw = new BinaryWriter(stream, System.Text.Encoding.UTF8, leaveOpen: true); + bw.Write(MAGIC); // 0x00 "CAR4" + bw.Write(0u); // 0x04 RelocatePtr (runtime fills) + bw.Write((uint)endPos); // 0x08 FileSizeForReloc + bw.Write(0u); // 0x0C Empty + bw.Write(carInfoOff); // 0x10 + bw.Write(collisionModelOff); // 0x14 + bw.Write(mainModelSetOff); // 0x18 + bw.Write(mainColorPatchOff); // 0x1C + bw.Write(wheelModelSetOff); // 0x20 + bw.Write(wheelColorPatchOff); // 0x24 + bw.Write(wingModelSetOff); // 0x28 + bw.Write(tire0Off); // 0x2C + bw.Write(tire1Off); // 0x30 + bw.Write(builtinDriverOff); // 0x34 + bw.Write(texSetOff); // 0x38 + bw.Write(0u); // 0x3C padding → header ends at 0x40 + + stream.Position = endPos; + } + + /// + /// Aligns the stream position to , then writes + /// and returns the section's absolute offset + /// (relative to ), or 0 if data is null/empty. + /// + private static uint WriteSectionAligned(Stream stream, byte[] data, long basePos, int alignment) + { + if (data == null || data.Length == 0) + return 0; + + AlignStream(stream, alignment); + uint offset = (uint)(stream.Position - basePos); + stream.Write(data); + return offset; + } + + private static void AlignStream(Stream stream, int alignment) + { + long pos = stream.Position; + long aligned = (pos + alignment - 1) & ~(long)(alignment - 1); + long padding = aligned - pos; + if (padding > 0) + stream.Write(new byte[padding]); + } + + // ── On-demand parsed helpers ───────────────────────────────────────────── + + /// Parses and returns the main body ModelSet2, or null if absent. + public ModelSet2 GetMainModelSet() => ParseModelSet2(MainModelSetBytes); + + /// Parses and returns the wheel ModelSet2, or null if absent. + public ModelSet2 GetWheelModelSet() => ParseModelSet2(WheelModelSetBytes); + + /// Parses and returns the wing ModelSet2, or null if absent. + public ModelSet2 GetWingModelSet() => ParseModelSet2(WingModelSetBytes); + + /// Parses and returns tire ModelSet2 slot 0, or null if absent. + public ModelSet2 GetTireModelSet0() => ParseModelSet2(TireModelSet0Bytes); + + /// Parses and returns tire ModelSet2 slot 1, or null if absent. + public ModelSet2 GetTireModelSet1() => ParseModelSet2(TireModelSet1Bytes); + + private static ModelSet2 ParseModelSet2(byte[] data) + { + if (data == null || data.Length < 8) + return null; + + try + { + using var ms = new MemoryStream(data); + var modelSet = new ModelSet2(); + modelSet.FromStream(ms); + return modelSet; + } + catch + { + return null; + } + } +} diff --git a/PDTools.Files/Models/PS2/ModelSet/ModelSet1.cs b/PDTools.Files/Models/PS2/ModelSet/ModelSet1.cs index bfea110a..b46a3f84 100644 --- a/PDTools.Files/Models/PS2/ModelSet/ModelSet1.cs +++ b/PDTools.Files/Models/PS2/ModelSet/ModelSet1.cs @@ -212,6 +212,10 @@ public override int GetMaterialCount() public override List GetVariationMaterials(int varIndex) { + // Track models have no variation materials (VariationMaterialsTable is empty). + // Fall back to the base Materials list so the dumper doesn't crash. + if (varIndex >= VariationMaterialsTable.Count) + return Materials; return VariationMaterialsTable[varIndex]; } diff --git a/PDTools.Files/Models/PS2/ModelSet/ModelSet2Model.cs b/PDTools.Files/Models/PS2/ModelSet/ModelSet2Model.cs index 8f4cd4c2..9e0542b9 100644 --- a/PDTools.Files/Models/PS2/ModelSet/ModelSet2Model.cs +++ b/PDTools.Files/Models/PS2/ModelSet/ModelSet2Model.cs @@ -165,14 +165,13 @@ private void setBound(float[] min, float[] max) } float[] dist = new float[3]; - float total_UNUSED = 0.0f; float final = 0.0f; for (i = 0; i < 3; i++) { float center = (min[i] + max[i]) * 0.5f; dist[i] = center; - final = (max[i] - center) * (max[i] - center); - total_UNUSED += final; + float half = max[i] - center; + final += half * half; // Fixed: accumulate all three axes (was replacing each iteration) } // Not original, added for convenience @@ -181,6 +180,23 @@ private void setBound(float[] min, float[] max) Unk = MathF.Sqrt(final); } + /// + /// Sets the model origin and bounding sphere radius from an axis-aligned bounding box. + /// Skips populating the 8 corner vectors (Bounds list stays empty). + /// + public void SetOriginAndRadius(Vector3 min, Vector3 max) + { + float cx = (min.X + max.X) * 0.5f; + float cy = (min.Y + max.Y) * 0.5f; + float cz = (min.Z + max.Z) * 0.5f; + Origin = new Vector3(cx, cy, cz); + + float dx = max.X - cx; + float dy = max.Y - cy; + float dz = max.Z - cz; + Unk = MathF.Sqrt(dx * dx + dy * dy + dz * dz); + } + public static uint GetSize() { return 0x28; diff --git a/PDTools.Files/Models/PS2/ModelSet/ModelSet2Serializer.cs b/PDTools.Files/Models/PS2/ModelSet/ModelSet2Serializer.cs index 746755b3..3370e240 100644 --- a/PDTools.Files/Models/PS2/ModelSet/ModelSet2Serializer.cs +++ b/PDTools.Files/Models/PS2/ModelSet/ModelSet2Serializer.cs @@ -63,8 +63,10 @@ public void Write(Stream stream) bs.WriteUInt32(ModelSet2.MAGIC); bs.WriteUInt32((uint)(relocationInfoOffset - _baseMdlPos)); bs.WriteUInt32(relocationDataSize); - bs.WriteUInt32(0); // Relocatrion base + bs.WriteUInt32(0); // Relocation base bs.WriteUInt32((uint)lastPos); // File size + bs.WriteByte(0); // 0x14: unk + bs.WriteByte(_modelSet.InstanceFlags); // 0x15: InstanceFlags (preserves original or 0 for new models) bs.Position = lastPos; } @@ -74,28 +76,54 @@ private void WriteInstance(BinaryStream bs) long instanceOffset = bs.Position; int size = _modelSet.GetInstanceSize(); - bs.Position = instanceOffset + 0x20; + // Instance header: 0x20 bytes (explicit zero-init; runtime fills +0x00 and +0x10) + bs.WriteInt32(0); // +0x00: parentModelSetPtr (runtime fills this) + long outRegsPtrPos = bs.Position; + bs.WriteInt32(0); // +0x04: outRegistersPtr (patched below if registers exist) + long unkRegsPtrPos = bs.Position; + bs.WriteInt32(0); // +0x08: unkRegistersPtr (patched below if registers exist) + long hostMethodRegsPtrPos = bs.Position; + bs.WriteInt32(0); // +0x0C: hostMethodRegistersPtr (patched below if registers exist) + bs.WriteInt32(0); // +0x10: hostMethodInfosFuncs (runtime fills this) + bs.WriteInt32(0); // +0x14 + bs.WriteInt32(0); // +0x18 + bs.WriteInt32(0); // +0x1C + + // Register arrays follow the 0x20-byte header long outRegistersOffset = bs.Position; for (int i = 0; i < _modelSet.OutRegisterInfos.Count; i++) bs.WriteUInt32(0); + long unkRegistersOffset = bs.Position; + for (int i = 0; i < _modelSet.InstanceUnkRegisterCount; i++) // Fixed: was missing entirely + bs.WriteUInt32(0); + long hostMethodRegistersOffset = bs.Position; for (int i = 0; i < _modelSet.HostMethodInfos.Count; i++) bs.WriteUInt32(0); + long lastPos = bs.Position; + // Patch register pointers in instance header if (_modelSet.OutRegisterInfos.Count > 0) { - bs.Position = instanceOffset + 0x04; + bs.Position = outRegsPtrPos; WriteOffset32(bs, (uint)(outRegistersOffset - _baseMdlPos)); } + if (_modelSet.InstanceUnkRegisterCount > 0) // Fixed: was missing entirely + { + bs.Position = unkRegsPtrPos; + WriteOffset32(bs, (uint)(unkRegistersOffset - _baseMdlPos)); + } + if (_modelSet.HostMethodInfos.Count > 0) { - bs.Position = instanceOffset + 0x0C; - WriteOffset32(bs, ((uint)(hostMethodRegistersOffset - _baseMdlPos))); + bs.Position = hostMethodRegsPtrPos; + WriteOffset32(bs, (uint)(hostMethodRegistersOffset - _baseMdlPos)); } + // Write instance offset and size to MDLS header bs.Position = _baseMdlPos + 0x7C; WriteOffset32(bs, (uint)(instanceOffset - _baseMdlPos)); @@ -137,12 +165,12 @@ private void WriteVariationMaterials(BinaryStream bs) for (int i = 0; i < _modelSet.VariationMaterials.Count; i++) { bs.Position = variationMaterialsOffset + (i * 0x04); - WriteOffset32(bs, (uint)(_baseMdlPos - dataOffset)); + WriteOffset32(bs, (uint)(dataOffset - _baseMdlPos)); // Fixed: was _baseMdlPos - dataOffset bs.Position = dataOffset; - for (int j = 0; i < _modelSet.VariationMaterials[j].Count; j++) + for (int j = 0; j < _modelSet.VariationMaterials[i].Count; j++) // Fixed: j < count, [i] not [j] { - PGLUmaterial material = _modelSet.VariationMaterials[j][i]; + PGLUmaterial material = _modelSet.VariationMaterials[i][j]; // Fixed: [i][j] not [j][i] material.Write(bs); } dataOffset = bs.Position; @@ -303,6 +331,7 @@ private void WriteVariationTextureSets(BinaryStream bs) } } + TexSetListHashToOffset.Add(listHash, (uint)texOffsetTable); // Fixed: was never populated } } diff --git a/PDTools.Files/Models/PS2/RenderCommands/Cmd_BBoxRender.cs b/PDTools.Files/Models/PS2/RenderCommands/Cmd_BBoxRender.cs index ddb2a0d5..74bdbdae 100644 --- a/PDTools.Files/Models/PS2/RenderCommands/Cmd_BBoxRender.cs +++ b/PDTools.Files/Models/PS2/RenderCommands/Cmd_BBoxRender.cs @@ -19,9 +19,14 @@ public class Cmd_BBoxRender : ModelSetupPS2Command public Vector3[] BBox { get; set; } public List CommandsOnRender { get; set; } = []; + /// Absolute stream byte offset where the BBox Vector3 points begin (recorded + /// on read so callers can scale the cull box in place without re-serialising). + public long BBoxStreamOffset { get; set; } + public override void Read(BinaryStream bs, int commandsBaseOffset) { byte count = bs.Read1Byte(); + BBoxStreamOffset = bs.Position; BBox = new Vector3[count]; for (int i = 0; i < count; i++) BBox[i] = new Vector3(bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle()); diff --git a/PDTools.Files/Models/PS2/RenderCommands/Cmd_ModelSet_setShapeTweenRatio.cs b/PDTools.Files/Models/PS2/RenderCommands/Cmd_ModelSet_setShapeTweenRatio.cs new file mode 100644 index 00000000..da41282d --- /dev/null +++ b/PDTools.Files/Models/PS2/RenderCommands/Cmd_ModelSet_setShapeTweenRatio.cs @@ -0,0 +1,27 @@ +using Syroot.BinaryData; + +namespace PDTools.Files.Models.PS2.RenderCommands; + +/// +/// GT4 and above. Calls ModelSet2::setShapeTweenRatio with a direct float argument. +/// Opcode 52 (0x34). +/// +public class Cmd_ModelSet_setShapeTweenRatio : ModelSetupPS2Command +{ + public override ModelSetupPS2Opcode Opcode => ModelSetupPS2Opcode.ModelSet_setShapeTweenRatio; + + public float Ratio { get; set; } + + public override void Read(BinaryStream bs, int commandsBaseOffset) + { + Ratio = bs.ReadSingle(); + } + + public override void Write(BinaryStream bs) + { + bs.WriteSingle(Ratio); + } + + public override string ToString() + => $"{nameof(Cmd_ModelSet_setShapeTweenRatio)}({Ratio:G})"; +} diff --git a/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglCylinderMapHint.cs b/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglCylinderMapHint.cs new file mode 100644 index 00000000..812973f7 --- /dev/null +++ b/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglCylinderMapHint.cs @@ -0,0 +1,29 @@ +using System.Numerics; +using Syroot.BinaryData; + +namespace PDTools.Files.Models.PS2.RenderCommands; + +/// +/// Calls pglCylinderMapHint with 3 floats. Opcode 49 (0x31). +/// +public class Cmd_pglCylinderMapHint : ModelSetupPS2Command +{ + public override ModelSetupPS2Opcode Opcode => ModelSetupPS2Opcode.pglCylinderMapHint; + + public Vector3 Hint { get; set; } + + public override void Read(BinaryStream bs, int commandsBaseOffset) + { + Hint = new Vector3(bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle()); + } + + public override void Write(BinaryStream bs) + { + bs.WriteSingle(Hint.X); + bs.WriteSingle(Hint.Y); + bs.WriteSingle(Hint.Z); + } + + public override string ToString() + => $"{nameof(Cmd_pglCylinderMapHint)}({Hint.X:G}, {Hint.Y:G}, {Hint.Z:G})"; +} diff --git a/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglLoadMatrix.cs b/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglLoadMatrix.cs index 7f31d364..8878ec91 100644 --- a/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglLoadMatrix.cs +++ b/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglLoadMatrix.cs @@ -18,8 +18,13 @@ public class Cmd_pglLoadMatrix : ModelSetupPS2Command public Matrix4x4 Matrix { get; set; } + /// Absolute stream byte offset of the 16 matrix floats (recorded on read so + /// callers can scale the translation column in place). + public long MatrixStreamOffset { get; set; } + public override void Read(BinaryStream bs, int commandsBaseOffset) { + MatrixStreamOffset = bs.Position; Matrix = new Matrix4x4( bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), diff --git a/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglMultMatrix.cs b/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglMultMatrix.cs index 110e2bec..d3e4ee01 100644 --- a/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglMultMatrix.cs +++ b/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglMultMatrix.cs @@ -15,8 +15,13 @@ public class Cmd_pglMultMatrix : ModelSetupPS2Command public Matrix4x4 Matrix { get; set; } + /// Absolute stream byte offset of the 16 matrix floats (recorded on read so + /// callers can scale the translation column in place). + public long MatrixStreamOffset { get; set; } + public override void Read(BinaryStream bs, int commandsBaseOffset) { + MatrixStreamOffset = bs.Position; Matrix = new Matrix4x4( bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle(), diff --git a/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglTranslate.cs b/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglTranslate.cs index ef189075..92e19581 100644 --- a/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglTranslate.cs +++ b/PDTools.Files/Models/PS2/RenderCommands/Cmd_pglTranslate.cs @@ -21,6 +21,10 @@ public class Cmd_pglTranslate : ModelSetupPS2Command /// public Vector3 Vector { get; set; } + /// Absolute stream byte offset of the translation floats (recorded on read so + /// callers can scale the placement in place without re-serialising). + public long VectorStreamOffset { get; set; } + public Cmd_pglTranslate() { @@ -33,6 +37,7 @@ public Cmd_pglTranslate(float x, float y, float z) public override void Read(BinaryStream bs, int commandsBaseOffset) { + VectorStreamOffset = bs.Position; Vector = new Vector3(bs.ReadSingle(), bs.ReadSingle(), bs.ReadSingle()); } diff --git a/PDTools.Files/Models/PS2/RenderCommands/ModelSetupCommand.cs b/PDTools.Files/Models/PS2/RenderCommands/ModelSetupCommand.cs index a850728e..dfe89418 100644 --- a/PDTools.Files/Models/PS2/RenderCommands/ModelSetupCommand.cs +++ b/PDTools.Files/Models/PS2/RenderCommands/ModelSetupCommand.cs @@ -78,6 +78,8 @@ public static ModelSetupPS2Command GetByOpcode(ModelSetupPS2Opcode opcode) ModelSetupPS2Opcode.pglAlphaFail => new Cmd_pglAlphaFail(), ModelSetupPS2Opcode.pglGT3_2_1ui => new Cmd_GT3_2_1ui(), ModelSetupPS2Opcode.pglGT3_2_4f => new Cmd_GT3_2_4f(), + ModelSetupPS2Opcode.pglCylinderMapHint => new Cmd_pglCylinderMapHint(), + ModelSetupPS2Opcode.ModelSet_setShapeTweenRatio => new Cmd_ModelSet_setShapeTweenRatio(), ModelSetupPS2Opcode.pgl_53 => new Cmd_Unk53(), ModelSetupPS2Opcode.CallVM => new Cmd_CallVM(), ModelSetupPS2Opcode.VM_pglRotate => new Cmd_VM_pglRotate(), diff --git a/PDTools.Files/Models/PS2/VIFCommand.cs b/PDTools.Files/Models/PS2/VIFCommand.cs index ae878466..47ef82bb 100644 --- a/PDTools.Files/Models/PS2/VIFCommand.cs +++ b/PDTools.Files/Models/PS2/VIFCommand.cs @@ -18,6 +18,11 @@ public class VIFCommand public GIFTag GIFTag { get; set; } public List UnpackData { get; set; } = []; + /// Absolute stream byte offset where this command's data (unpack elements / + /// GIFTag) begins. Recorded on read so callers can patch the raw vertex bytes in place + /// without re-serialising the whole model. + public long DataStreamOffset { get; set; } + public void FromStream(BinaryStream bs) { VUAddr = bs.ReadUInt16(); @@ -27,6 +32,8 @@ public void FromStream(BinaryStream bs) CommandOpcode = (VIFCommandOpcode)(bits & 0b1111111); IRQ = (bits >> 7 & 1) == 1; + DataStreamOffset = bs.Position; + if (VUAddr == 0xC0C0) { GIFTag = new GIFTag(); diff --git a/PDTools.Files/Textures/PGLUTextureInfo.cs b/PDTools.Files/Textures/PGLUTextureInfo.cs index 717e21be..0a46c37f 100644 --- a/PDTools.Files/Textures/PGLUTextureInfo.cs +++ b/PDTools.Files/Textures/PGLUTextureInfo.cs @@ -22,4 +22,10 @@ public abstract class PGLUTextureInfo public abstract void Write(BinaryStream bs); public abstract void Read(BinaryStream bs, long basePos); + + /// + /// Short token for this texture's pixel format (e.g. "A8R8G8B8", "DXT45", "IDTEX8"), + /// used to tag dumped filenames so the format can be reproduced on rebuild. + /// + public virtual string GetPixelFormatName() => "UNKNOWN"; } diff --git a/PDTools.Files/Textures/PS2/TextureSetBuilder.cs b/PDTools.Files/Textures/PS2/TextureSetBuilder.cs index 2868c1d7..6962fd20 100644 --- a/PDTools.Files/Textures/PS2/TextureSetBuilder.cs +++ b/PDTools.Files/Textures/PS2/TextureSetBuilder.cs @@ -33,11 +33,13 @@ public class TextureSetBuilder /* Used to keep track of GS blocks without texture data allocated * So that we can put other textures's data in them */ - private readonly Dictionary _unusedGsBlocksIndices = []; + private readonly SortedDictionary _unusedGsBlocksIndices = new(); /* Used to keep track of all GS blocks we've used up */ private readonly List _usedGsBlocksIndices = []; + private Dictionary _variationPaletteCache = new(); + private int _lastFreeVerticalBlock = -1; private ushort _tbp_Textures = 0; @@ -73,9 +75,6 @@ private void AddImage(Image img, TextureConfig config) { _logger?.LogInformation("Adding image {x}x{y}, format={format}", img.Width, img.Height, config.Format); - if (config.IsTextureMap) - img.Mutate(e => e.Resize((int)BitOperations.RoundUpToPowerOf2((uint)img.Width), (int)BitOperations.RoundUpToPowerOf2((uint)img.Height))); - var pgluTexture = new PGLUtexture(); pgluTexture.tex0.PSM = config.Format; @@ -214,7 +213,7 @@ public void AddClutPatch(int clutPatchSetIndex, int pgluTextureIndex, string pat else throw new Exception($"Texture file '{path}' must use less than {paletteSize} colors ahead of time."); - ClutPatchTask clutPatch = new ClutPatchTask(fullPalette); + ClutPatchTask clutPatch = new ClutPatchTask(fullPalette, (ushort)pgluTextureIndex); while (_clutPatchSets.Count <= clutPatchSetIndex) _clutPatchSets.Add([]); @@ -234,6 +233,20 @@ public TextureSet1 Build() WriteClutPatches(); + // Sometimes this information is useful to debug changes to the Tex1 Optimization + Console.WriteLine("--- Tex1 ALLOCATION MAP ---"); + foreach (var t in _textures) + { + Console.WriteLine($"Format: {t.PGLUTexture.tex0.PSM} | Size: {t.Image.Width}x{t.Image.Height} | Blocks Used: {t.SizeInGSBlocks} | TBP: {t.PGLUTexture.tex0.TBP0_TextureBaseAddress}"); + } + foreach (var p in _texSet.pgluTextures) + { + if (p.tex0.CBP_ClutBlockPointer > 0) + Console.WriteLine($"Palette Format: {p.tex0.PSM} | CBP: {p.tex0.CBP_ClutBlockPointer} | CSA: {p.tex0.CSA_ClutEntryOffset}"); + } + Console.WriteLine($"Peak Allocation (Max Block): {GetPeakAllocation()}"); + Console.WriteLine("---------------------------"); + bool swizzle = _textures.Count >= 2; if (swizzle) BuildSwizzledTransfers(); @@ -284,258 +297,333 @@ private void WritePalettes() /// /// /// - private (ushort CBP, byte CSA) FitPaletteToGSMemory(SCE_GS_PSM textureFormat, int width, int height, Rgba32[] palette, bool reuseOldPaletteLocations = false) - { - if (reuseOldPaletteLocations) + private (ushort CBP, byte CSA) FitPaletteToGSMemory(SCE_GS_PSM textureFormat, int width, int height, Rgba32[] palette, bool reuseOldPaletteLocations = false) + { + string paletteHash = string.Empty; + + if (reuseOldPaletteLocations) + { + // 1. Check Base Textures (Original Logic) + for (int i = 0; i < _texSet.pgluTextures.Count; i++) + { + PGLUtexture pgluTexture = _texSet.pgluTextures[i]; + if (pgluTexture.tex0.PSM == textureFormat && _textures[i].Palette != null && _textures[i].Palette.AsSpan().SequenceEqual(palette)) + { + return (pgluTexture.tex0.CBP_ClutBlockPointer, pgluTexture.tex0.CSA_ClutEntryOffset); + } + } + + // 2. Check Previously Allocated Variation Palettes (The 10% Saver) + using (var md5 = System.Security.Cryptography.MD5.Create()) + { + var bytes = MemoryMarshal.AsBytes(palette.AsSpan()).ToArray(); + paletteHash = textureFormat.ToString() + "_" + BitConverter.ToString(System.Security.Cryptography.MD5.HashData(bytes)); + } + + if (_variationPaletteCache.TryGetValue(paletteHash, out var cachedLocation)) + { + return cachedLocation; // Found an exact match from a previous variation + } + } + + List usedBlocksOfTexture = GSPixelFormat.PSM_CT32.GetUsedBlocks(width, height); + int size = Tex1Utils.GetDataSize(width, height, SCE_GS_PSM.SCE_GS_PSMCT32); + int csa = 0; + byte csaTakenSpace = (byte)Math.Min(size / 32, 8); + int idx = CanFitBlocksInUnusedBlocks(usedBlocksOfTexture, csaTakenSpace); + + ushort cbp; + if (idx != -1) + { + cbp = (ushort)idx; + if (usedBlocksOfTexture.Count == 1) + { + // Fix 1: Read the block, update it, and WRITE IT BACK + GSBlock block = _unusedGsBlocksIndices[idx + usedBlocksOfTexture[0]]; + csa = block.CurrentCSA; + + block.CurrentCSA += csaTakenSpace; + + if (block.CurrentCSA >= 8) + { + _unusedGsBlocksIndices.Remove(block.Index); + _usedGsBlocksIndices.Add((ushort)block.Index); + } + else + { + // CRITICAL C# FIX: Save the modified struct back to the dictionary + _unusedGsBlocksIndices[block.Index] = block; + } + } + else + { + for (int i = 0; i < usedBlocksOfTexture.Count; i++) + { + _unusedGsBlocksIndices.Remove((ushort)(idx + usedBlocksOfTexture[i])); + _usedGsBlocksIndices.Add((ushort)(idx + usedBlocksOfTexture[i])); + } + } + } + else + { + cbp = _tbp_Textures; // Lock in the CBP BEFORE incrementing TBP + _tbp_Textures += (ushort)usedBlocksOfTexture.Count; + + if (usedBlocksOfTexture.Count == 1) + { + csa = 0; // Starts at 0 for a brand new block + + // Fix 2: Track the CBP we just used, not the freshly incremented TBP! + GSBlock partialFilledBlock = new GSBlock(cbp, csaTakenSpace); + _unusedGsBlocksIndices.Add(partialFilledBlock.Index, partialFilledBlock); + } + else + { + for (int i = 0; i < usedBlocksOfTexture.Count; i++) + _usedGsBlocksIndices.Add((ushort)(cbp + usedBlocksOfTexture[i])); + } + } + + switch (textureFormat) + { + case SCE_GS_PSM.SCE_GS_PSMT8: + _gsMemory.WriteTexPSMCT32(cbp, 1, 0, 0, 16, 16, MemoryMarshal.Cast(palette), csa * 32); + break; + case SCE_GS_PSM.SCE_GS_PSMT4: + _gsMemory.WriteTexPSMCT32(cbp, 1, 0, 0, 8, 2, MemoryMarshal.Cast(palette), csa * 32); + break; + } + + if (reuseOldPaletteLocations && !string.IsNullOrEmpty(paletteHash)) { - for (int i = 0; i < _texSet.pgluTextures.Count; i++) - { - // Is there an identical palette somewhere already? - PGLUtexture pgluTexture = _texSet.pgluTextures[i]; - if (pgluTexture.tex0.PSM == textureFormat && _textures[i].Palette.AsSpan().SequenceEqual(palette)) - { - // return its cbp - Save on size - return (pgluTexture.tex0.CBP_ClutBlockPointer, pgluTexture.tex0.CSA_ClutEntryOffset); - } - } - } - - /* The game cheats a bit with "CSA" - is it even used for its original purpose? - * "CSA" here is used as an offset WITHIN the block itself - * So a block (256 bytes) can store 4 PSMT4 palettes (64 * 4) - * CSA goes every 32 */ - - List usedBlocksOfTexture = GSPixelFormat.PSM_CT32.GetUsedBlocks(width, height); - int size = Tex1Utils.GetDataSize(width, height, SCE_GS_PSM.SCE_GS_PSMCT32); - int csa = 0; - byte csaTakenSpace = (byte)Math.Min(size / 32, 8); - int idx = CanFitBlocksInUnusedBlocks(usedBlocksOfTexture, csaTakenSpace); - - ushort cbp; - if (idx != -1) - { - cbp = (ushort)idx; - - // Is this a palette that fits in one singular block? - if (usedBlocksOfTexture.Count == 1) - { - GSBlock block = _unusedGsBlocksIndices[idx + usedBlocksOfTexture[0]]; - csa = block.CurrentCSA; - - block.CurrentCSA += csaTakenSpace; - if (block.CurrentCSA >= 8) - { - // Block CSA is 8 (32 * 8 = 256 bytes). This block is filled, move on - _unusedGsBlocksIndices.Remove(block.Index); - _usedGsBlocksIndices.Add((ushort)block.Index); - _tbp_Textures++; - } - } - else - { - for (int i = 0; i < usedBlocksOfTexture.Count; i++) - { - _unusedGsBlocksIndices.Remove((ushort)(idx + usedBlocksOfTexture[i])); - _usedGsBlocksIndices.Add((ushort)(idx + usedBlocksOfTexture[i])); - } - } - } - else - { - cbp = _tbp_Textures; - _tbp_Textures += (ushort)usedBlocksOfTexture.Count; - - if (usedBlocksOfTexture.Count == 1) - { - csa = csaTakenSpace; - - GSBlock partialFilledBlock = new GSBlock(_tbp_Textures + usedBlocksOfTexture[0], csaTakenSpace); - _unusedGsBlocksIndices.Add(partialFilledBlock.Index, partialFilledBlock); - } - else - { - for (int i = 0; i < usedBlocksOfTexture.Count; i++) - _usedGsBlocksIndices.Add((ushort)(_tbp_Textures + usedBlocksOfTexture[i])); - } - } - - switch (textureFormat) - { - case SCE_GS_PSM.SCE_GS_PSMT8: - _gsMemory.WriteTexPSMCT32(cbp, 1, - 0, 0, - 16, 16, - MemoryMarshal.Cast(palette), - csa * 32); - break; - - case SCE_GS_PSM.SCE_GS_PSMT4: - _gsMemory.WriteTexPSMCT32(cbp, 1, - 0, 0, - 8, 2, - MemoryMarshal.Cast(palette), - csa * 32); - break; + _variationPaletteCache[paletteHash] = (cbp, (byte)csa); } return (cbp, (byte)csa); - } - - private void WriteClutPatches() - { - if (_texSet.ClutPatchSet.Count < 1) - return; - - for (ushort i = 0; i < _texSet.pgluTextures.Count; i++) - { - var clutPatch = _texSet.ClutPatchSet[0].TexturesToPatch[i]; - var pgluTexture= _texSet.pgluTextures[i]; - - clutPatch.CBP_ClutBufferBasePointer = pgluTexture.tex0.CBP_ClutBlockPointer; - clutPatch.CSA_ClutEntryOffset = pgluTexture.tex0.CSA_ClutEntryOffset; - clutPatch.PGLUTextureIndex = i; - clutPatch.Format = SCE_GS_PSM.SCE_GS_PSMCT32; - } - - for (int varIndex = 1; varIndex < _clutPatchSets.Count; varIndex++) - { - var clutPatchSet = new ClutPatchSet(); - _texSet.ClutPatchSet.Add(clutPatchSet); - - for (ushort textureIndex = 0; textureIndex < _clutPatchSets[varIndex].Count; textureIndex++) - { - ClutPatchTask clutPatchTask = _clutPatchSets[varIndex][textureIndex]; - - SCE_GS_PSM textureFormat = _textures[textureIndex].PGLUTexture.tex0.PSM; - int width = textureFormat == SCE_GS_PSM.SCE_GS_PSMT8 ? 16 : 8; - int height = textureFormat == SCE_GS_PSM.SCE_GS_PSMT8 ? 16 : 2; - - var clutPatch = new TextureClutPatch(); - (ushort CBP, byte CSA) = FitPaletteToGSMemory(textureFormat, width, height, clutPatchTask.Palette, reuseOldPaletteLocations: true); - clutPatch.CBP_ClutBufferBasePointer = CBP; - clutPatch.CSA_ClutEntryOffset = CSA; - clutPatch.PGLUTextureIndex = textureIndex; - clutPatch.Format = SCE_GS_PSM.SCE_GS_PSMCT32; - - clutPatchSet.TexturesToPatch.Add(clutPatch); - } - } - } + } + + private void WriteClutPatches() + { + if (_clutPatchSets.Count == 0) + return; + + // 1. Find all texture indices that are patched in ANY variation + // Using a SortedSet ensures the array order is perfectly identical across all variations + var patchedTextureIndices = new SortedSet(); + for (int v = 1; v < _clutPatchSets.Count; v++) + { + foreach (var task in _clutPatchSets[v]) + patchedTextureIndices.Add(task.TargetTextureIndex); + } + + // 2. Base Variation (0): Establish the baseline array + foreach (ushort targetIndex in patchedTextureIndices) + { + var pgluTexture = _texSet.pgluTextures[targetIndex]; + + var clutPatch = new TextureClutPatch + { + CBP_ClutBufferBasePointer = pgluTexture.tex0.CBP_ClutBlockPointer, + CSA_ClutEntryOffset = pgluTexture.tex0.CSA_ClutEntryOffset, + PGLUTextureIndex = targetIndex, + Format = SCE_GS_PSM.SCE_GS_PSMCT32 + }; + _texSet.ClutPatchSet[0].TexturesToPatch.Add(clutPatch); + } + + // 3. Subsequent Variations: Must perfectly mirror the Base Variation array length and order + for (int varIndex = 1; varIndex < _clutPatchSets.Count; varIndex++) + { + var clutPatchSet = new ClutPatchSet(); + _texSet.ClutPatchSet.Add(clutPatchSet); + + // Quick lookup dictionary for the tasks present in this specific variation + var tasksForThisVariation = _clutPatchSets[varIndex].ToDictionary(t => t.TargetTextureIndex); + + // Iterate over the exact same sorted baseline indices + foreach (ushort targetIndex in patchedTextureIndices) + { + var clutPatch = new TextureClutPatch + { + PGLUTextureIndex = targetIndex, + Format = SCE_GS_PSM.SCE_GS_PSMCT32 + }; + + if (tasksForThisVariation.TryGetValue(targetIndex, out var clutPatchTask)) + { + // This variation ACTIVELY changes this texture. Write the new palette to VRAM. + SCE_GS_PSM textureFormat = _textures[targetIndex].PGLUTexture.tex0.PSM; + int width = textureFormat == SCE_GS_PSM.SCE_GS_PSMT8 ? 16 : 8; + int height = textureFormat == SCE_GS_PSM.SCE_GS_PSMT8 ? 16 : 2; + + (ushort CBP, byte CSA) = FitPaletteToGSMemory(textureFormat, width, height, clutPatchTask.Palette, reuseOldPaletteLocations: true); + + clutPatch.CBP_ClutBufferBasePointer = CBP; + clutPatch.CSA_ClutEntryOffset = CSA; + } + else + { + // This variation DOES NOT change this texture. + // Generate a Ghost Patch pointing to the base palette to prevent index desync. + var baseTexture = _texSet.pgluTextures[targetIndex]; + clutPatch.CBP_ClutBufferBasePointer = baseTexture.tex0.CBP_ClutBlockPointer; + clutPatch.CSA_ClutEntryOffset = baseTexture.tex0.CSA_ClutEntryOffset; + } + + clutPatchSet.TexturesToPatch.Add(clutPatch); + } + } + } /// /// Fits all the textures to the emulated GS memory in a block-optimized way and updates their block pointers. /// /// /// - private void WriteTexturesOptimized() - { - /* Order by textures that allocates the most GS blocks without actually using them - * That way we can put texture data in there - - * There's still some good improvements that can be made here for certain - as i am only trying to fit the rest of the textures where they can fit - instead of arranging them to begin with - */ - - var texturesByUnusedGsBlocks = _textures.OrderByDescending(e => e.SizeInGSBlocks); - foreach (TextureTask texture in texturesByUnusedGsBlocks) - { - List usedBlocksOfTexture = texture.TexturePixelFormat.GetUsedBlocks(texture.Image.Width, texture.Image.Height); - - // #1: Start searching if we can fit the texture in all unused GS blocks. - int unusedBlockFitIndex = CanFitBlocksInUnusedBlocks(usedBlocksOfTexture); - if (unusedBlockFitIndex != -1) - { - // We were able to fit the texture in unused blocks - for (int i = 0; i < usedBlocksOfTexture.Count; i++) - { - if (_unusedGsBlocksIndices.ContainsKey((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i]))) - _unusedGsBlocksIndices.Remove((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i])); - - _usedGsBlocksIndices.Add((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i])); - } - - texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = (ushort)unusedBlockFitIndex; - WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, - texture.Image.Width, texture.Image.Height, - texture.PGLUTexture.tex0.PSM, - texture.PackedImageData); - continue; - } - - // #2: Check if we can fit the texture after the last vertical row (provided the page layout is ok?) - int afterLastRowFitBlockIdx = -1; - if (_tbp_Textures != 0 && _lastFreeVerticalBlock != -1) - { - for (ushort blockIdx = (ushort)_lastFreeVerticalBlock; blockIdx < _tbp_Textures; blockIdx++) - { - int j = 0; - for (j = 0; j < usedBlocksOfTexture.Count; j++) - { - if (_usedGsBlocksIndices.Contains((ushort)(blockIdx + usedBlocksOfTexture[j]))) - { - // Starting block index is not suitable to fit the texture, move to next one - break; - } - } - - if (j == usedBlocksOfTexture.Count) - { - afterLastRowFitBlockIdx = blockIdx; - break; - } - } - - if (afterLastRowFitBlockIdx != -1) - { - for (int i = 0; i < usedBlocksOfTexture.Count; i++) - { - if (_unusedGsBlocksIndices.ContainsKey((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i]))) - _unusedGsBlocksIndices.Remove((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i])); - - _usedGsBlocksIndices.Add((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i])); - } - - texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = (ushort)afterLastRowFitBlockIdx; - _lastFreeVerticalBlock = (ushort)(afterLastRowFitBlockIdx + texture.FirstFreeVerticalBlock); - - _tbp_Textures = _usedGsBlocksIndices.Max(e => e); - WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, - texture.Image.Width, texture.Image.Height, - texture.PGLUTexture.tex0.PSM, - texture.PackedImageData); - continue; - } - } - - - // Unable to fit anywhere (it seems). We are allocating new blocks starting from tbp - texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = _tbp_Textures; - _lastFreeVerticalBlock = (ushort)(_tbp_Textures + texture.FirstFreeVerticalBlock); - - for (int i = 0; i < texture.UnusedGSBlocks.Count; i++) - { - ushort idx = (ushort)(_tbp_Textures + texture.UnusedGSBlocks[i]); - _unusedGsBlocksIndices.Add(idx, new GSBlock(idx, 0)); - } - - for (int j = 0; j < usedBlocksOfTexture.Count; j++) - _usedGsBlocksIndices.Add((ushort)(_tbp_Textures + usedBlocksOfTexture[j])); - - if (_tbp_Textures + texture.SizeInGSBlocks >= GSMemory.MAX_BLOCKS) - throw new OutOfMemoryException($"Textures take more space than the maximum GS memory capacity ({_tbp_Textures + texture.SizeInGSBlocks} >= {GSMemory.MAX_BLOCKS})."); - - WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, - texture.Image.Width, texture.Image.Height, - texture.PGLUTexture.tex0.PSM, - texture.PackedImageData); - + + + // We are going to intercept the textures before they ever touch the emulated GS Memory. + // If the pixel indices match an image we have already processed, we point the texture header + // to the existing TBP and skip allocating new blocks entirely + private void WriteTexturesOptimized() + { + /* Order by textures that allocates the most GS blocks without actually using them + * That way we can put texture data in there + * There's still some good improvements that can be made here for certain + as i am only trying to fit the rest of the textures where they can fit + instead of arranging them to begin with + */ + + var texturesOptimized = _textures + // Group formats to keep GS page matrices aligned + .OrderByDescending(t => t.PGLUTexture.tex0.PSM) + // Anchor the massive liveries first so they stack perfectly flush + .ThenByDescending(t => t.SizeInGSBlocks) + .ToList(); + + // Cache to track which pixel arrays are already in VRAM + var allocatedTBPs = new Dictionary(); + + foreach (TextureTask texture in texturesOptimized) + { + // Generate the unique, format-aware hash key for this texture + var cacheKey = new TextureCacheKey( + texture.PGLUTexture.tex0.PSM, + texture.Image.Width, + texture.Image.Height, + texture.PackedImageData + ); + + // --- TBP Deduplication Check --- + if (allocatedTBPs.TryGetValue(cacheKey, out ushort existingTbp)) + { + _logger?.LogInformation("Deduplicating Texture Data. Sharing TBP: {tbp}", existingTbp); + texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = existingTbp; + continue; + } + + List usedBlocksOfTexture = texture.TexturePixelFormat.GetUsedBlocks(texture.Image.Width, texture.Image.Height); + + // #1: Start searching if we can fit the texture in all unused GS blocks. + int unusedBlockFitIndex = CanFitBlocksInUnusedBlocks(usedBlocksOfTexture); + if (unusedBlockFitIndex != -1) + { + // We were able to fit the texture in unused blocks + for (int i = 0; i < usedBlocksOfTexture.Count; i++) + { + if (_unusedGsBlocksIndices.ContainsKey((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i]))) + _unusedGsBlocksIndices.Remove((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i])); + + _usedGsBlocksIndices.Add((ushort)(unusedBlockFitIndex + usedBlocksOfTexture[i])); + } + + texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = (ushort)unusedBlockFitIndex; + WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, + texture.Image.Width, texture.Image.Height, + texture.PGLUTexture.tex0.PSM, + texture.PackedImageData); + + // Cache the successfully written texture + allocatedTBPs[cacheKey] = texture.PGLUTexture.tex0.TBP0_TextureBaseAddress; + continue; + } + + // #2: Check if we can fit the texture after the last vertical row (provided the page layout is ok?) + int afterLastRowFitBlockIdx = -1; + if (_tbp_Textures != 0 && _lastFreeVerticalBlock != -1) + { + for (ushort blockIdx = (ushort)_lastFreeVerticalBlock; blockIdx < _tbp_Textures; blockIdx++) + { + int j = 0; + for (j = 0; j < usedBlocksOfTexture.Count; j++) + { + if (_usedGsBlocksIndices.Contains((ushort)(blockIdx + usedBlocksOfTexture[j]))) + { + // Starting block index is not suitable to fit the texture, move to next one + break; + } + } + + if (j == usedBlocksOfTexture.Count) + { + afterLastRowFitBlockIdx = blockIdx; + break; + } + } + + if (afterLastRowFitBlockIdx != -1) + { + for (int i = 0; i < usedBlocksOfTexture.Count; i++) + { + if (_unusedGsBlocksIndices.ContainsKey((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i]))) + _unusedGsBlocksIndices.Remove((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i])); + + _usedGsBlocksIndices.Add((ushort)(afterLastRowFitBlockIdx + usedBlocksOfTexture[i])); + } + + texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = (ushort)afterLastRowFitBlockIdx; + _lastFreeVerticalBlock = (ushort)(afterLastRowFitBlockIdx + texture.FirstFreeVerticalBlock); + + _tbp_Textures = _usedGsBlocksIndices.Max(e => e); + WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, + texture.Image.Width, texture.Image.Height, + texture.PGLUTexture.tex0.PSM, + texture.PackedImageData); + + // Cache the successfully written texture + allocatedTBPs[cacheKey] = texture.PGLUTexture.tex0.TBP0_TextureBaseAddress; + continue; + } + } + + // Unable to fit anywhere (it seems). We are allocating new blocks starting from tbp + texture.PGLUTexture.tex0.TBP0_TextureBaseAddress = _tbp_Textures; + _lastFreeVerticalBlock = (ushort)(_tbp_Textures + texture.FirstFreeVerticalBlock); + + for (int i = 0; i < texture.UnusedGSBlocks.Count; i++) + { + ushort idx = (ushort)(_tbp_Textures + texture.UnusedGSBlocks[i]); + _unusedGsBlocksIndices.Add(idx, new GSBlock(idx, 0)); + } + + for (int j = 0; j < usedBlocksOfTexture.Count; j++) + _usedGsBlocksIndices.Add((ushort)(_tbp_Textures + usedBlocksOfTexture[j])); + + if (_tbp_Textures + texture.SizeInGSBlocks >= GSMemory.MAX_BLOCKS) + throw new OutOfMemoryException($"Textures take more space than the maximum GS memory capacity ({_tbp_Textures + texture.SizeInGSBlocks} >= {GSMemory.MAX_BLOCKS})."); + + WriteTextureToGSMemory(texture.PGLUTexture.tex0.TBP0_TextureBaseAddress, texture.PGLUTexture.tex0.TBW_TextureBufferWidth, + texture.Image.Width, texture.Image.Height, + texture.PGLUTexture.tex0.PSM, + texture.PackedImageData); + + // Cache the successfully written texture + allocatedTBPs[cacheKey] = texture.PGLUTexture.tex0.TBP0_TextureBaseAddress; + uint textureTbp = texture.SizeInGSBlocks; - if (textureTbp == 1) - textureTbp = 4; _tbp_Textures += (ushort)textureTbp; - } - } + } + } private void BuildTransfers() { @@ -564,28 +652,55 @@ private void BuildTransfers() } } - private void BuildSwizzledTransfers() - { - int lastUsedBlock = _usedGsBlocksIndices.Max(e => e); - - // Make sure we calculate (and align) the size from the blocks instead since we're swizzling - var transferSizes = Tex1Utils.CalculateSwizzledTransferSizes(lastUsedBlock * GSMemory.BLOCK_SIZE_BYTES); - - int tbp = 0; - foreach (var (Width, Height) in transferSizes) - { - _logger?.LogDebug("Adding swizzled transfer {x}x{y}, tbp={tbp}", Width, Height, tbp); - - byte[] transferData = new byte[Width * Height * 4]; - _gsMemory.ReadTexPSMCT32(tbp, 1, - 0, 0, - Width, Height, - MemoryMarshal.Cast(transferData)); - AddTransfer(GSPixelFormat.PSM_CT32, (ushort)tbp, 1, (ushort)Width, (ushort)Height, transferData); - - tbp += transferData.Length / GSMemory.BLOCK_SIZE_BYTES; - } - } + private void BuildSwizzledTransfers() + { + // 1. Calculate the TRUE physical boundary of the VRAM + int maxTextureBlock = 0; + foreach (var t in _textures) + { + int endBlock = t.PGLUTexture.tex0.TBP0_TextureBaseAddress + t.SizeInGSBlocks; + if (endBlock > maxTextureBlock) maxTextureBlock = endBlock; + } + + int maxPaletteBlock = 0; + foreach (var p in _texSet.pgluTextures) + { + int endBlock = p.tex0.CBP_ClutBlockPointer + 1; + if (endBlock > maxPaletteBlock) maxPaletteBlock = endBlock; + } + + // Check the Variation Palettes --- + int maxPatchBlock = 0; + foreach (var patchSet in _texSet.ClutPatchSet) + { + foreach (var patch in patchSet.TexturesToPatch) + { + int endBlock = patch.CBP_ClutBufferBasePointer + 1; + if (endBlock > maxPatchBlock) maxPatchBlock = endBlock; + } + } + + // The true required footprint encompasses textures, base palettes, and patch palettes + int lastUsedBlock = GetPeakAllocation(); + + // Make sure we calculate and align the size from the blocks instead since we're swizzling + var transferSizes = Tex1Utils.CalculateSwizzledTransferSizes(lastUsedBlock * GSMemory.BLOCK_SIZE_BYTES); + + int tbp = 0; + foreach (var (Width, Height) in transferSizes) + { + _logger?.LogDebug("Adding swizzled transfer {x}x{y}, tbp={tbp}", Width, Height, tbp); + + byte[] transferData = new byte[Width * Height * 4]; + _gsMemory.ReadTexPSMCT32(tbp, 1, + 0, 0, + Width, Height, + MemoryMarshal.Cast(transferData)); + AddTransfer(GSPixelFormat.PSM_CT32, (ushort)tbp, 1, (ushort)Width, (ushort)Height, transferData); + + tbp += transferData.Length / GSMemory.BLOCK_SIZE_BYTES; + } + } private static bool ImageFitsColorPalette(Image img, int paletteSize, out List colorPalette) { @@ -613,38 +728,35 @@ private static bool ImageFitsColorPalette(Image img, int paletteSize, ou /// Blocks to fit /// CSA to fit in a block /// Block index start. -1 if it could not be fitted. - private int CanFitBlocksInUnusedBlocks(List usedBlocksOfTexture, int csa = 8) - { - int unusedBlockFitIndex = -1; - - foreach (GSBlock block in _unusedGsBlocksIndices.Values) - { - // Special case when a texture/palette fits into a single block where we can - // tweak the csa register to point to it - if (usedBlocksOfTexture.Count == 1 && block.CurrentCSA + csa <= 8) - { - // We can reuse a partially filled block using CSA - return block.Index; - } - - int j = 0; - for (j = 0; j < usedBlocksOfTexture.Count; j++) - { - int blockIdx = block.Index + usedBlocksOfTexture[j]; - - if (!_unusedGsBlocksIndices.ContainsKey((ushort)blockIdx)) - break; - } - - if (j == usedBlocksOfTexture.Count) - { - unusedBlockFitIndex = block.Index; - break; - } - } - - return unusedBlockFitIndex; - } + private int CanFitBlocksInUnusedBlocks(List usedBlocksOfTexture, int csa = 8) + { + int unusedBlockFitIndex = -1; + + // Because this is a SortedDictionary, it now strictly evaluates the lowest memory addresses first. + foreach (var kvp in _unusedGsBlocksIndices) + { + GSBlock block = kvp.Value; + + if (usedBlocksOfTexture.Count == 1 && block.CurrentCSA + csa <= 8) + return block.Index; + + int j = 0; + for (j = 0; j < usedBlocksOfTexture.Count; j++) + { + int blockIdx = block.Index + usedBlocksOfTexture[j]; + if (!_unusedGsBlocksIndices.ContainsKey((ushort)blockIdx)) + break; // Gap is too small, collision detected + } + + if (j == usedBlocksOfTexture.Count) + { + unusedBlockFitIndex = block.Index; + break; // Found the lowest possible gap that fits + } + } + + return unusedBlockFitIndex; + } /// /// Creates image data for the specified texture. @@ -783,6 +895,34 @@ public static (Rgba32[] TiledPalette, int[] LinearToTiledPaletteIndices) MakeTil return (outpal, indices); } + + private int GetPeakAllocation() + { + int maxBlock = 0; + + foreach (var t in _textures) + { + int endBlock = t.PGLUTexture.tex0.TBP0_TextureBaseAddress + t.SizeInGSBlocks; + if (endBlock > maxBlock) maxBlock = endBlock; + } + + foreach (var p in _texSet.pgluTextures) + { + int endBlock = p.tex0.CBP_ClutBlockPointer + 1; + if (endBlock > maxBlock) maxBlock = endBlock; + } + + foreach (var patchSet in _texSet.ClutPatchSet) + { + foreach (var patch in patchSet.TexturesToPatch) + { + int endBlock = patch.CBP_ClutBufferBasePointer + 1; + if (endBlock > maxBlock) maxBlock = endBlock; + } + } + + return maxBlock; + } } public class TextureTask @@ -835,10 +975,14 @@ public class TextureTask public class ClutPatchTask { public Rgba32[] Palette { get; set; } + + // The new property that ties this palette to a specific texture index + public ushort TargetTextureIndex { get; set; } - public ClutPatchTask(Rgba32[] palette) + public ClutPatchTask(Rgba32[] palette, ushort targetTextureIndex) { Palette = palette; + TargetTextureIndex = targetTextureIndex; } } @@ -853,3 +997,35 @@ public GSBlock(int index, byte currentCSA) CurrentCSA = currentCSA; } } + +public readonly struct TextureCacheKey : IEquatable +{ + public SCE_GS_PSM Format { get; } + public int Width { get; } + public int Height { get; } + public string DataHash { get; } + + public TextureCacheKey(SCE_GS_PSM format, int width, int height, byte[] data) + { + Format = format; + Width = width; + Height = height; + + using (var md5 = System.Security.Cryptography.MD5.Create()) + { + DataHash = BitConverter.ToString(md5.ComputeHash(data)); + } + } + + public bool Equals(TextureCacheKey other) + { + return Format == other.Format && + Width == other.Width && + Height == other.Height && + DataHash == other.DataHash; + } + + public override bool Equals(object obj) => obj is TextureCacheKey other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(Format, Width, Height, DataHash); +} \ No newline at end of file diff --git a/PDTools.Files/Textures/PS2/TextureSetPS2Base.cs b/PDTools.Files/Textures/PS2/TextureSetPS2Base.cs index 868a81bb..088681a1 100644 --- a/PDTools.Files/Textures/PS2/TextureSetPS2Base.cs +++ b/PDTools.Files/Textures/PS2/TextureSetPS2Base.cs @@ -16,12 +16,13 @@ namespace PDTools.Files.Textures.PS2; public abstract class TextureSetPS2Base { protected GSMemory _gsMemory = new(); - protected byte[] _inputData; public ushort TotalBlockSize { get; set; } public List pgluTextures { get; set; } = []; public List GSTransfers { get; set; } = []; + protected byte[] _inputData; // TextureSet1.cs uses this, may need refactor + protected void InitializeGSMemory() { // Initialize GS Memory. This will unswizzle each buffer as needed @@ -68,8 +69,9 @@ protected Image GetImageData(PGLUtexture texture, TextureClutPatch textu if (_gsMemory is null) throw new Exception("Not input mode"); - int fullWidth = (int)Math.Pow(2, texture.tex0.TW_TextureWidth); - int fullHeight = (int)Math.Pow(2, texture.tex0.TH_TextureHeight); + // faster and cleaner to use standard C# bit-shifting + int fullWidth = 1 << texture.tex0.TW_TextureWidth; + int fullHeight = 1 << texture.tex0.TH_TextureHeight; byte[] textureData; uint[] palette = null; @@ -105,17 +107,15 @@ protected Image GetImageData(PGLUtexture texture, TextureClutPatch textu palette, (csa * 32)); break; - case SCE_GS_PSM.SCE_GS_PSMCT16: // TODO: this doesn't work properly when csa > 0 - ushort[] palette16 = new ushort[8 * 2]; - _gsMemory.ReadTexPSMCT16(cbp, - 1, - 0, 0, - 8, 2, // Always 8x2 for PSMT4 - palette16, - csa * 32); + case SCE_GS_PSM.SCE_GS_PSMCT16: + ushort[] palette16_T4 = new ushort[16]; + + // Use CSA * 32 to offset the VRAM read, but read into index 0 of palette16_T4 + _gsMemory.ReadTexPSMCT16(cbp, 1, 0, 0, 8, 2, palette16_T4, csa * 32); Console.WriteLine("Warning: CSA > 0 not properly supported for PSMCT16 yet"); - PSMCT16To32(palette, palette16); + // Convert directly + PSMCT16To32(palette, palette16_T4); break; default: throw new NotImplementedException($"Invalid or not supported palette format {texture.tex0.CPSM_ClutPartPixelFormatSetup}"); @@ -144,17 +144,14 @@ protected Image GetImageData(PGLUtexture texture, TextureClutPatch textu csa * 32); break; case SCE_GS_PSM.SCE_GS_PSMCT16: - ushort[] palette16 = new ushort[16 * 16]; + ushort[] palette16_T8 = new ushort[256]; - _gsMemory.ReadTexPSMCT16(cbp, - 1, - 0, 0, - 8, 2, // Always 16x16 for PSMT8 - palette16, - csa * 32); + // Use CSA * 32 to offset the VRAM read + _gsMemory.ReadTexPSMCT16(cbp, 1, 0, 0, 16, 16, palette16_T8, csa * 32); Console.WriteLine("Warning: CSA > 0 not properly supported for PSMCT16 yet"); - PSMCT16To32(palette, palette16); + // Convert directly + PSMCT16To32(palette, palette16_T8); break; default: @@ -214,10 +211,14 @@ protected Image GetImageData(PGLUtexture texture, TextureClutPatch textu { for (var x = 0; x < fullWidth; x++) { - img[x, y] = pixels[y * fullWidth + x]; - - byte a = texture.tex0.PSM == SCE_GS_PSM.SCE_GS_PSMCT24 ? (byte)0xFF : (byte)Tex1Utils.Normalize(img[x, y].A, 0x00, 0x80, 0x00, 0xFF); - img[x, y] = new Rgba32(img[x, y].R, img[x, y].G, img[x, y].B, a); // Rescale alpha 0-128 to 0-256. PS2 things + // 1. Grab raw pixel struct + Rgba32 p = pixels[y * fullWidth + x]; + + // 2. Halve the alpha channel + p.A = texture.tex0.PSM == SCE_GS_PSM.SCE_GS_PSMCT24 ? (byte)0xFF : (byte)Tex1Utils.Normalize(p.A, 0x00, 0x80, 0x00, 0xFF); + + // 3. Assign + img[x, y] = p; } } @@ -272,18 +273,23 @@ protected static Rgba32[] MakeTiledPalette(Span pal) return outpal; } - protected static void PSMCT16To32(uint[] palette, ushort[] palette16) - { - // Page 72, GS User's Manual - // PSMCT16 stores the higher 5 bits of each color when converting to PSMCT32 - for (int i = 0; i < 16; i++) - { - byte r = (byte)(((palette16[i] >> 0) & 0b11111) << 3); - byte g = (byte)(((palette16[i] >> 5) & 0b11111) << 3); - byte b = (byte)(((palette16[i] >> 10) & 0b11111) << 3); - byte a = (palette16[i] >> 15 == 1) ? (byte)0x80 : (byte)0x00; - - palette[i] = (uint)(r | g << 8 | b << 16 | a << 24); - } - } -} + protected static void PSMCT16To32(uint[] palette, ushort[] palette16) + { + for (int i = 0; i < palette16.Length; i++) + { + // Extract 5-bit values and correctly expand them to 8-bit (0-255) + byte r = (byte)((palette16[i] >> 0) & 0b11111); + byte g = (byte)((palette16[i] >> 5) & 0b11111); + byte b = (byte)((palette16[i] >> 10) & 0b11111); + + r = (byte)((r << 3) | (r >> 2)); + g = (byte)((g << 3) | (g >> 2)); + b = (byte)((b << 3) | (b >> 2)); + + byte a = ((palette16[i] & 0x8000) != 0) ? (byte)0x80 : (byte)0x00; // Use a direct bitmask against the 15th bit + + // Always write to the exact same index in the image's local palette + palette[i] = (uint)(r | g << 8 | b << 16 | a << 24); + } + } +} \ No newline at end of file diff --git a/PDTools.Files/Textures/PS3/PGLUCellTextureInfo.cs b/PDTools.Files/Textures/PS3/PGLUCellTextureInfo.cs index b36182ce..732adde2 100644 --- a/PDTools.Files/Textures/PS3/PGLUCellTextureInfo.cs +++ b/PDTools.Files/Textures/PS3/PGLUCellTextureInfo.cs @@ -251,6 +251,7 @@ public override void Read(BinaryStream bs, long basePos) uint imageNameOffset = bs.ReadUInt32(); bs.Position = imageNameOffset - basePos; SourceFileName = bs.ReadString(StringCoding.ZeroTerminated); + Name = SourceFileName; } internal void CreateDDSData(byte[] imageData, Stream outStream) @@ -275,7 +276,6 @@ internal void CreateDDSData(byte[] imageData, Stream outStream) default: // 32bpp header.PitchOrLinearSize = (Width * 32 + 7) / 8; - //bs.WriteInt32(0); break; } @@ -289,7 +289,6 @@ internal void CreateDDSData(byte[] imageData, Stream outStream) case CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45: header.FormatFlags = DDSPixelFormatFlags.DDPF_FOURCC; - // FourCC if (format == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1) header.FourCCName = "DXT1"; else if (format == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23) @@ -303,20 +302,22 @@ internal void CreateDDSData(byte[] imageData, Stream outStream) header.FourCCName = "DX10"; header.RGBBitCount = 32; - header.RBitMask = 0x000000FF; // RBitMask - header.GBitMask = 0x0000FF00; // GBitMask - header.BBitMask = 0x00FF0000; // BBitMask - header.ABitMask = 0xFF000000; // ABitMask + header.RBitMask = 0x000000FF; // R BitMask + header.GBitMask = 0x0000FF00; // G BitMask + header.BBitMask = 0x00FF0000; // B BitMask + header.ABitMask = 0xFF000000; // A BitMask header.DxgiFormat = DDS_DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM; break; } - // Unswizzle + bool is32bpp = format == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8 + || format == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_D8R8G8B8; - if ((format == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8 || format == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_D8R8G8B8) - && !FormatBits.HasFlag(CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_LN)) + // Unswizzle + if (is32bpp && !FormatBits.HasFlag(CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_LN)) { + // Swizzled (SZ): de-swizzle via Morton order (dimensions are power-of-two). int byteCount = Width * Height * 4; byte[] newImageData = new byte[byteCount]; @@ -335,17 +336,33 @@ internal void CreateDDSData(byte[] imageData, Stream outStream) imageData = newImageData; } + else if (is32bpp) + { + // Linear (LN): rows are padded to the row pitch (Pitch, in bytes; typically + // nextPow2(Width) * 4). DDS/Pfim expect tightly-packed rows, so strip the + // per-row padding here. Without this, any texture whose width isn't a power of + // two decodes sheared into diagonal stripes. + int rowBytes = Width * 4; + int stride = Pitch >= rowBytes ? Pitch : rowBytes; + if (stride != rowBytes && imageData.Length >= stride * Height) + { + byte[] tight = new byte[rowBytes * Height]; + for (int y = 0; y < Height; y++) + Array.Copy(imageData, y * stride, tight, y * rowBytes, rowBytes); + + imageData = tight; + } + } - // Swap channels for DDS - if (format == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8 || format == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_D8R8G8B8) + // Remap channels into the DDS R8G8B8A8 byte order. + // Source is big-endian A8R8G8B8, i.e. bytes [A,R,G,B]; the GCM remap (InR/InG/InB/InA) + // selects, per output channel, the source component using A=0,R=1,G=2,B=3 — which is + // exactly the source byte index. (The previous code byte-reversed each pixel first, + // which scrambled the standard identity remap into R<->G / B<->A channel swaps.) + if (is32bpp) { - var sp = MemoryMarshal.Cast(imageData); for (var i = 0; i < Width * Height * 4; i += 4) { - // Swap endian first - sp[i / 4] = BinaryPrimitives.ReverseEndianness(sp[i / 4]); - - // Remap channels byte r = imageData[i + (byte)InR]; byte g = imageData[i + (byte)InG]; byte b = imageData[i + (byte)InB]; @@ -364,16 +381,43 @@ internal void CreateDDSData(byte[] imageData, Stream outStream) public void InitFromDDSImage(IImage image, CELL_GCM_TEXTURE_FORMAT format) { - FormatBits = format; + // The built data is linear (not swizzled), so the LN flag must be set or the decoder + // (which keys off this register) will Morton-deswizzle it back into garbage. + FormatBits = format | CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_LN; Width = (ushort)image.Width; Height = (ushort)image.Height; - Pitch = Width * 4; - MipmapLevelLast = (byte)image.MipMaps.Length; + + // RSX linear (LN) row pitch = bytes per row that PD writes, and it's format-specific: + // 32bpp (A8R8G8B8/D8R8G8B8): nextPow2(Width) * 4 (e.g. 46px -> 64px -> 256; a tight 184 + // isn't 64-aligned and the GPU rejects it). FromStandardImage row-pads data to match. + // DXT1 : blocksPerRow * 8 (8 bytes per 4x4 block) + // DXT23/DXT45 : blocksPerRow * 16 (16 bytes per block) + // The old code wrote Width*4 for ALL DXT, which equals blocksPerRow*16 for block-aligned + // widths (so DXT23/45 were right) but is DOUBLE the DXT1 pitch -> the RSX over-strides each + // block row and runs off the data half-way down -> garbled bottom half in-game. + bool is32bpp = format == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8 + || format == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_D8R8G8B8; + int blocksPerRow = (Width + 3) / 4; + Pitch = format switch + { + CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1 => blocksPerRow * 8, + CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23 => blocksPerRow * 16, + CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45 => blocksPerRow * 16, + _ => is32bpp ? NextPowerOfTwo(Width) * 4 : Width * 4, + }; + + // PD's GPB textures live in main (XDR) memory, not RSX local video memory. Writing the + // default LOCAL makes the GPU sample texels from the wrong memory pool -> in-game crash. + Location = CELL_GCM_LOCATION.CELL_GCM_LOCATION_MAIN; + + // MipmapLevelLast is a 1-based level COUNT (PD writes 1 for a single-mip texture); Pfim's + // MipMaps does not include the base level, so add 1. We always build a single mip (-m 1). + MipmapLevelLast = (byte)(image.MipMaps.Length + 1); var cellBufferInfo = BufferInfo as CellTextureBuffer; cellBufferInfo.Width = (ushort)image.Width; cellBufferInfo.Height = (ushort)image.Height; - cellBufferInfo.LastMipmapLevel = (byte)image.MipMaps.Length; + cellBufferInfo.LastMipmapLevel = MipmapLevelLast; cellBufferInfo.FormatBits = format | CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_LN; } @@ -405,27 +449,250 @@ public bool FromStandardImage(string path, CELL_GCM_TEXTURE_FORMAT format) pixels[j] = BinaryPrimitives.ReverseEndianness(pixels[j]); } - BufferInfo.ImageData = ddsData; + // Row-pad the tightly-packed pixels up to the RSX pitch (nextPow2(Width)*4) so the + // stored layout matches PD/hardware. No-op when Pitch == Width*4 (pow2 widths, DXT). + BufferInfo.ImageData = PadRowsToPitch(ddsData, Width, Height, Pitch); SourceFileName = Path.GetFileNameWithoutExtension(path); File.Delete(ddsFileName); return true; } + /// The pixel format a .dds file actually stores (probed from its header). + private enum DdsSourceFormat { Unknown, Uncompressed, Dxt1, Dxt3, Dxt5 } + + /// + /// Probes a DDS file's header for the pixel format it ACTUALLY stores and where its pixel data + /// begins. Handles legacy FourCC DXTn, DX10-extended (DXGI) BC/uncompressed, and raw RGB(A). + /// + private static (DdsSourceFormat fmt, int dataOffset) ProbeDds(byte[] file) + { + const uint DDPF_FOURCC = 0x4; + uint pfFlags = BinaryPrimitives.ReadUInt32LittleEndian(file.AsSpan(0x50)); + string fourCC = Encoding.ASCII.GetString(file, 0x54, 4); + + if ((pfFlags & DDPF_FOURCC) != 0) + { + if (fourCC == "DX10") + { + // DX10 extended header (20 bytes) follows the 0x80 base header; dxgiFormat is at 0x80. + uint dxgi = BinaryPrimitives.ReadUInt32LittleEndian(file.AsSpan(0x80)); + DdsSourceFormat f = dxgi switch + { + 70 or 71 or 72 => DdsSourceFormat.Dxt1, // BC1 (typeless/unorm/unorm_srgb) + 73 or 74 or 75 => DdsSourceFormat.Dxt3, // BC2 + 76 or 77 or 78 => DdsSourceFormat.Dxt5, // BC3 + _ => DdsSourceFormat.Uncompressed, // e.g. 28 R8G8B8A8, 87 B8G8R8A8 + }; + return (f, 0x94); + } + + DdsSourceFormat ff = fourCC switch + { + "DXT1" => DdsSourceFormat.Dxt1, + "DXT2" or "DXT3" => DdsSourceFormat.Dxt3, + "DXT4" or "DXT5" => DdsSourceFormat.Dxt5, + _ => DdsSourceFormat.Unknown, + }; + return (ff, 0x80); + } + + // No FourCC -> uncompressed RGB(A) described by the legacy bit masks. + return (DdsSourceFormat.Uncompressed, 0x80); + } + + /// + /// Decodes a Pfim-loaded DDS into tightly-packed canonical RGBA bytes ([R,G,B,A] per pixel). + /// Pfim normalises any source channel order/format (DXT, A8R8G8B8, B8G8R8A8, DX10, ...) into its + /// BGRA buffer, so this works regardless of how the .dds was authored. + /// + private static byte[] DecodeToRgba(IImage dds) + { + int w = dds.Width, h = dds.Height, stride = dds.Stride; + byte[] src = dds.Data; + byte[] outp = new byte[w * h * 4]; + int di = 0; + switch (dds.Format) + { + case ImageFormat.Rgba32: // Pfim Rgba32 == BGRA byte order + for (int y = 0; y < h; y++) + { + int row = y * stride; + for (int x = 0; x < w; x++) + { + int s = row + x * 4; + outp[di++] = src[s + 2]; // R + outp[di++] = src[s + 1]; // G + outp[di++] = src[s + 0]; // B + outp[di++] = src[s + 3]; // A + } + } + break; + case ImageFormat.Rgb24: // BGR byte order, opaque + for (int y = 0; y < h; y++) + { + int row = y * stride; + for (int x = 0; x < w; x++) + { + int s = row + x * 3; + outp[di++] = src[s + 2]; // R + outp[di++] = src[s + 1]; // G + outp[di++] = src[s + 0]; // B + outp[di++] = 255; // A + } + } + break; + default: + throw new NotSupportedException($"Unsupported DDS pixel layout for decode: {dds.Format}"); + } + return outp; + } + + /// + /// Builds a Cell texture from a .dds file. The filename flag (DXT1/DXT23/DXT45/A8R8G8B8/...) is the + /// authority on the OUTPUT format; this probes what the .dds actually contains and, if it already + /// matches a DXT target, copies the blocks verbatim (lossless, the inverse of ). + /// Otherwise it decodes the image and re-encodes/repacks into the requested format: 32bpp targets + /// repack channels into PD's big-endian A8R8G8B8, mismatched DXT targets are BC-encoded. + /// No texconv involved. + /// + public bool FromDDS(string path, CELL_GCM_TEXTURE_FORMAT format) + { + byte[] file = File.ReadAllBytes(path); + if (file.Length < 0x80 || file[0] != 'D' || file[1] != 'D' || file[2] != 'S' || file[3] != ' ') + { + Console.WriteLine($"Not a valid DDS file: {path}"); + return false; + } + + var (srcFmt, dataOffset) = ProbeDds(file); + + var dds = Pfimage.FromFile(path); + InitFromDDSImage(dds, format); + + // We only ever store the base level, so present the texture as single-mip regardless of any + // mips the source .dds carried (avoids claiming mip data we didn't write). + MipmapLevelLast = 1; + if (BufferInfo is CellTextureBuffer cbuf) + cbuf.LastMipmapLevel = 1; + + bool targetIsDxt = format is CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1 + or CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23 + or CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45; + + if (targetIsDxt) + { + DdsSourceFormat targetAsSrc = format switch + { + CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1 => DdsSourceFormat.Dxt1, + CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23 => DdsSourceFormat.Dxt3, + _ => DdsSourceFormat.Dxt5, + }; + + int blocksPerRow = (Width + 3) / 4; + int blocksPerCol = (Height + 3) / 4; + int blockBytes = blocksPerRow * blocksPerCol * (targetAsSrc == DdsSourceFormat.Dxt1 ? 8 : 16); + + if (srcFmt == targetAsSrc && dataOffset + blockBytes <= file.Length) + { + // Lossless: the .dds already holds exactly this DXT format. Standard DDS block order + // == PD PS3 order, so copy the base-level blocks verbatim (no BC re-encode). + BufferInfo.ImageData = file.AsMemory(dataOffset, blockBytes); + } + else + { + // Source is uncompressed or a different BC format -> decode and BC-encode to target. + byte[] rgba = DecodeToRgba(dds); + var encoder = new BCnEncoder.Encoder.BcEncoder(); + encoder.OutputOptions.Format = targetAsSrc switch + { + DdsSourceFormat.Dxt1 => BCnEncoder.Shared.CompressionFormat.Bc1, + DdsSourceFormat.Dxt3 => BCnEncoder.Shared.CompressionFormat.Bc2, + _ => BCnEncoder.Shared.CompressionFormat.Bc3, + }; + encoder.OutputOptions.Quality = BCnEncoder.Encoder.CompressionQuality.BestQuality; + encoder.OutputOptions.GenerateMipMaps = false; + BufferInfo.ImageData = encoder.EncodeToRawBytes(rgba, Width, Height, BCnEncoder.Encoder.PixelFormat.Rgba32)[0]; + } + } + else + { + // 32bpp target (A8R8G8B8 / D8R8G8B8): decode to canonical RGBA, then store PD's big-endian + // [A,R,G,B] byte order, row-padded to the RSX pitch. Lossless for any 32bpp source. + byte[] rgba = DecodeToRgba(dds); + byte[] tight = new byte[Width * Height * 4]; + for (int i = 0; i < Width * Height; i++) + { + byte r = rgba[i * 4 + 0], g = rgba[i * 4 + 1], b = rgba[i * 4 + 2], a = rgba[i * 4 + 3]; + tight[i * 4 + 0] = a; + tight[i * 4 + 1] = r; + tight[i * 4 + 2] = g; + tight[i * 4 + 3] = b; + } + BufferInfo.ImageData = PadRowsToPitch(tight, Width, Height, Pitch); + } + + SourceFileName = Path.GetFileNameWithoutExtension(path); + return true; + } + + private static int NextPowerOfTwo(int value) + { + int p = 1; + while (p < value) + p <<= 1; + return p; + } + + /// + /// Pads each 4-bytes-per-pixel row of (width*4 bytes) out to + /// bytes. Returns the input unchanged when no padding is needed. + /// + private static Memory PadRowsToPitch(Memory tight, int width, int height, int pitch) + { + int rowBytes = width * 4; + if (pitch <= rowBytes) + return tight; + + byte[] padded = new byte[pitch * height]; + Span src = tight.Span; + for (int y = 0; y < height; y++) + src.Slice(y * rowBytes, rowBytes).CopyTo(padded.AsSpan(y * pitch)); + return padded; + } + + /// + /// Serialises this texture to a standard .dds (DXT FourCC for DXT1/23/45, a DX10 R8G8B8A8 header + /// for 32bpp) - the editable form used by the lossless DDS round-trip. Inverse of . + /// public byte[] GetDDS() { using var ms = new MemoryStream(); - CreateDDSData(BufferInfo.ImageData.ToArray(), ms); // Change format for DXT10 if we're doing a direct extract to dds + CreateDDSData(BufferInfo.ImageData.ToArray(), ms); ms.Position = 0; return ms.ToArray(); } + public override string GetPixelFormatName() + { + CELL_GCM_TEXTURE_FORMAT format = FormatBits & ~CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_LN; + return format switch + { + CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_A8R8G8B8 => "A8R8G8B8", + CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_D8R8G8B8 => "D8R8G8B8", + CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1 => "DXT1", + CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23 => "DXT23", + CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45 => "DXT45", + _ => format.ToString(), + }; + } + public override Image GetAsImage() { // TODO: don't make a dds first. decode straight away. using var ms = new MemoryStream(); - CreateDDSData(BufferInfo.ImageData.ToArray(), ms); // Change format for DXT10 if we're doing a direct extract to dds + CreateDDSData(BufferInfo.ImageData.ToArray(), ms); ms.Position = 0; var dds = Pfimage.FromStream(ms); @@ -436,8 +703,11 @@ public override Image GetAsImage() } else if (dds.Format == ImageFormat.Rgba32) { - // Without the alignment, for some reason pfim's data becomes weird - var i = Image.LoadPixelData(dds.Data, (int)Utils.MiscUtils.AlignValue((uint)dds.Width, 4), dds.Height); + // Image data is now tightly packed (rows de-padded in CreateDDSData), so the + // image width matches the data exactly. The previous code rounded the width up + // to a multiple of 4, which made LoadPixelData over-read and throw for any + // texture whose width wasn't a multiple of 4 (producing no output at all). + var i = Image.LoadPixelData(dds.Data, dds.Width, dds.Height); return i; } else @@ -450,7 +720,18 @@ public override Image GetAsImage() private static void ConvertFileToDDS(string fileName, CELL_GCM_TEXTURE_FORMAT imgFormat) { - string arguments = $"\"{fileName}\""; + // Strip the PNG's colour-profile chunks (sRGB/gAMA/iCCP/cHRM) before handing it to texconv. + // texconv/WIC otherwise honor an embedded profile and gamma-convert the pixels - WITHOUT a + // profile it treats the data as-is (raw passthrough) for every output format. This replaces + // the old "-srgbo" flag, which was passthrough for A8R8G8B8 but BRIGHTENED DXT (linear->sRGB) + // so unmodified DXT textures came back too bright. Stripping (no srgb flag) is byte-exact + // passthrough for A8R8G8B8/D8R8G8B8 AND DXT, for both chunkless dumps and editor-saved PNGs. + string tempDir = Path.Combine(Path.GetTempPath(), "txs3conv_" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(tempDir); + string strippedPng = Path.Combine(tempDir, Path.GetFileName(fileName)); // same base name -> /.dds + StripPngColorChunks(fileName, strippedPng); + + string arguments = $"\"{strippedPng}\""; if (imgFormat == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1) arguments += " -f DXT1"; else if (imgFormat == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23) @@ -465,11 +746,54 @@ private static void ConvertFileToDDS(string fileName, CELL_GCM_TEXTURE_FORMAT im arguments += " -y" // Overwrite if it exists + " -m 1" // Don't care about extra mipmaps + " -nologo" // No copyright logo - + " -srgb" // Auto correct gamma + $" -o {Path.GetDirectoryName(fileName)}"; // Set directory to file input's directory - Process converter = Process.Start(Path.Combine(Directory.GetCurrentDirectory(), "texconv.exe"), arguments); + // texconv.exe is deployed next to the converter executable. Resolve it from the + // app's base directory (not the caller's CWD) so the build works no matter where + // the process was launched from + Process converter = Process.Start(Path.Combine(AppContext.BaseDirectory, "texconv.exe"), arguments); converter.WaitForExit(); + + try { Directory.Delete(tempDir, true); } catch { /* best-effort temp cleanup */ } + } + + /// + /// Copies PNG to , dropping the colour-profile + /// ancillary chunks (sRGB/gAMA/iCCP/cHRM) so texconv doesn't gamma-convert by them. Pixels are + /// untouched. Falls back to a plain copy if the file isn't a PNG. + /// + private static void StripPngColorChunks(string src, string dst) + { + byte[] d = File.ReadAllBytes(src); + ReadOnlySpan sig = [0x89, (byte)'P', (byte)'N', (byte)'G', 0x0D, 0x0A, 0x1A, 0x0A]; + if (d.Length < 8 || !d.AsSpan(0, 8).SequenceEqual(sig)) + { + File.Copy(src, dst, overwrite: true); + return; + } + + using var outFs = new FileStream(dst, FileMode.Create); + outFs.Write(d, 0, 8); + int p = 8; + while (p + 8 <= d.Length) + { + int len = (d[p] << 24) | (d[p + 1] << 16) | (d[p + 2] << 8) | d[p + 3]; + int total = 12 + len; + if (total < 12 || p + total > d.Length) + break; + + bool isColourChunk = (d[p + 4] == 's' && d[p + 5] == 'R' && d[p + 6] == 'G' && d[p + 7] == 'B') + || (d[p + 4] == 'g' && d[p + 5] == 'A' && d[p + 6] == 'M' && d[p + 7] == 'A') + || (d[p + 4] == 'i' && d[p + 5] == 'C' && d[p + 6] == 'C' && d[p + 7] == 'P') + || (d[p + 4] == 'c' && d[p + 5] == 'H' && d[p + 6] == 'R' && d[p + 7] == 'M'); + if (!isColourChunk) + outFs.Write(d, p, total); + + bool isEnd = d[p + 4] == 'I' && d[p + 5] == 'E' && d[p + 6] == 'N' && d[p + 7] == 'D'; + p += total; + if (isEnd) + break; + } } } diff --git a/PDTools.Files/Textures/PSP/PGLUGETextureInfo.cs b/PDTools.Files/Textures/PSP/PGLUGETextureInfo.cs index 20091085..5211b4af 100644 --- a/PDTools.Files/Textures/PSP/PGLUGETextureInfo.cs +++ b/PDTools.Files/Textures/PSP/PGLUGETextureInfo.cs @@ -4,12 +4,14 @@ using System.Drawing; using System.IO; using System.Linq; +using System.Numerics; using System.Reflection.Emit; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; using BCnEncoder.Decoder; +using BCnEncoder.Encoder; using BCnEncoder.Shared; using PDTools.Files.Textures.PS2; @@ -19,6 +21,7 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Quantization; using Syroot.BinaryData; @@ -106,13 +109,30 @@ public override void Read(BinaryStream bs, long basePos) CommandList.Read(bs, this); } + public override string GetPixelFormatName() => TPF.tpf switch + { + eSCE_GE_TPF.SCE_GE_TPF_8888 => "8888", + eSCE_GE_TPF.SCE_GE_TPF_IDTEX4 => "IDTEX4", + eSCE_GE_TPF.SCE_GE_TPF_IDTEX8 => "IDTEX8", + eSCE_GE_TPF.SCE_GE_TPF_DXT1 => "DXT1", + eSCE_GE_TPF.SCE_GE_TPF_DXT3 => "DXT3", + eSCE_GE_TPF.SCE_GE_TPF_DXT5 => "DXT5", + _ => TPF.tpf.ToString(), + }; + public override Image GetAsImage() { var mip1 = MipInfos[0]; + + // mip1.Width is the texture buffer stride: the real width padded up to a power of + // two. Pixels are decoded at that stride, then cropped to the real display size. int width = mip1.Width; - int height = 1 << mip1.Unk1; - int regionWidth = (int)(width * UMIN); + // The real (display) dimensions come from the buffer info. The previous code derived + // the crop width from width * UMIN, but UMIN is a texture-coordinate bound, not the + // real/padded ratio, so it left padding columns in (e.g. a 16-wide texture in a + // 32-wide buffer came out 32 wide). Height already uses BufferInfo.Height directly. + int regionWidth = BufferInfo.Width; int regionHeight = BufferInfo.Height; //(int)(height * VMIN); Image img = new Image(width, BufferInfo.Height); @@ -128,6 +148,22 @@ public override Image GetAsImage() throw new NotImplementedException("SCE_GE_TPF_4444 not yet implemented."); break; case eSCE_GE_TPF.SCE_GE_TPF_8888: + { + byte[] data = new byte[BufferInfo.ImageData.Span.Length]; + BufferInfo.ImageData.Span.CopyTo(data); + + if (TMODE.hsm == SCE_GE_TMODE_HSM.SCE_GE_TMODE_HSM_HIGHSPEED) + Swizzle(BufferInfo.ImageData.Span, data, width, BufferInfo.Height, GEUtils.BitsPerPixel(TPF.tpf)); + + // PSP GU_PSM_8888 stores each pixel as little-endian R,G,B,A bytes, + // which maps directly onto Rgba32. + ReadOnlySpan pixels = MemoryMarshal.Cast(data); + for (var y = 0; y < BufferInfo.Height; y++) + { + for (var x = 0; x < width; x++) + img[x, y] = pixels[y * width + x]; + } + } break; case eSCE_GE_TPF.SCE_GE_TPF_IDTEX4: case eSCE_GE_TPF.SCE_GE_TPF_IDTEX8: @@ -141,13 +177,17 @@ public override Image GetAsImage() Swizzle(BufferInfo.ImageData.Span, data, width, BufferInfo.Height, bpp); Span clut = MemoryMarshal.Cast(ClutBufferInfo.ClutData); - BitStream bs = new BitStream(BitStreamMode.Read, data, BitStreamSignificantBitOrder.MSB); + // IDTEX4 packs two 4-bit indices per byte; PSP (GU_PSM_T4) stores the + // even pixel in the LOW nibble and the odd pixel in the HIGH nibble. for (var y = 0; y < BufferInfo.Height; y++) { for (var x = 0; x < width; x++) { - int idx = (int)bs.ReadBits(bpp); + int pixelIndex = (y * width) + x; + int idx = bpp == 8 + ? data[pixelIndex] + : ((pixelIndex & 1) == 0 ? data[pixelIndex >> 1] & 0x0F : data[pixelIndex >> 1] >> 4); img[x, y] = new Rgba32(clut[idx]); } } @@ -175,26 +215,28 @@ public override Image GetAsImage() byte[] data = new byte[BufferInfo.ImageData.Span.Length]; BufferInfo.ImageData.Span.CopyTo(data); + // PSP stores the BCn colour endpoints/indices halves swapped vs BCn: + // [idx_lo, idx_hi, color0, color1] on PSP -> [color0, color1, idx_lo, idx_hi]. var src = MemoryMarshal.Cast(data); - int size = data.Length; - for (int j = 0; size >= 16; size -= 16, j++) + if (TPF.tpf == eSCE_GE_TPF.SCE_GE_TPF_DXT1) { - ushort[] converted = new ushort[8]; - - converted[4] = src[1]; - converted[5] = src[2]; - converted[6] = src[3]; - converted[7] = src[0]; - - converted[0] = src[6]; - converted[1] = src[7]; - converted[2] = src[4]; - converted[3] = src[5]; - - for (int i = 0; i < 8; i++) - src[i] = converted[i]; - - src = src[8..]; + // DXT1 blocks are 8 bytes (4 u16) = just the colour block. + for (int i = 0; i + 4 <= src.Length; i += 4) + { + (src[i + 0], src[i + 2]) = (src[i + 2], src[i + 0]); + (src[i + 1], src[i + 3]) = (src[i + 3], src[i + 1]); + } + } + else + { + // DXT3/DXT5 blocks are 16 bytes (8 u16) = [alpha block][colour block]. + // The alpha block (u16 0..3) is stored as-is; only the colour block (u16 4..7) + // has its endpoint/index halves swapped. + for (int i = 0; i + 8 <= src.Length; i += 8) + { + (src[i + 4], src[i + 6]) = (src[i + 6], src[i + 4]); + (src[i + 5], src[i + 7]) = (src[i + 7], src[i + 5]); + } } ColorRgba32[] colors = decoder.DecodeRaw(data.ToArray(), width, BufferInfo.Height, bcType); @@ -231,6 +273,303 @@ void Swizzle(Span data, Span output, int width, int height, int bpp) } } } + + /// + /// Row stride (in texels) for a PSP texture: a power-of-two width for 3SXT containers, or a + /// 16-byte-aligned row for Tpp1 containers (16-byte alignment is required by the swizzler). + /// + public static int RowStrideTexels(int width, int bpp, bool tpp1) + { + // The swizzler works on 16-byte blocks, so the row stride must be at least 16 bytes. + int minTexels = 16 * 8 / bpp; + + if (tpp1) + { + int strideBytes = (((width * bpp + 7) / 8) + 15) & ~15; + return Math.Max(strideBytes * 8 / bpp, minTexels); + } + + return Math.Max((int)BitOperations.RoundUpToPowerOf2((uint)width), minTexels); + } + + /// + /// Builds a PSP 8888/RGBA texture from a standard image file. The pixel data is padded to the + /// row stride and swizzled (high-speed mode), matching how the game stores its own 8888 + /// textures. selects the Tpp1 (16-byte-aligned) stride instead of 3SXT. + /// + public static PGLUGETextureInfo CreateFrom8888(string imagePath, bool tpp1 = false) + { + using Image image = Image.Load(imagePath); + + int width = image.Width; + int height = image.Height; + int mipWidth = RowStrideTexels(width, 32, tpp1); + int strideBytes = mipWidth * 4; + int dataHeight = (height + 7) & ~7; // swizzle operates on 8-row blocks + + // Tightly-packed source pixels (R,G,B,A), re-laid at the padded row stride. + byte[] src = new byte[width * height * 4]; + image.CopyPixelDataTo(src); + + byte[] linear = new byte[strideBytes * dataHeight]; + for (int y = 0; y < height; y++) + Array.Copy(src, y * width * 4, linear, y * strideBytes, width * 4); + + byte[] swizzled = new byte[strideBytes * dataHeight]; + SwizzleForWrite(linear, swizzled, mipWidth, dataHeight, 32); + + var tex = new PGLUGETextureInfo(); + tex.Name = Path.GetFileName(imagePath); + tex.TPF.tpf = eSCE_GE_TPF.SCE_GE_TPF_8888; + tex.TMODE.hsm = SCE_GE_TMODE_HSM.SCE_GE_TMODE_HSM_HIGHSPEED; + tex.MipInfos[0] = new GEMipInfo { Width = (ushort)mipWidth }; + tex.BufferInfo = new GETextureBuffer + { + Width = (ushort)width, + Height = (ushort)height, + FormatBits = eSCE_GE_TPF.SCE_GE_TPF_8888, + LastMipmapLevel = 1, + ImageData = swizzled, + ImageSize = (uint)swizzled.Length, + }; + return tex; + } + + /// + /// Builds a PSP (3SXT) paletted texture (IDTEX4 = 16 colors, IDTEX8 = 256 colors) from a + /// standard image file. The image is colour-quantized into a 32-bit (C8888) CLUT, the indices + /// are packed, padded to a power-of-two stride and swizzled (high-speed mode). + /// + public static PGLUGETextureInfo CreateFromIndexed(string imagePath, eSCE_GE_TPF format, bool tpp1 = false) + { + int maxColors; + int bpp; + if (format == eSCE_GE_TPF.SCE_GE_TPF_IDTEX4) { maxColors = 16; bpp = 4; } + else if (format == eSCE_GE_TPF.SCE_GE_TPF_IDTEX8) { maxColors = 256; bpp = 8; } + else throw new ArgumentException($"{format} is not an indexed PSP format.", nameof(format)); + + using Image image = Image.Load(imagePath); + int width = image.Width; + int height = image.Height; + int mipWidth = RowStrideTexels(width, bpp, tpp1); + int strideBytes = (mipWidth * bpp) / 8; + + // Tpp1 always swizzles; 3SXT swizzles only when the width fills the stride (mipWidth==pow2(W)). + // Swizzled data must be whole 8-row blocks; LINEAR data uses the real height (PD doesn't pad + // linear textures - padding it to align8 inflates the file and shifts later GPB textures). + int texWidth = (int)BitOperations.RoundUpToPowerOf2((uint)width); + bool swizzle = tpp1 || mipWidth == texWidth; + int dataHeight = swizzle ? ((height + 7) & ~7) : height; + + byte[] sourcePixels = new byte[width * height * 4]; + image.CopyPixelDataTo(sourcePixels); // tightly-packed R,G,B,A + + byte[] clutData = new byte[maxColors * 4]; // C8888, padded to the full palette + byte[] pixelIndices = new byte[width * height]; + + // Prefer an exact palette: if the image already has <= maxColors unique colours (true for + // the game's own UI/map textures) this is lossless. Quantization is only a fallback for + // true-colour inputs, since the quantizer's nearest-match underweights alpha differences. + var colorToIndex = new Dictionary(); + bool exact = true; + for (int i = 0; i < width * height; i++) + { + uint color = BitConverter.ToUInt32(sourcePixels, i * 4); + if (!colorToIndex.TryGetValue(color, out int idx)) + { + if (colorToIndex.Count >= maxColors) { exact = false; break; } + idx = colorToIndex.Count; + colorToIndex[color] = idx; + Array.Copy(sourcePixels, i * 4, clutData, idx * 4, 4); + } + pixelIndices[i] = (byte)idx; + } + + if (!exact) + { + var quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = maxColors, Dither = null }); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(Configuration.Default); + using IndexedImageFrame indexed = frameQuantizer.BuildPaletteAndQuantizeFrame( + image.Frames.RootFrame, new SixLabors.ImageSharp.Rectangle(0, 0, width, height)); + + ReadOnlySpan palette = indexed.Palette.Span; + Array.Clear(clutData); + for (int i = 0; i < palette.Length && i < maxColors; i++) + { + clutData[i * 4 + 0] = palette[i].R; + clutData[i * 4 + 1] = palette[i].G; + clutData[i * 4 + 2] = palette[i].B; + clutData[i * 4 + 3] = palette[i].A; + } + + for (int y = 0; y < height; y++) + { + ReadOnlySpan indices = indexed.DangerousGetRowSpan(y); + for (int x = 0; x < width; x++) + pixelIndices[y * width + x] = indices[x]; + } + } + + // Pack indices at the padded row stride. IDTEX4 (GU_PSM_T4) stores the even pixel in + // the LOW nibble and the odd pixel in the HIGH nibble. + byte[] linear = new byte[strideBytes * dataHeight]; + for (int y = 0; y < height; y++) + { + int rowBase = y * strideBytes; + for (int x = 0; x < width; x++) + { + byte idx = pixelIndices[y * width + x]; + if (bpp == 8) + { + linear[rowBase + x] = idx; + } + else + { + int bi = rowBase + (x >> 1); + linear[bi] |= (x & 1) == 0 ? (byte)(idx & 0xF) : (byte)(idx << 4); + } + } + } + + // (swizzle decided up top: Tpp1 always; 3SXT only when mipWidth == pow2(W). A narrow 3SXT + // IDTEX is LINEAR/hsm=0 - swizzling it there mis-sets TSIZE0 -> invisible UI.) + byte[] imageData; + if (swizzle) + { + imageData = new byte[strideBytes * dataHeight]; + SwizzleForWrite(linear, imageData, mipWidth, dataHeight, bpp); + } + else + { + imageData = linear; + } + + var tex = new PGLUGETextureInfo(); + tex.Name = Path.GetFileName(imagePath); + tex.TPF.tpf = format; + tex.TMODE.hsm = swizzle ? SCE_GE_TMODE_HSM.SCE_GE_TMODE_HSM_HIGHSPEED : SCE_GE_TMODE_HSM.SCE_GE_TMODE_HSM_NORMAL; + tex.MipInfos[0] = new GEMipInfo { Width = (ushort)mipWidth }; + tex.BufferInfo = new GETextureBuffer + { + Width = (ushort)width, + Height = (ushort)height, + FormatBits = format, + LastMipmapLevel = 1, + ImageData = imageData, + ImageSize = (uint)imageData.Length, + }; + // 3SXT stores exactly the colours the image uses (its clutinfo carries an explicit NumColors, + // e.g. a 4-colour dot => 4) - matching PD keeps the tail compact. Tpp1 has NO clutinfo: its + // decoder derives the count from CLOAD (whole 8-entry blocks), so its palette MUST stay the + // full maxColors size or the decode runs off the end ("Could not read 32 bytes"). + int usedColors = (exact && !tpp1) ? colorToIndex.Count : maxColors; + byte[] finalClut = new byte[usedColors * 4]; + Array.Copy(clutData, finalClut, usedColors * 4); + + tex.ClutBufferInfo = new GEClutBufferInfo + { + ClutType = SCE_GE_CLUT_CPF.SCE_GE_CLUT_CPF_8888, + NumColors = (ushort)usedColors, + ClutData = finalClut, + }; + return tex; + } + + /// + /// Builds the texture data for a Tpp1 container (8888 / IDTEX4 / IDTEX8). Same encoding as the + /// 3SXT path but using Tpp1's 16-byte-aligned row stride; written by . + /// + public static PGLUGETextureInfo CreateTpp1(string imagePath, eSCE_GE_TPF format) => format switch + { + eSCE_GE_TPF.SCE_GE_TPF_8888 => CreateFrom8888(imagePath, tpp1: true), + eSCE_GE_TPF.SCE_GE_TPF_IDTEX4 => CreateFromIndexed(imagePath, format, tpp1: true), + eSCE_GE_TPF.SCE_GE_TPF_IDTEX8 => CreateFromIndexed(imagePath, format, tpp1: true), + _ => throw new NotSupportedException($"Tpp1 building supports 8888 / IDTEX4 / IDTEX8 (got {format})."), + }; + + /// + /// Builds a PSP (3SXT) DXT1 texture from a standard image file. + /// + public static PGLUGETextureInfo CreateFromDXT1(string imagePath) + => CreateFromDXT(imagePath, eSCE_GE_TPF.SCE_GE_TPF_DXT1); + + /// + /// Builds a PSP (3SXT) DXT1/DXT5 texture from a standard image file. The image is BCn-encoded + /// and the colour block's endpoint/index halves are swapped to PSP order (the alpha block of + /// DXT5 is left as-is). DXT data is stored linearly (not swizzled). + /// + public static PGLUGETextureInfo CreateFromDXT(string imagePath, eSCE_GE_TPF format) + { + bool isDxt5 = format == eSCE_GE_TPF.SCE_GE_TPF_DXT5; + if (format != eSCE_GE_TPF.SCE_GE_TPF_DXT1 && !isDxt5) + throw new ArgumentException($"{format} is not a supported DXT format (use DXT1 or DXT5).", nameof(format)); + + using Image image = Image.Load(imagePath); + int width = image.Width; + int height = image.Height; + int encWidth = (width + 3) & ~3; // DXT blocks are 4x4 + int encHeight = (height + 3) & ~3; + + byte[] source = new byte[width * height * 4]; + image.CopyPixelDataTo(source); // tightly-packed R,G,B,A + + var encoder = new BcEncoder(); + encoder.OutputOptions.Format = isDxt5 ? CompressionFormat.Bc3 : CompressionFormat.Bc1; + encoder.OutputOptions.Quality = CompressionQuality.BestQuality; + encoder.OutputOptions.GenerateMipMaps = false; + byte[] bc = encoder.EncodeToRawBytes(source, width, height, PixelFormat.Rgba32)[0]; + + // BCn colour block stores [color0, color1, idx_lo, idx_hi]; PSP wants + // [idx_lo, idx_hi, color0, color1] - swap the two 4-byte halves of the colour block. + // (DXT5 = 16-byte blocks with the colour block at +8; the 8-byte alpha block is untouched.) + int blockSize = isDxt5 ? 16 : 8; + int colourOffset = isDxt5 ? 8 : 0; + for (int i = 0; i + blockSize <= bc.Length; i += blockSize) + { + for (int k = 0; k < 4; k++) + (bc[i + colourOffset + k], bc[i + colourOffset + 4 + k]) = (bc[i + colourOffset + 4 + k], bc[i + colourOffset + k]); + } + + var tex = new PGLUGETextureInfo(); + tex.Name = Path.GetFileName(imagePath); + tex.TPF.tpf = format; + tex.TMODE.hsm = SCE_GE_TMODE_HSM.SCE_GE_TMODE_HSM_NORMAL; // DXT is not swizzled + tex.MipInfos[0] = new GEMipInfo { Width = (ushort)encWidth }; + tex.BufferInfo = new GETextureBuffer + { + Width = (ushort)width, + Height = (ushort)height, + FormatBits = format, + LastMipmapLevel = 1, + ImageData = bc, + ImageSize = (uint)bc.Length, + }; + return tex; + } + + /// + /// Inverse of - lays out linear pixel rows into the PSP's + /// 16-byte x 8-row swizzled block order. + /// + static void SwizzleForWrite(ReadOnlySpan linear, Span output, int width, int height, int bpp) + { + int stride = (width * bpp) / 8; + int rowBlocks = stride / 16; + int srcOffset = 0; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < stride; x++) + { + int blockX = x / 16; + int blockY = y / 8; + int blockIndex = blockX + (blockY * rowBlocks); + int blockAddress = blockIndex * 16 * 8; + output[blockAddress + (x - blockX * 16) + ((y - blockY * 8) * 16)] = linear[srcOffset]; + srcOffset++; + } + } + } } public class GEMipInfo diff --git a/PDTools.Files/Textures/TextureSet3.cs b/PDTools.Files/Textures/TextureSet3.cs index 4d3474d5..e52b8101 100644 --- a/PDTools.Files/Textures/TextureSet3.cs +++ b/PDTools.Files/Textures/TextureSet3.cs @@ -1,445 +1,1101 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Buffers.Binary; - -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Formats; - -using Syroot.BinaryData.Core; -using Syroot.BinaryData; - -using PDTools.Files.Textures.PS3; -using PDTools.Files.Textures.PS4; -using PDTools.Files.Textures.PSP; -using SixLabors.Fonts; - -namespace PDTools.Files.Textures; - -public class TextureSet3 -{ - public const string MAGIC = "TXS3"; - public const string MAGIC_LE = "3SXT"; - - public List Buffers { get; set; } = []; - public List TextureInfos { get; set; } = []; - public List ClutInfos { get; set; } = []; - - public bool WriteNames { get; set; } - - public bool LittleEndian { get; set; } - - public long DataPointer { get; set; } - - /// - /// Original relocation pointer - /// May be slightly offset (0x200) if the texture set is in a CourseData/PAC due to header - /// - public uint RelocPtr { get; set; } - - public long BaseTextureSetPosition { get; set; } - - public TextureSet3() - { - - } - - public void FromStream(Stream stream, TextureConsoleType consoleType) - { - BaseTextureSetPosition = stream.Position; - - BinaryStream bs = new BinaryStream(stream); - string magic = bs.ReadString(4); - if (magic == "TXS3") - bs.ByteConverter = ByteConverter.Big; - else if (magic == "3SXT") - bs.ByteConverter = ByteConverter.Little; - else - throw new InvalidDataException("Could not parse TXS3 from stream, not a valid TXS3 image file."); - - int fileSize = bs.ReadInt32(); - - if (consoleType == TextureConsoleType.PS4) // 64 bit - { - ReadPS4Header(bs); - } - else if (consoleType == TextureConsoleType.PS3) - { - ReadPS3Header(bs); - } - else if (consoleType == TextureConsoleType.PSP) - { - ReadPSPHeader(bs); - } - else - throw new NotSupportedException($"Console type {consoleType} not supported"); - } - - private void ReadPSPHeader(BinaryStream bs) - { - // Total Header size is 0x40 - - RelocPtr = bs.ReadUInt32(); // Original Position, if bundled - bs.Position += 4; - bs.Position += 4; // Sometimes 1 - - short pgluTexturesCount = bs.ReadInt16(); - short bufferInfoCount = bs.ReadInt16(); - int pgluTexturesOffset = bs.ReadInt32(); - int bufferInfosOffset = bs.ReadInt32(); - uint relocSize = bs.ReadUInt32(); - ushort unkCount_0x24 = bs.ReadUInt16(); - ushort clutMapEntryCount = bs.ReadUInt16(); - uint unkOffset_0x28 = bs.ReadUInt32(); - uint clutMapOffset = bs.ReadUInt32(); - uint unkOffset_0x30 = bs.ReadUInt32(); - ushort unkCount_0x34 = bs.ReadUInt16(); - bs.ReadUInt16(); - uint unkOffset_0x38 = bs.ReadUInt32(); - - if (bufferInfoCount > 0) - { - for (int i = 0; i < bufferInfoCount; i++) - { - bs.Position = BaseTextureSetPosition + (bufferInfosOffset - RelocPtr) + (i * 0x20); - - TextureSet3Buffer bufferInfo = new GETextureBuffer(); - bufferInfo.Read(bs); - Buffers.Add(bufferInfo); - - bs.Position = BaseTextureSetPosition + (bufferInfo.ImageOffset - RelocPtr); - bufferInfo.ImageData = new byte[bufferInfo.ImageSize]; - bs.ReadExactly(bufferInfo.ImageData.Span); - } - } - - if (clutMapEntryCount > 0) - { - for (int i = 0; i < clutMapEntryCount; i++) - { - bs.Position = BaseTextureSetPosition + (clutMapOffset - RelocPtr) + (i * 0x0C); - - GEClutBufferInfo clutInfo = new GEClutBufferInfo(); - clutInfo.Read(bs); - ClutInfos.Add(clutInfo); - - bs.Position = BaseTextureSetPosition + (clutInfo.ClutBufferOffset - RelocPtr); - clutInfo.ClutData = new byte[GEUtils.BitsPerPixel((eSCE_GE_TPF)clutInfo.ClutType) / 8 * clutInfo.NumColors]; - bs.ReadExactly(clutInfo.ClutData); - } - } - - if (pgluTexturesCount > 0) - { - for (int i = 0; i < pgluTexturesCount; i++) - { - bs.Position = BaseTextureSetPosition + (pgluTexturesOffset - RelocPtr) + (i * 0x98); - - PGLUGETextureInfo textureInfo = new PGLUGETextureInfo(); - textureInfo.Read(bs, BaseTextureSetPosition); - TextureInfos.Add(textureInfo); - - textureInfo.BufferInfo = (GETextureBuffer)Buffers[(int)textureInfo.BufferId]; - - if (textureInfo.ClutMapEntryIndex != -1) - textureInfo.ClutBufferInfo = (GEClutBufferInfo)ClutInfos[textureInfo.ClutMapEntryIndex]; - } - } - } - - private void ReadPS3Header(BinaryStream bs) - { - RelocPtr = bs.ReadUInt32(); // Original Position, if bundled - bs.Position += 4; - bs.Position += 4; // Sometimes 1 - - // TODO: Implement proper image count reading - right now we only care about the real present images - short pgluTexturesCount = bs.ReadInt16(); - short bufferInfoCount = bs.ReadInt16(); - int pgluTexturesOffset = bs.ReadInt32(); - int bufferInfosOffset = bs.ReadInt32(); - DataPointer = bs.ReadUInt32(); - - if (bufferInfoCount > 0) - { - for (int i = 0; i < bufferInfoCount; i++) - { - bs.Position = BaseTextureSetPosition + (bufferInfosOffset - RelocPtr) + (i * 0x20); - - TextureSet3Buffer bufferInfo = new CellTextureBuffer(); - bufferInfo.Read(bs); - Buffers.Add(bufferInfo); - } - } - - if (pgluTexturesCount > 0) - { - for (int i = 0; i < pgluTexturesCount; i++) - { - bs.Position = BaseTextureSetPosition + (pgluTexturesOffset - RelocPtr) + (i * 0x44); - - TextureSet3Buffer texture = Buffers[i]; - - PGLUTextureInfo textureInfo = new PGLUCellTextureInfo(); - textureInfo.Read(bs, BaseTextureSetPosition); - TextureInfos.Add(textureInfo); - textureInfo.BufferInfo = Buffers[(int)textureInfo.BufferId]; - - bs.Position = BaseTextureSetPosition + texture.ImageOffset; - texture.ImageData = new byte[texture.ImageSize]; - bs.ReadExactly(texture.ImageData.Span); - } - } - } - - private void ReadPS4Header(BinaryStream bs) - { - long relocPtr = bs.ReadInt64(); - long relocPtr2 = bs.ReadInt64(); - int unk = bs.ReadInt32(); // Unknown, 2 - - short pgluTexturesCount = bs.ReadInt16(); - short bufferInfoCount = bs.ReadInt16(); - long pgluTexturesOffset = bs.ReadInt64(); - long bufferInfosOffset = bs.ReadInt64(); - DataPointer = bs.ReadInt64(); - - // TODO - bs.ReadInt64(); - bs.ReadInt64(); - bs.ReadInt64(); - bs.ReadInt16(); - bs.Position += 14; - bs.ReadInt64(); - bs.Position += 8; - - if (bufferInfoCount > 0) - { - for (int i = 0; i < bufferInfoCount; i++) - { - bs.Position = BaseTextureSetPosition + (bufferInfosOffset - RelocPtr) + (i * 0x30); - - TextureSet3Buffer bufferInfo = new OrbisTextureBuffer(); - bufferInfo.Read(bs); - Buffers.Add(bufferInfo); - - bs.Position = BaseTextureSetPosition + bufferInfo.ImageOffset; - bufferInfo.ImageData = new byte[bufferInfo.ImageSize]; - bs.ReadExactly(bufferInfo.ImageData.Span); - } - } - - if (pgluTexturesCount > 0) - { - for (int i = 0; i < pgluTexturesCount; i++) - { - bs.Position = BaseTextureSetPosition + (pgluTexturesOffset - RelocPtr) + (i * 0x48); - - PGLUTextureInfo textureInfo = new PGLUOrbisTextureInfo(); - textureInfo.Read(bs, BaseTextureSetPosition); - TextureInfos.Add(textureInfo); - textureInfo.BufferInfo = Buffers[(int)textureInfo.BufferId]; - } - } - } - - public void BuildTextureSetFile(string outputName) - { - using var ms = new FileStream(outputName, FileMode.Create); - using var bs = new BinaryStream(ms, ByteConverter.Big); - - if (LittleEndian) - bs.ByteConverter = ByteConverter.Little; - - WriteToStream(bs); - } - - /// - /// Format depends on extension. png, jpg... - /// - /// - public void ConvertToStandardFormat(string outputName) - { - Console.WriteLine($"Processing {outputName} with {TextureInfos.Count} texture(s)..."); - - for (int i = 0; i < TextureInfos.Count; i++) - { - PGLUTextureInfo texture = TextureInfos[i]; - string texturePath = outputName; - - string actualName = !string.IsNullOrEmpty(texture.Name) ? texture.Name : - TextureInfos.Count > 1 ? $"{i}.png" : - $"{Path.GetFileNameWithoutExtension(outputName)}.png"; - - if (TextureInfos.Count > 1) - texturePath = Path.Combine(Path.GetDirectoryName(outputName), Path.GetFileNameWithoutExtension(outputName), actualName); - else - texturePath = Path.Combine(Path.GetDirectoryName(texturePath), actualName); - - Console.WriteLine($"- Converting '{texturePath}'..."); - - using var img = texture.GetAsImage(); - - texturePath = Path.ChangeExtension(texturePath, ".png"); // Incase texture.Name doesn't have an extension, or we are outputting to dds. - - Directory.CreateDirectory(Path.GetDirectoryName(texturePath)); - img.Save(texturePath); - } - } - - public void AddTexture(PGLUTextureInfo texture) - { - ArgumentNullException.ThrowIfNull(texture, nameof(texture)); - ArgumentNullException.ThrowIfNull(texture.BufferInfo, nameof(texture.BufferInfo)); - - TextureInfos.Add(texture); - Buffers.Add(texture.BufferInfo); - - texture.BufferId = (uint)Buffers.Count - 1; - - } - - /// - /// Writes the texture set to a stream. - /// - /// Stream to write to. - /// Base position for the texture set - /// Whether to write the image data. If not, writing the image data and relinking offsets/finishing up the TXS3 header should be done at your own discretion. - public void WriteToStream(BinaryStream bs, int txsBasePos = 0, bool writeImageData = true) - { - BaseTextureSetPosition = txsBasePos; - - BuildPS3TextureSet(bs, txsBasePos, writeImageData); - } - - private void BuildPS3TextureSet(BinaryStream bs, int txsBasePos, bool writeImageData = true) - { - if (!LittleEndian) - bs.WriteString(MAGIC, StringCoding.Raw); - else - bs.WriteString(MAGIC_LE); - - bs.Position = txsBasePos + 0x14; - bs.WriteInt16((short)Buffers.Count); // Image Params Count; - bs.WriteInt16((short)TextureInfos.Count); // Image Info Count; - bs.WriteInt32(txsBasePos + 0x40); // PGLTexture Offset (render params) - - int imageInfoOffset = txsBasePos + 0x40 + (0x44 * TextureInfos.Count); - bs.WriteInt32(imageInfoOffset); - - // Unk offset, Set to header end - // FIXME: This might not work for images in models or course packs - bs.WriteInt32(0x100); - - // Write textures's render params - bs.Position = txsBasePos + 0x40; - foreach (var textureInfo in TextureInfos) - textureInfo.Write(bs); - - // Skip the texture info for now - bs.Position = imageInfoOffset + (Buffers.Count * 0x20); - - int mainHeaderSize = (int)bs.Position; - - // Write texture names - int lastNamePos = (int)bs.Position; - for (int i = 0; i < TextureInfos.Count; i++) - { - PGLUTextureInfo texture = TextureInfos[i]; - if (WriteNames && !string.IsNullOrEmpty(texture.Name)) - { - bs.WriteString(texture.Name, StringCoding.ZeroTerminated); - - // Update name offset - bs.Position = txsBasePos + 0x40 + (i * 0x44); - bs.Position += 0x40; // Skip to name offset field - bs.WriteInt32(lastNamePos); - } - - bs.Position = lastNamePos; - } - - int endPos = (int)bs.Position; - if (writeImageData) - { - bs.Align(0x80, grow: true); - endPos = (int)bs.Position; - } - - // Actually write the textures now and their linked information - for (int i = 0; i < TextureInfos.Count; i++) - { - int imageOffset = 0, endImageOffset = 0; - if (writeImageData) - { - imageOffset = (int)bs.Position; - bs.Write(Buffers[i].ImageData.Span); - endImageOffset = (int)bs.Position; - } - - bs.Position = txsBasePos + imageInfoOffset + (i * 0x20); - - var textureInfo = TextureInfos[i] as PGLUCellTextureInfo; - bs.WriteInt32(imageOffset); - bs.WriteInt32(endImageOffset - imageOffset); // Size - bs.WriteByte(2); - bs.WriteByte((byte)textureInfo.FormatBits); - bs.WriteByte((byte)(textureInfo.MipmapLevelLast)); - bs.WriteByte(1); - bs.WriteUInt16(textureInfo.Width); - bs.WriteUInt16(textureInfo.Height); - bs.WriteUInt16(1); - bs.WriteUInt16(0); - bs.Position += 12; // Pad - } - - // Finish up main header - bs.Position = txsBasePos + 4; - if (txsBasePos != 0) - { - bs.WriteInt32(0); - bs.WriteInt32(txsBasePos); - bs.WriteInt32(0); - } - else - { - bs.WriteInt32((int)bs.Length); - bs.WriteInt32(0); - bs.WriteInt32(mainHeaderSize); - } - - bs.Position = endPos; - } - - public byte[] GetExternalImageDataOfTexture(Stream stream, PGLUTextureInfo texture, long basePos = 0) - { - stream.Position = basePos + (texture.BufferInfo.ImageOffset - DataPointer); - - var bytes = stream.ReadBytes((int)texture.BufferInfo.ImageSize); - var ms = new MemoryStream(); - (texture as PGLUCellTextureInfo).CreateDDSData(bytes, ms); - - return ms.ToArray(); - } - - public void FromFile(string file, TextureConsoleType consoleType = TextureConsoleType.PS3) - { - using var fs = new FileStream(file, FileMode.Open); - FromStream(fs, consoleType); - } - - - public enum TextureConsoleType - { - PSP, - PS3, - PS4, - }; +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Buffers.Binary; + +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Formats; + +using Syroot.BinaryData.Core; +using Syroot.BinaryData; + +using PDTools.Files.Textures.PS3; +using PDTools.Files.Textures.PS4; +using PDTools.Files.Textures.PSP; +using SixLabors.Fonts; + +namespace PDTools.Files.Textures; + +public class TextureSet3 +{ + public const string MAGIC = "TXS3"; + public const string MAGIC_LE = "3SXT"; + + public List Buffers { get; set; } = []; + public List TextureInfos { get; set; } = []; + public List ClutInfos { get; set; } = []; + + public bool WriteNames { get; set; } + + public bool LittleEndian { get; set; } + + /// + /// The on-disk container this texture set was read from: "TXS3" (PS3), "3SXT" (PSP) or + /// "Tpp1" (PSP compact). Used to tag dumped filenames so the original container + pixel + /// format can be reproduced on rebuild (neither is recoverable from a PNG). + /// + public string ContainerTag { get; set; } + + public long DataPointer { get; set; } + + /// + /// Original relocation pointer + /// May be slightly offset (0x200) if the texture set is in a CourseData/PAC due to header + /// + public uint RelocPtr { get; set; } + + public long BaseTextureSetPosition { get; set; } + + public TextureSet3() + { + + } + + public void FromStream(Stream stream, TextureConsoleType consoleType) + { + BaseTextureSetPosition = stream.Position; + + BinaryStream bs = new BinaryStream(stream); + string magic = bs.ReadString(4); + if (magic == "TXS3") + bs.ByteConverter = ByteConverter.Big; + else if (magic == "3SXT") + bs.ByteConverter = ByteConverter.Little; + else + throw new InvalidDataException("Could not parse TXS3 from stream, not a valid TXS3 image file."); + + ContainerTag = magic; + + int fileSize = bs.ReadInt32(); + + if (consoleType == TextureConsoleType.PS4) // 64 bit + { + ReadPS4Header(bs); + } + else if (consoleType == TextureConsoleType.PS3) + { + ReadPS3Header(bs); + } + else if (consoleType == TextureConsoleType.PSP) + { + ReadPSPHeader(bs); + } + else + throw new NotSupportedException($"Console type {consoleType} not supported"); + } + + private void ReadPSPHeader(BinaryStream bs) + { + // Total Header size is 0x40 + + RelocPtr = bs.ReadUInt32(); // Original Position, if bundled + bs.Position += 4; + bs.Position += 4; // Sometimes 1 + + short pgluTexturesCount = bs.ReadInt16(); + short bufferInfoCount = bs.ReadInt16(); + int pgluTexturesOffset = bs.ReadInt32(); + int bufferInfosOffset = bs.ReadInt32(); + uint relocSize = bs.ReadUInt32(); + ushort unkCount_0x24 = bs.ReadUInt16(); + ushort clutMapEntryCount = bs.ReadUInt16(); + uint unkOffset_0x28 = bs.ReadUInt32(); + uint clutMapOffset = bs.ReadUInt32(); + uint unkOffset_0x30 = bs.ReadUInt32(); + ushort unkCount_0x34 = bs.ReadUInt16(); + bs.ReadUInt16(); + uint unkOffset_0x38 = bs.ReadUInt32(); + + if (bufferInfoCount > 0) + { + for (int i = 0; i < bufferInfoCount; i++) + { + bs.Position = BaseTextureSetPosition + (bufferInfosOffset - RelocPtr) + (i * 0x20); + + TextureSet3Buffer bufferInfo = new GETextureBuffer(); + bufferInfo.Read(bs); + Buffers.Add(bufferInfo); + + bs.Position = BaseTextureSetPosition + (bufferInfo.ImageOffset - RelocPtr); + bufferInfo.ImageData = new byte[bufferInfo.ImageSize]; + bs.ReadExactly(bufferInfo.ImageData.Span); + } + } + + if (clutMapEntryCount > 0) + { + for (int i = 0; i < clutMapEntryCount; i++) + { + bs.Position = BaseTextureSetPosition + (clutMapOffset - RelocPtr) + (i * 0x0C); + + GEClutBufferInfo clutInfo = new GEClutBufferInfo(); + clutInfo.Read(bs); + ClutInfos.Add(clutInfo); + + bs.Position = BaseTextureSetPosition + (clutInfo.ClutBufferOffset - RelocPtr); + clutInfo.ClutData = new byte[GEUtils.BitsPerPixel((eSCE_GE_TPF)clutInfo.ClutType) / 8 * clutInfo.NumColors]; + bs.ReadExactly(clutInfo.ClutData); + } + } + + if (pgluTexturesCount > 0) + { + for (int i = 0; i < pgluTexturesCount; i++) + { + bs.Position = BaseTextureSetPosition + (pgluTexturesOffset - RelocPtr) + (i * 0x98); + + PGLUGETextureInfo textureInfo = new PGLUGETextureInfo(); + textureInfo.Read(bs, BaseTextureSetPosition); + TextureInfos.Add(textureInfo); + + textureInfo.BufferInfo = (GETextureBuffer)Buffers[(int)textureInfo.BufferId]; + + if (textureInfo.ClutMapEntryIndex != -1) + textureInfo.ClutBufferInfo = (GEClutBufferInfo)ClutInfos[textureInfo.ClutMapEntryIndex]; + } + } + } + + private void ReadPS3Header(BinaryStream bs) + { + RelocPtr = bs.ReadUInt32(); // Original Position, if bundled + bs.Position += 4; + bs.Position += 4; // Sometimes 1 + + // TODO: Implement proper image count reading - right now we only care about the real present images + short pgluTexturesCount = bs.ReadInt16(); + short bufferInfoCount = bs.ReadInt16(); + int pgluTexturesOffset = bs.ReadInt32(); + int bufferInfosOffset = bs.ReadInt32(); + DataPointer = bs.ReadUInt32(); + + if (bufferInfoCount > 0) + { + for (int i = 0; i < bufferInfoCount; i++) + { + bs.Position = BaseTextureSetPosition + (bufferInfosOffset - RelocPtr) + (i * 0x20); + + TextureSet3Buffer bufferInfo = new CellTextureBuffer(); + bufferInfo.Read(bs); + Buffers.Add(bufferInfo); + } + } + + if (pgluTexturesCount > 0) + { + for (int i = 0; i < pgluTexturesCount; i++) + { + bs.Position = BaseTextureSetPosition + (pgluTexturesOffset - RelocPtr) + (i * 0x44); + + TextureSet3Buffer texture = Buffers[i]; + + PGLUTextureInfo textureInfo = new PGLUCellTextureInfo(); + textureInfo.Read(bs, BaseTextureSetPosition); + TextureInfos.Add(textureInfo); + + // PS3 texture infos are parallel to buffer infos (texture i <-> buffer i); the + // image data is read into Buffers[i] below, so link the same buffer. (The PS3 Read + // doesn't populate BufferId, so the old Buffers[BufferId] pointed every texture at + // buffer 0 - in a multi-texture set every texture dumped as the first.) + textureInfo.BufferId = (uint)i; + textureInfo.BufferInfo = texture; + + bs.Position = BaseTextureSetPosition + texture.ImageOffset; + texture.ImageData = new byte[texture.ImageSize]; + bs.ReadExactly(texture.ImageData.Span); + } + } + } + + private void ReadPS4Header(BinaryStream bs) + { + long relocPtr = bs.ReadInt64(); + long relocPtr2 = bs.ReadInt64(); + int unk = bs.ReadInt32(); // Unknown, 2 + + short pgluTexturesCount = bs.ReadInt16(); + short bufferInfoCount = bs.ReadInt16(); + long pgluTexturesOffset = bs.ReadInt64(); + long bufferInfosOffset = bs.ReadInt64(); + DataPointer = bs.ReadInt64(); + + // TODO + bs.ReadInt64(); + bs.ReadInt64(); + bs.ReadInt64(); + bs.ReadInt16(); + bs.Position += 14; + bs.ReadInt64(); + bs.Position += 8; + + if (bufferInfoCount > 0) + { + for (int i = 0; i < bufferInfoCount; i++) + { + bs.Position = BaseTextureSetPosition + (bufferInfosOffset - RelocPtr) + (i * 0x30); + + TextureSet3Buffer bufferInfo = new OrbisTextureBuffer(); + bufferInfo.Read(bs); + Buffers.Add(bufferInfo); + + bs.Position = BaseTextureSetPosition + bufferInfo.ImageOffset; + bufferInfo.ImageData = new byte[bufferInfo.ImageSize]; + bs.ReadExactly(bufferInfo.ImageData.Span); + } + } + + if (pgluTexturesCount > 0) + { + for (int i = 0; i < pgluTexturesCount; i++) + { + bs.Position = BaseTextureSetPosition + (pgluTexturesOffset - RelocPtr) + (i * 0x48); + + PGLUTextureInfo textureInfo = new PGLUOrbisTextureInfo(); + textureInfo.Read(bs, BaseTextureSetPosition); + TextureInfos.Add(textureInfo); + textureInfo.BufferInfo = Buffers[(int)textureInfo.BufferId]; + } + } + } + + public void BuildTextureSetFile(string outputName) + { + using var ms = new FileStream(outputName, FileMode.Create); + using var bs = new BinaryStream(ms, ByteConverter.Big); + + if (LittleEndian) + bs.ByteConverter = ByteConverter.Little; + + WriteToStream(bs); + } + + /// + /// Writes a PSP (3SXT) texture set: single-texture (8888 / IDTEX4 / IDTEX8 / DXT1 / DXT5) or, when + /// the set holds more than one texture, a multi-texture container (non-paletted 8888 / DXT only). + /// + public void BuildPSPTextureSetFile(string outputName) + { + using var ms = new FileStream(outputName, FileMode.Create); + using var bs = new BinaryStream(ms, ByteConverter.Little); + if (TextureInfos.Count > 1) + BuildPSPMultiTextureSet(bs); + else + BuildPSPTextureSet(bs); + } + + /// + /// Writes a multi-texture PSP (3SXT) "texture set" file (e.g. env2.txs = 2x DXT1). Layout matches + /// PD: header, N texture infos, N GE command lists, N buffer infos, N names, then the image data + /// region (first image aligned to 0x80, the rest packed 0x10-aligned). Non-paletted only for now + /// (8888 / DXT1 / DXT5) - no multi-texture IDTEX/CLUT sets exist in GT PSP. + /// + private void BuildPSPMultiTextureSet(BinaryStream bs) + { + int n = TextureInfos.Count; + var texs = new PGLUGETextureInfo[n]; + var bufs = new GETextureBuffer[n]; + var fmts = new eSCE_GE_TPF[n]; + for (int i = 0; i < n; i++) + { + texs[i] = (PGLUGETextureInfo)TextureInfos[i]; + bufs[i] = (GETextureBuffer)texs[i].BufferInfo; + fmts[i] = bufs[i].FormatBits; + if (fmts[i] is eSCE_GE_TPF.SCE_GE_TPF_IDTEX4 or eSCE_GE_TPF.SCE_GE_TPF_IDTEX8) + throw new NotSupportedException("Multi-texture PSP sets with paletted (IDTEX) textures aren't supported yet (only 8888 / DXT)."); + if (fmts[i] is not (eSCE_GE_TPF.SCE_GE_TPF_8888 or eSCE_GE_TPF.SCE_GE_TPF_DXT1 or eSCE_GE_TPF.SCE_GE_TPF_DXT5)) + throw new NotSupportedException($"Unsupported multi-texture PSP format {fmts[i]}."); + } + + const int texOff = 0x40, texInfoSize = 0x98, bufInfoSize = 0x20; + const int cmdCount = 17; // non-paletted command list length + int cmdLen = cmdCount * 4; + int cmdStart = texOff + (n * texInfoSize); + int bufStart = cmdStart + (n * cmdLen); + int nameStart = bufStart + (n * bufInfoSize); + + // Names (concatenated, null-terminated), before the image data. + var nameBytes = new byte[n][]; + var nameOff = new int[n]; + int pos = nameStart; + for (int i = 0; i < n; i++) + { + nameOff[i] = pos; + nameBytes[i] = Encoding.ASCII.GetBytes(texs[i].Name ?? string.Empty); + pos += nameBytes[i].Length + 1; + } + + // Image data region: first image 0x80-aligned (PD's relocSize), the rest 0x10-aligned. + var imgOff = new int[n]; + int cur = (pos + 0x7F) & ~0x7F; + for (int i = 0; i < n; i++) + { + if (i > 0) + cur = (cur + 0xF) & ~0xF; + imgOff[i] = cur; + cur += bufs[i].ImageData.Length; + } + int relocSize = imgOff[0]; + int fileSize = (cur + 0x7F) & ~0x7F; // 128-aligned (PD; GPB packs textures on a 128 grid) + + // ── Header (0x40) ── + bs.WriteString("3SXT", StringCoding.Raw); + bs.WriteInt32(fileSize); + bs.WriteUInt32(0); // relocation pointer (0 = standalone) + bs.WriteInt32(0); + bs.WriteInt32(0); + bs.WriteInt16((short)n); // texture info count + bs.WriteInt16((short)n); // buffer info count + bs.WriteInt32(texOff); + bs.WriteInt32(bufStart); + bs.WriteInt32(relocSize); // @0x20 = start of image data region + bs.WriteInt16(0); + bs.WriteInt16(0); // clut map entry count (none) + bs.WriteInt32(0); + bs.WriteInt32(0); // clut map offset (none) + bs.WriteInt32(0); + bs.WriteInt32(0); + bs.WriteInt32(0); + bs.WriteInt32(0); + + // ── Texture infos (N x 0x98) ── + for (int i = 0; i < n; i++) + { + var buffer = bufs[i]; + eSCE_GE_TPF format = fmts[i]; + int width = buffer.Width, height = buffer.Height; + int mipWidth = texs[i].MipInfos[0].Width; + int texWidth = (int)BitOperations.RoundUpToPowerOf2((uint)width); // pow2(W), not stride (see BuildPSPTextureSet) + int paddedHeight = (int)BitOperations.RoundUpToPowerOf2((uint)height); + int log2W = BitOperations.Log2((uint)texWidth); + int log2H = BitOperations.Log2((uint)paddedHeight); + float uScale = (float)width / texWidth; + float vScale = (float)height / paddedHeight; + uint tmodeReg = 0xc2000000u | (uint)texs[i].TMODE.hsm; + uint tpfReg = 0xc3000100u | (byte)format; + + bs.WriteInt32(cmdStart + (i * cmdLen)); // subParamsOff + bs.WriteInt32(0); + bs.WriteSingle(uScale); + bs.WriteSingle(vScale); + bs.WriteUInt32(0x80000000); + bs.WriteUInt32(0x80000000); + bs.WriteUInt32(0xc0000100); + bs.WriteUInt32(0xc1000000); + bs.WriteUInt32(tmodeReg); + bs.WriteUInt32(tpfReg); + bs.WriteUInt32(0xc4000000); // CLOAD (no clut) + bs.WriteUInt32(0xc500ff00); // CLUT (no clut) + bs.WriteUInt32(0xc6000101); + bs.WriteUInt32(0xc7000101); + bs.WriteUInt32(0xc8000000); + bs.WriteUInt32(0xc9008100); + bs.WriteUInt32(0xca000000); + bs.WriteInt32(0); + bs.WriteUInt32(0); + bs.WriteUInt16((ushort)mipWidth); + bs.WriteByte((byte)log2W); + bs.WriteByte((byte)log2H); + for (int m = 1; m < 8; m++) { bs.WriteUInt32(0); bs.WriteUInt32(0); } + bs.WriteInt16(0); + bs.WriteInt16(-1); // clut map entry index (-1 = none) + bs.WriteInt16(0); + bs.WriteUInt16((ushort)i); // buffer id + bs.WriteUInt32(0); + bs.WriteInt32(nameOff[i]); + } + + // ── GE command lists (N x 17 u32) ── + for (int i = 0; i < n; i++) + { + eSCE_GE_TPF format = fmts[i]; + int width = bufs[i].Width, height = bufs[i].Height; + int mipWidth = texs[i].MipInfos[0].Width; + int texWidth = (int)BitOperations.RoundUpToPowerOf2((uint)width); // pow2(W), not stride (see BuildPSPTextureSet) + int paddedHeight = (int)BitOperations.RoundUpToPowerOf2((uint)height); + int log2W = BitOperations.Log2((uint)texWidth); + int log2H = BitOperations.Log2((uint)paddedHeight); + float uScale = (float)width / texWidth; + float vScale = (float)height / paddedHeight; + uint suRaw = 0x48000000u | (BitConverter.SingleToUInt32Bits(uScale) >> 8); + uint svRaw = 0x49000000u | (BitConverter.SingleToUInt32Bits(vScale) >> 8); + uint tmodeReg = 0xc2000000u | (uint)texs[i].TMODE.hsm; + uint tpfReg = 0xc3000100u | (byte)format; + + bs.WriteUInt32(0xa0000000); // TBP0 + bs.WriteUInt32(0xa8000000u | (uint)mipWidth); // TBW0 + bs.WriteUInt32(0xb8000000u | ((uint)log2H << 8) | (uint)log2W); // TSIZE0 + bs.WriteUInt32(suRaw); + bs.WriteUInt32(svRaw); + bs.WriteUInt32(0x4a800000); + bs.WriteUInt32(0x4b800000); + bs.WriteUInt32(0xc0000100); + bs.WriteUInt32(tmodeReg); + bs.WriteUInt32(tpfReg); + bs.WriteUInt32(0xc6000101); + bs.WriteUInt32(0xc7000101); + bs.WriteUInt32(0xc8000000); + bs.WriteUInt32(0xc9008100); + bs.WriteUInt32(0xca000000); + bs.WriteUInt32(0xcb000000); // TFLUSH + bs.WriteUInt32(0x0b000000); // RET + } + + // ── Buffer infos (N x 0x20) ── + for (int i = 0; i < n; i++) + { + bool isDxt = fmts[i] is eSCE_GE_TPF.SCE_GE_TPF_DXT1 or eSCE_GE_TPF.SCE_GE_TPF_DXT3 or eSCE_GE_TPF.SCE_GE_TPF_DXT5; + bs.WriteInt32(imgOff[i]); + bs.WriteInt32(bufs[i].ImageData.Length); + bs.WriteByte((byte)(isDxt ? 0 : 8)); + bs.WriteByte((byte)fmts[i]); + bs.WriteByte(1); // mip count + bs.WriteByte(0); // (env2 multi-texture buffers use 0 here, vs 1 for single) + bs.WriteUInt16((ushort)bufs[i].Width); + bs.WriteUInt16((ushort)bufs[i].Height); + bs.WriteUInt16(1); + bs.WriteUInt16(0); + bs.WriteInt32(0); + bs.WriteInt32(0); + bs.WriteInt32(0); // full buffer-info stride is 0x20 (multi-texture infos are contiguous) + } + + // ── Names ── + while (bs.Position < nameStart) + bs.WriteByte(0); + for (int i = 0; i < n; i++) + bs.WriteString(texs[i].Name ?? string.Empty, StringCoding.ZeroTerminated); + + // ── Image data ── + for (int i = 0; i < n; i++) + { + while (bs.Position < imgOff[i]) + bs.WriteByte(0); + bs.Write(bufs[i].ImageData.Span); + } + while (bs.Position < fileSize) + bs.WriteByte(0); + } + + private void BuildPSPTextureSet(BinaryStream bs) + { + if (TextureInfos.Count != 1) + throw new NotSupportedException("BuildPSPTextureSet is the single-texture path; multi-texture sets are built by BuildPSPMultiTextureSet."); + + var tex = (PGLUGETextureInfo)TextureInfos[0]; + var buffer = (GETextureBuffer)tex.BufferInfo; + eSCE_GE_TPF format = buffer.FormatBits; + + bool paletted = format is eSCE_GE_TPF.SCE_GE_TPF_IDTEX4 or eSCE_GE_TPF.SCE_GE_TPF_IDTEX8; + bool isDxt = format is eSCE_GE_TPF.SCE_GE_TPF_DXT1 or eSCE_GE_TPF.SCE_GE_TPF_DXT3 or eSCE_GE_TPF.SCE_GE_TPF_DXT5; + bool supportedDxt = format is eSCE_GE_TPF.SCE_GE_TPF_DXT1 or eSCE_GE_TPF.SCE_GE_TPF_DXT5; + if (format != eSCE_GE_TPF.SCE_GE_TPF_8888 && !paletted && !supportedDxt) + throw new NotSupportedException($"PSP building supports 8888 / IDTEX4 / IDTEX8 / DXT1 / DXT5 (got {format})."); + + GEClutBufferInfo clut = tex.ClutBufferInfo; + if (paletted && (clut is null || clut.ClutData is null)) + throw new InvalidDataException("Paletted PSP texture is missing its CLUT."); + + int width = buffer.Width; + int height = buffer.Height; + // The Create* helpers set MipInfos[0].Width to the buffer's row stride in texels (the 16-byte + // minimum can exceed the texture width for narrow IDTEX). TSIZE0/UMIN use the texture's own + // pow2 DIMENSION, not the stride - PD sets TSIZE0 = pow2(W) and TBW0 = stride separately; + // using the stride for TSIZE0 mis-sizes small IDTEX (e.g. 8px -> reported 32px) -> invisible. + int mipWidth = tex.MipInfos[0].Width; + int texWidth = (int)BitOperations.RoundUpToPowerOf2((uint)width); + int paddedHeight = (int)BitOperations.RoundUpToPowerOf2((uint)height); + int log2W = BitOperations.Log2((uint)texWidth); + int log2H = BitOperations.Log2((uint)paddedHeight); + byte[] image = buffer.ImageData.ToArray(); + + const int texOff = 0x40; + const int texInfoSize = 0x98; + int subParamsOff = texOff + texInfoSize; + int cmdCount = paletted ? 21 : 17; // paletted adds CBP, CBW, CLUT, CLOAD + int bufOff = subParamsOff + (cmdCount * 4); + const int bufInfoSize = 0x20; + int imgOff = (bufOff + bufInfoSize + 0xF) & ~0xF; + + byte[] clutData = paletted ? clut.ClutData : []; + int numColors = paletted ? clut.NumColors : 0; + + int clutInfoOff = 0, clutDataOff = 0, nameOff; + if (paletted) + { + clutInfoOff = imgOff + image.Length; + clutDataOff = (clutInfoOff + 0x0C + 0xF) & ~0xF; + nameOff = clutDataOff + clutData.Length; + } + else + { + nameOff = imgOff + image.Length; + } + + byte[] name = Encoding.ASCII.GetBytes(tex.Name ?? string.Empty); + // 3SXT fileSize is padded to 128 (every real PD texture is; the GPB packs each texture on a + // 128-byte grid). A 64-aligned size (e.g. tip_up at 0x240) leaves the texture non-128 in the + // GPB + trailing 0x5E, which breaks the game's loader for THIS and all following textures. + int fileSize = (nameOff + name.Length + 1 + 0x7F) & ~0x7F; + + float uScale = (float)width / texWidth; + float vScale = (float)height / paddedHeight; + uint suRaw = 0x48000000u | (BitConverter.SingleToUInt32Bits(uScale) >> 8); + uint svRaw = 0x49000000u | (BitConverter.SingleToUInt32Bits(vScale) >> 8); + + uint tmodeReg = 0xc2000000u | (uint)tex.TMODE.hsm; // TMODE: high-speed (swizzled) bit + uint tpfReg = 0xc3000100u | (byte)format; // TPF: 8888=3, IDTEX4=4, IDTEX8=5, DXT1=8 + uint cloadReg = 0xc4000000u | (uint)((numColors + 7) / 8); // CLOAD: 8-entry blocks, rounded up (PD uses the exact colour count) + uint clutReg = paletted ? 0xc500ff03u : 0xc500ff00u; // CLUT: cpf C8888 when paletted + + // ── Header (0x40) ── + bs.WriteString("3SXT", StringCoding.Raw); + bs.WriteInt32(fileSize); + bs.WriteUInt32(0); // relocation pointer (0 = standalone) + bs.WriteInt32(0); + bs.WriteInt32(0); + bs.WriteInt16(1); // texture info count + bs.WriteInt16(1); // buffer info count + bs.WriteInt32(texOff); + bs.WriteInt32(bufOff); + bs.WriteInt32(fileSize); // relocation size + bs.WriteInt16(0); + bs.WriteInt16((short)(paletted ? 1 : 0)); // clut entry count + bs.WriteInt32(0); + bs.WriteInt32(paletted ? clutInfoOff : 0); // clut map offset + bs.WriteInt32(0); + bs.WriteInt32(0); + bs.WriteInt32(0); + bs.WriteInt32(0); + + // ── Texture info (0x98) ── + bs.WriteInt32(subParamsOff); + bs.WriteInt32(0); + bs.WriteSingle(uScale); // UMIN (used width / buffer width) + bs.WriteSingle(vScale); // VMIN (used height / buffer height) + bs.WriteUInt32(0x80000000); // UMAX + bs.WriteUInt32(0x80000000); // VMAX + bs.WriteUInt32(0xc0000100); // TMAP + bs.WriteUInt32(0xc1000000); // TSHADE + bs.WriteUInt32(tmodeReg); // TMODE + bs.WriteUInt32(tpfReg); // TPF + bs.WriteUInt32(cloadReg); // CLOAD + bs.WriteUInt32(clutReg); // CLUT + bs.WriteUInt32(0xc6000101); // FILTER + bs.WriteUInt32(0xc7000101); // TWRAP + bs.WriteUInt32(0xc8000000); // LEVEL + bs.WriteUInt32(0xc9008100); // TFUNC + bs.WriteUInt32(0xca000000); // TEC + bs.WriteInt32(0); // runtime clut offset + bs.WriteUInt32(0); // mip0 relocation pointer + bs.WriteUInt16((ushort)mipWidth); + bs.WriteByte((byte)log2W); + bs.WriteByte((byte)log2H); + for (int i = 1; i < 8; i++) { bs.WriteUInt32(0); bs.WriteUInt32(0); } // unused mips + bs.WriteInt16(0); + bs.WriteInt16((short)(paletted ? 0 : -1)); // clut map entry index + bs.WriteInt16(0); + bs.WriteUInt16(0); // buffer id + bs.WriteUInt32(0); + bs.WriteInt32(nameOff); + + // ── GE command list (subParams) ── + if (paletted) + { + bs.WriteUInt32(0xb0000000); // CBP - clut address (relocated at runtime) + bs.WriteUInt32(0xb1000000); // CBW - clut address upper bits + } + bs.WriteUInt32(0xa0000000); // TBP0 - texture address (relocated at runtime) + bs.WriteUInt32(0xa8000000u | (uint)mipWidth); // TBW0 - buffer width + bs.WriteUInt32(0xb8000000u | ((uint)log2H << 8) | (uint)log2W); // TSIZE0 + bs.WriteUInt32(suRaw); // SU + bs.WriteUInt32(svRaw); // SV + bs.WriteUInt32(0x4a800000); // TU + bs.WriteUInt32(0x4b800000); // TV + bs.WriteUInt32(0xc0000100); // TMAP + bs.WriteUInt32(tmodeReg); // TMODE + bs.WriteUInt32(tpfReg); // TPF + bs.WriteUInt32(0xc6000101); // TFILTER + bs.WriteUInt32(0xc7000101); // TWRAP + bs.WriteUInt32(0xc8000000); // TLEVEL + bs.WriteUInt32(0xc9008100); // TFUNC + bs.WriteUInt32(0xca000000); // TEC + if (paletted) + { + bs.WriteUInt32(clutReg); // CLUT + bs.WriteUInt32(cloadReg); // CLOAD + } + bs.WriteUInt32(0xcb000000); // TFLUSH + bs.WriteUInt32(0x0b000000); // RET + + // ── Buffer info (0x20) ── + bs.WriteInt32(imgOff); + bs.WriteInt32(image.Length); + // byte8 = the swizzle flag: 8 when high-speed/swizzled, 0 when linear (narrow IDTEX) or DXT. + // (The old `isDxt ? 0 : 8` wrongly marked LINEAR IDTEX as swizzled.) + bs.WriteByte((byte)(tex.TMODE.hsm == SCE_GE_TMODE_HSM.SCE_GE_TMODE_HSM_HIGHSPEED ? 8 : 0)); + bs.WriteByte((byte)format); + bs.WriteByte(1); // mip count + bs.WriteByte(1); + bs.WriteUInt16((ushort)width); + bs.WriteUInt16((ushort)height); + bs.WriteUInt16(1); + bs.WriteUInt16(0); + bs.WriteInt32(0); + bs.WriteInt32(0); + + // ── Image data ── + while (bs.Position < imgOff) + bs.WriteByte(0); + bs.Write(image); + + // ── CLUT (info block + palette data) ── + if (paletted) + { + while (bs.Position < clutInfoOff) + bs.WriteByte(0); + bs.WriteByte(0); + bs.WriteByte((byte)clut.ClutType); + bs.WriteUInt16((ushort)numColors); + bs.WriteInt32(clutDataOff); + bs.WriteInt32(0); + + while (bs.Position < clutDataOff) + bs.WriteByte(0); + bs.Write(clutData); + } + + // ── Texture name ── + while (bs.Position < nameOff) + bs.WriteByte(0); + bs.WriteString(tex.Name ?? string.Empty, StringCoding.ZeroTerminated); + while (bs.Position < fileSize) + bs.WriteByte(0); + } + + /// + /// Writes a single-texture Tpp1 PSP container (8888 / IDTEX4 / IDTEX8). The texture's image + /// data must already be encoded at Tpp1's 16-byte-aligned stride (see PGLUGETextureInfo.CreateTpp1). + /// + public void BuildTpp1File(string outputName) + { + using var ms = new FileStream(outputName, FileMode.Create); + using var bs = new BinaryStream(ms, ByteConverter.Little); + BuildTpp1Stream(bs); + } + + private void BuildTpp1Stream(BinaryStream bs) + { + if (TextureInfos.Count != 1) + throw new NotSupportedException("Tpp1 building supports exactly one texture."); + + var tex = (PGLUGETextureInfo)TextureInfos[0]; + var buffer = (GETextureBuffer)tex.BufferInfo; + eSCE_GE_TPF format = buffer.FormatBits; + + bool paletted = format is eSCE_GE_TPF.SCE_GE_TPF_IDTEX4 or eSCE_GE_TPF.SCE_GE_TPF_IDTEX8; + if (format != eSCE_GE_TPF.SCE_GE_TPF_8888 && !paletted) + throw new NotSupportedException($"Tpp1 building supports 8888 / IDTEX4 / IDTEX8 (got {format})."); + + GEClutBufferInfo clut = tex.ClutBufferInfo; + if (paletted && (clut is null || clut.ClutData is null)) + throw new InvalidDataException("Paletted Tpp1 texture is missing its CLUT."); + + int width = buffer.Width; + int height = buffer.Height; + int stride = tex.MipInfos[0].Width; // row stride in texels (16-byte aligned) + int log2W = BitOperations.Log2(BitOperations.RoundUpToPowerOf2((uint)width)); + int log2H = BitOperations.Log2(BitOperations.RoundUpToPowerOf2((uint)height)); + byte[] image = buffer.ImageData.ToArray(); + byte[] clutData = paletted ? clut.ClutData : []; + int numColors = paletted ? clut.NumColors : 0; + + const int entryTableOffset = 0x30; + const int subParamsOffset = 0x38; + int cmdListLen = (paletted ? 10 : 6) * 4; // 0x28 / 0x18 + const int imageOffset = 0x80; // command list is always padded up to 0x80 + int clutOffset = imageOffset + image.Length; + int dataBytes = image.Length + clutData.Length; + int field10 = ((dataBytes + 0x1FFF) / 0x2000) << 16; // ceil(dataBytes / 0x2000) << 16 + int fileSize = clutOffset + clutData.Length; + + uint tmodeReg = 0xC2000000u | (uint)tex.TMODE.hsm; + + // ── Header (0x30) ── + bs.WriteString("Tpp1", StringCoding.Raw); + bs.WriteInt32(0); + bs.WriteInt32(0); + bs.WriteInt32(cmdListLen); // 0x0C: command list length + bs.WriteInt32(field10); // 0x10: ceil(data / 0x2000) << 16 + bs.WriteInt32(1); // 0x14: texture count + bs.WriteInt32(entryTableOffset); // 0x18 + bs.WriteInt32(0); // 0x1C + while (bs.Position < entryTableOffset) + bs.WriteByte(0); + + // ── Texture entry (0x30) ── + bs.WriteInt32(subParamsOffset); + bs.WriteUInt16((ushort)width); + bs.WriteUInt16((ushort)height); + + // ── GE command list (0x38) ── + bs.WriteUInt32(0xC3000000u | (byte)format); // TPF + bs.WriteUInt32(tmodeReg); // TMODE + bs.WriteUInt32(0xB8000000u | ((uint)log2H << 8) | (uint)log2W); // TSIZE0 + bs.WriteUInt32(0xA0000000u | (uint)imageOffset); // TBP0 + bs.WriteUInt32(0xA8000000u | (uint)stride); // TBW0 + if (paletted) + { + bs.WriteUInt32(0xB0000000u | (uint)clutOffset); // CBP + bs.WriteUInt32(0xB1000000u); // CBW + bs.WriteUInt32(0xC500FF03u); // CLUT (C8888) + bs.WriteUInt32(0xC4000000u | (uint)((numColors + 7) / 8)); // CLOAD (8-entry blocks, rounded up) + } + bs.WriteUInt32(0x0B000000u); // RET + + // ── Image data + CLUT ── + while (bs.Position < imageOffset) + bs.WriteByte(0); + bs.Write(image); + if (paletted) + bs.Write(clutData); + while (bs.Position < fileSize) + bs.WriteByte(0); + } + + /// + /// Reads a Tpp1 PSP texture. Tpp1 is a compact single-texture container used by GT PSP; the + /// payload is the same PSP GE texture data as a 3SXT texture set, just with a leaner header + /// (a GE command list whose TBP0/TBW0/CBP/CLOAD point at the image and CLUT data). + /// + public void FromTpp1Stream(Stream stream) + { + long basePos = stream.Position; + BaseTextureSetPosition = basePos; + LittleEndian = true; + + var bs = new BinaryStream(stream, ByteConverter.Little); + string magic = bs.ReadString(4); + if (magic != "Tpp1") + throw new InvalidDataException("Could not parse Tpp1 from stream, not a valid Tpp1 texture."); + + ContainerTag = "Tpp1"; + + long fileLength = stream.Length - basePos; + + bs.Position = basePos + 0x14; + int textureCount = bs.ReadInt32(); + int entryTableOffset = bs.ReadInt32(); + + for (int i = 0; i < textureCount; i++) + { + bs.Position = basePos + entryTableOffset + (i * 8); + int subParamsOffset = bs.ReadInt32(); + ushort width = bs.ReadUInt16(); + ushort height = bs.ReadUInt16(); + + // Parse the GE command list (raw little-endian u32s: command = high byte, arg = low 24 bits). + bs.Position = basePos + subParamsOffset; + eSCE_GE_TPF format = eSCE_GE_TPF.SCE_GE_TPF_8888; + var hsm = SCE_GE_TMODE_HSM.SCE_GE_TMODE_HSM_NORMAL; + var clutFormat = SCE_GE_CLUT_CPF.SCE_GE_CLUT_CPF_8888; + int imageOffset = 0, stride = 0, clutOffset = 0, clutBlocks = 0; + bool reachedEnd = false; + while (!reachedEnd) + { + uint value = bs.ReadUInt32(); + int command = (int)(value >> 24); + int arg = (int)(value & 0xFFFFFF); + switch (command) + { + case 0xC3: format = (eSCE_GE_TPF)(arg & 0xFF); break; // TPF (pixel format) + case 0xC2: hsm = (SCE_GE_TMODE_HSM)(arg & 1); break; // TMODE (swizzle flag) + case 0xA0: imageOffset = arg; break; // TBP0 (image data offset) + case 0xA8: stride = arg; break; // TBW0 (buffer width in texels) + case 0xB0: clutOffset = arg; break; // CBP (CLUT data offset) + case 0xC4: clutBlocks = arg & 0xFF; break; // CLOAD (CLUT block count) + case 0xC5: clutFormat = (SCE_GE_CLUT_CPF)(arg & 0x3); break; // CLUT (CLUT pixel format) + case 0x0B: reachedEnd = true; break; // RET + } + } + + bool paletted = format is eSCE_GE_TPF.SCE_GE_TPF_IDTEX4 or eSCE_GE_TPF.SCE_GE_TPF_IDTEX8; + int numColors = clutBlocks * 8; + int clutBytesPerColor = clutFormat == SCE_GE_CLUT_CPF.SCE_GE_CLUT_CPF_8888 ? 4 : 2; + + int imageEnd = (paletted && clutOffset != 0) ? clutOffset : (int)fileLength; + int imageSize = imageEnd - imageOffset; + + var texture = new PGLUGETextureInfo(); + texture.TPF.tpf = format; + texture.TMODE.hsm = hsm; + texture.MipInfos[0] = new GEMipInfo { Width = (ushort)stride }; + + bs.Position = basePos + imageOffset; + byte[] imageData = bs.ReadBytes(imageSize); + + var buffer = new GETextureBuffer + { + Width = width, + Height = height, + FormatBits = format, + LastMipmapLevel = 1, + ImageData = imageData, + ImageSize = (uint)imageSize, + }; + texture.BufferInfo = buffer; + + if (paletted && clutOffset != 0) + { + bs.Position = basePos + clutOffset; + byte[] clutData = bs.ReadBytes(numColors * clutBytesPerColor); + texture.ClutBufferInfo = new GEClutBufferInfo + { + ClutType = clutFormat, + NumColors = (ushort)numColors, + ClutData = clutData, + }; + } + + texture.BufferId = (uint)Buffers.Count; + TextureInfos.Add(texture); + Buffers.Add(buffer); + } + } + + /// + /// Dumps every texture in the set to an editable image beside , + /// tagging the filename with the container + pixel format (e.g. "cobra_67.3SXT.IDTEX8.png") so a + /// faithful rebuild is possible. Multi-texture sets dump into a "<name>.txs/" folder. + /// + /// Reference path; the dump name/extension are derived from it. + /// When true, PS3 (Cell) textures are written as real .dds (lossless for DXT, + /// the inverse of ); PSP 3SXT/Tpp1 have no DDS equivalent + /// and always dump as PNG. + public void ConvertToStandardFormat(string outputName, bool asDds = false) + { + Console.WriteLine($"Processing {outputName} with {TextureInfos.Count} texture(s)..."); + + for (int i = 0; i < TextureInfos.Count; i++) + { + PGLUTextureInfo texture = TextureInfos[i]; + + // DDS output only applies to PS3 (Cell) textures - PSP 3SXT/Tpp1 have no DDS equivalent, + // so they always dump as PNG even when DDS is requested. + bool useDds = asDds && texture is PGLUCellTextureInfo; + string ext = useDds ? ".dds" : ".png"; + + // Single-texture sets derive the base name from the OUTPUT path (i.e. the input file) + // so the flagged dump name is predictable for round-tripping. Multi-texture sets fall + // back to the embedded name / index to disambiguate. + string baseName = TextureInfos.Count == 1 ? Path.GetFileNameWithoutExtension(outputName) : + !string.IsNullOrEmpty(texture.Name) ? Path.GetFileNameWithoutExtension(texture.Name) : + $"{i}"; + + // Tag the file with the container + pixel format so a faithful rebuild is possible + // (e.g. "cobra_67.3SXT.IDTEX8.png"). Neither is recoverable from the image itself. + string flag = string.IsNullOrEmpty(ContainerTag) ? "" : $".{ContainerTag}.{texture.GetPixelFormatName()}"; + string fileName = $"{baseName}{flag}{ext}"; + + // Multi-texture sets dump into a ".txs/" folder (tagged so the rebuild knows it's a + // multi-texture container, and so the config _path points at the set, not a single image). + string texturePath = TextureInfos.Count > 1 + ? Path.Combine(Path.GetDirectoryName(outputName), Path.GetFileNameWithoutExtension(outputName) + ".txs", fileName) + : Path.Combine(Path.GetDirectoryName(outputName), fileName); + + Console.WriteLine($"- Converting '{texturePath}'..."); + + Directory.CreateDirectory(Path.GetDirectoryName(texturePath)); + if (useDds) + { + // Write the texture as a real .dds (lossless for DXT - no BC re-encode on rebuild). + File.WriteAllBytes(texturePath, ((PGLUCellTextureInfo)texture).GetDDS()); + } + else + { + using var img = texture.GetAsImage(); + img.Save(texturePath); + } + } + } + + public void AddTexture(PGLUTextureInfo texture) + { + ArgumentNullException.ThrowIfNull(texture, nameof(texture)); + ArgumentNullException.ThrowIfNull(texture.BufferInfo, nameof(texture.BufferInfo)); + + TextureInfos.Add(texture); + Buffers.Add(texture.BufferInfo); + + texture.BufferId = (uint)Buffers.Count - 1; + + } + + /// + /// Writes the texture set to a stream. + /// + /// Stream to write to. + /// Base position for the texture set + /// Whether to write the image data. If not, writing the image data and relinking offsets/finishing up the TXS3 header should be done at your own discretion. + public void WriteToStream(BinaryStream bs, int txsBasePos = 0, bool writeImageData = true) + { + BaseTextureSetPosition = txsBasePos; + + BuildPS3TextureSet(bs, txsBasePos, writeImageData); + } + + private void BuildPS3TextureSet(BinaryStream bs, int txsBasePos, bool writeImageData = true) + { + if (!LittleEndian) + bs.WriteString(MAGIC, StringCoding.Raw); + else + bs.WriteString(MAGIC_LE); + + bs.Position = txsBasePos + 0x14; + bs.WriteInt16((short)Buffers.Count); // Image Params Count; + bs.WriteInt16((short)TextureInfos.Count); // Image Info Count; + bs.WriteInt32(txsBasePos + 0x40); // PGLTexture Offset (render params) + + int imageInfoOffset = txsBasePos + 0x40 + (0x44 * TextureInfos.Count); + bs.WriteInt32(imageInfoOffset); + + // DataPointer (@0x20): 0 in every real PD gpb2 texture. The standalone reader keys off the + // buffer's ImageOffset directly, so this is metadata - but match PD (was 0x100). + bs.WriteInt32(0); + + // Write textures's render params + bs.Position = txsBasePos + 0x40; + foreach (var textureInfo in TextureInfos) + textureInfo.Write(bs); + + // Skip the texture info for now + bs.Position = imageInfoOffset + (Buffers.Count * 0x20); + + int mainHeaderSize = (int)bs.Position; + + // Write texture names + int lastNamePos = (int)bs.Position; + for (int i = 0; i < TextureInfos.Count; i++) + { + PGLUTextureInfo texture = TextureInfos[i]; + if (WriteNames && !string.IsNullOrEmpty(texture.Name)) + { + bs.WriteString(texture.Name, StringCoding.ZeroTerminated); + + // Update name offset + bs.Position = txsBasePos + 0x40 + (i * 0x44); + bs.Position += 0x40; // Skip to name offset field + bs.WriteInt32(lastNamePos); + } + + bs.Position = lastNamePos; + } + + int endPos = (int)bs.Position; + if (writeImageData) + { + bs.Align(0x80, grow: true); + endPos = (int)bs.Position; + } + + // Actually write the textures now and their linked information + int lastImageEnd = (int)bs.Position; + for (int i = 0; i < TextureInfos.Count; i++) + { + int imageOffset = 0, endImageOffset = 0; + if (writeImageData) + { + imageOffset = (int)bs.Position; + bs.Write(Buffers[i].ImageData.Span); + endImageOffset = (int)bs.Position; + lastImageEnd = endImageOffset; + } + + bs.Position = txsBasePos + imageInfoOffset + (i * 0x20); + + var textureInfo = TextureInfos[i] as PGLUCellTextureInfo; + + // Match PD's buffer-info constants (the bytes around the format/mip). Originals use: + // @+8 = 0 for uncompressed (A8R8G8B8/D8R8G8B8), 2 for compressed (DXT1/23/45) + // @+10 = mip level COUNT (1 for single-mip) @+11 = 2 + // The old (2, fmt, mipLast=0, 1) layout differs from every real PD texture. + CELL_GCM_TEXTURE_FORMAT baseFmt = textureInfo.FormatBits & ~CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_LN; + bool compressed = baseFmt == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT1 + || baseFmt == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT23 + || baseFmt == CELL_GCM_TEXTURE_FORMAT.CELL_GCM_TEXTURE_COMPRESSED_DXT45; + + bs.WriteInt32(imageOffset); + bs.WriteInt32(endImageOffset - imageOffset); // Size + bs.WriteByte((byte)(compressed ? 2 : 0)); + bs.WriteByte((byte)textureInfo.FormatBits); + bs.WriteByte((byte)(textureInfo.MipmapLevelLast)); + bs.WriteByte(2); + bs.WriteUInt16(textureInfo.Width); + bs.WriteUInt16(textureInfo.Height); + bs.WriteUInt16(1); + bs.WriteUInt16(0); + bs.Position += 12; // Pad + } + + // PD appends a 3-byte zero trailer after the last image, so fileSize = imageEnd + 3 and + // the field at @0xC = imageEnd + 2 (NOT the small main-header size). Reproduce that exactly + // for standalone files - every real PD gpb2 texture has this and a wrong @0xC value is a + // likely loader crash. + if (writeImageData && txsBasePos == 0) + { + bs.Position = lastImageEnd; + bs.WriteByte(0); + bs.WriteByte(0); + bs.WriteByte(0); + } + + // Finish up main header + bs.Position = txsBasePos + 4; + if (txsBasePos != 0) + { + bs.WriteInt32(0); + bs.WriteInt32(txsBasePos); + bs.WriteInt32(0); + } + else + { + bs.WriteInt32((int)bs.Length); + bs.WriteInt32(0); + bs.WriteInt32(writeImageData ? lastImageEnd + 2 : mainHeaderSize); + } + + bs.Position = endPos; + } + + public byte[] GetExternalImageDataOfTexture(Stream stream, PGLUTextureInfo texture, long basePos = 0) + { + stream.Position = basePos + (texture.BufferInfo.ImageOffset - DataPointer); + + var bytes = stream.ReadBytes((int)texture.BufferInfo.ImageSize); + var ms = new MemoryStream(); + (texture as PGLUCellTextureInfo).CreateDDSData(bytes, ms); + + return ms.ToArray(); + } + + public void FromFile(string file, TextureConsoleType consoleType = TextureConsoleType.PS3) + { + using var fs = new FileStream(file, FileMode.Open); + FromStream(fs, consoleType); + } + + + public enum TextureConsoleType + { + PSP, + PS3, + PS4, + }; } \ No newline at end of file diff --git a/PDTools.Files/bin/Debug/net9.0/PDTools.Files.deps.json b/PDTools.Files/bin/Debug/net9.0/PDTools.Files.deps.json new file mode 100644 index 00000000..ef148e43 --- /dev/null +++ b/PDTools.Files/bin/Debug/net9.0/PDTools.Files.deps.json @@ -0,0 +1,261 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v9.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v9.0": { + "PDTools.Files/1.0.0": { + "dependencies": { + "BCnEncoder.Net": "2.1.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.1", + "PDTools.Hashing": "1.0.0", + "PDTools.Utils": "1.0.0", + "Pfim": "0.11.3", + "SharpZipLib": "1.4.2", + "SixLabors.ImageSharp": "3.1.6", + "SixLabors.ImageSharp.Drawing": "2.1.5", + "Syroot.BinaryData": "5.2.2", + "Syroot.BinaryData.Memory": "5.2.2" + }, + "runtime": { + "PDTools.Files.dll": {} + } + }, + "BCnEncoder.Net/2.1.0": { + "dependencies": { + "Microsoft.Toolkit.HighPerformance": "7.0.2" + }, + "runtime": { + "lib/netstandard2.1/BCnEncoder.dll": { + "assemblyVersion": "2.1.0.0", + "fileVersion": "2.1.0.0" + } + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": { + "runtime": { + "lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.124.61010" + } + } + }, + "Microsoft.Extensions.Logging.Abstractions/9.0.1": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1" + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Logging.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.124.61010" + } + } + }, + "Microsoft.Toolkit.HighPerformance/7.0.2": { + "runtime": { + "lib/net5.0/Microsoft.Toolkit.HighPerformance.dll": { + "assemblyVersion": "7.0.0.0", + "fileVersion": "7.0.2.1" + } + } + }, + "Pfim/0.11.3": { + "runtime": { + "lib/netstandard2.0/Pfim.dll": { + "assemblyVersion": "0.11.3.0", + "fileVersion": "0.11.3.0" + } + } + }, + "SharpZipLib/1.4.2": { + "runtime": { + "lib/net6.0/ICSharpCode.SharpZipLib.dll": { + "assemblyVersion": "1.4.2.13", + "fileVersion": "1.4.2.13" + } + } + }, + "SixLabors.Fonts/2.0.8": { + "runtime": { + "lib/net6.0/SixLabors.Fonts.dll": { + "assemblyVersion": "2.0.0.0", + "fileVersion": "2.0.8.0" + } + } + }, + "SixLabors.ImageSharp/3.1.6": { + "runtime": { + "lib/net6.0/SixLabors.ImageSharp.dll": { + "assemblyVersion": "3.0.0.0", + "fileVersion": "3.1.6.0" + } + } + }, + "SixLabors.ImageSharp.Drawing/2.1.5": { + "dependencies": { + "SixLabors.Fonts": "2.0.8", + "SixLabors.ImageSharp": "3.1.6" + }, + "runtime": { + "lib/net6.0/SixLabors.ImageSharp.Drawing.dll": { + "assemblyVersion": "2.0.0.0", + "fileVersion": "2.1.5.0" + } + } + }, + "Syroot.BinaryData/5.2.2": { + "dependencies": { + "Syroot.BinaryData.Core": "5.2.0" + }, + "runtime": { + "lib/netstandard2.0/Syroot.BinaryData.dll": { + "assemblyVersion": "5.2.2.0", + "fileVersion": "5.2.2.0" + } + } + }, + "Syroot.BinaryData.Core/5.2.0": { + "runtime": { + "lib/netcoreapp2.1/Syroot.BinaryData.Core.dll": { + "assemblyVersion": "5.2.0.0", + "fileVersion": "5.2.0.0" + } + } + }, + "Syroot.BinaryData.Memory/5.2.2": { + "dependencies": { + "Syroot.BinaryData.Core": "5.2.0" + }, + "runtime": { + "lib/netcoreapp2.1/Syroot.BinaryData.Memory.dll": { + "assemblyVersion": "5.2.2.0", + "fileVersion": "5.2.2.0" + } + } + }, + "PDTools.Hashing/1.0.0": { + "runtime": { + "PDTools.Hashing.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" + } + } + }, + "PDTools.Utils/1.0.0": { + "dependencies": { + "Syroot.BinaryData": "5.2.2", + "Syroot.BinaryData.Memory": "5.2.2" + }, + "runtime": { + "PDTools.Utils.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" + } + } + } + } + }, + "libraries": { + "PDTools.Files/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "BCnEncoder.Net/2.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-bBKYUDhpYenmp8vy5BVUHmS2LFym0NMga9V+S1eMJ+4zQLBwDw63Bs83AKOyqBKC2AqqgxXdTFA2a2V+SmGoIw==", + "path": "bcnencoder.net/2.1.0", + "hashPath": "bcnencoder.net.2.1.0.nupkg.sha512" + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Tr74eP0oQ3AyC24ch17N8PuEkrPbD0JqIfENCYqmgKYNOmL8wQKzLJu3ObxTUDrjnn4rHoR1qKa37/eQyHmCDA==", + "path": "microsoft.extensions.dependencyinjection.abstractions/9.0.1", + "hashPath": "microsoft.extensions.dependencyinjection.abstractions.9.0.1.nupkg.sha512" + }, + "Microsoft.Extensions.Logging.Abstractions/9.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-w2gUqXN/jNIuvqYwX3lbXagsizVNXYyt6LlF57+tMve4JYCEgCMMAjRce6uKcDASJgpMbErRT1PfHy2OhbkqEA==", + "path": "microsoft.extensions.logging.abstractions/9.0.1", + "hashPath": "microsoft.extensions.logging.abstractions.9.0.1.nupkg.sha512" + }, + "Microsoft.Toolkit.HighPerformance/7.0.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-tG6v0AhDw/yJMtiH/tI4F6I/+sNIfu4++6WFfN73FyNcPh31et33w45wo8Lx1sfxcfLtkYk+w2fvU/MubYg4VQ==", + "path": "microsoft.toolkit.highperformance/7.0.2", + "hashPath": "microsoft.toolkit.highperformance.7.0.2.nupkg.sha512" + }, + "Pfim/0.11.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-UNVStuGHVIGyBlQaLX8VY6KpzZm/pG2zpV8ewNSXNFKFVPn8dLQKJITfps3lwUMzwTL+Do7RrMUvgQ1ZsPTu4w==", + "path": "pfim/0.11.3", + "hashPath": "pfim.0.11.3.nupkg.sha512" + }, + "SharpZipLib/1.4.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==", + "path": "sharpziplib/1.4.2", + "hashPath": "sharpziplib.1.4.2.nupkg.sha512" + }, + "SixLabors.Fonts/2.0.8": { + "type": "package", + "serviceable": true, + "sha512": "sha512-psTLKJVKaD8PKm+Bz/mSSNo4m0VCTYoJ97TF/ynxEWkn4sFUipI0rzE2V/KBtfU0TOn/gF/+1XBOEvusGZZMhA==", + "path": "sixlabors.fonts/2.0.8", + "hashPath": "sixlabors.fonts.2.0.8.nupkg.sha512" + }, + "SixLabors.ImageSharp/3.1.6": { + "type": "package", + "serviceable": true, + "sha512": "sha512-dHQ5jugF9v+5/LCVHCWVzaaIL6WOehqJy6eju/0VFYFPEj2WtqkGPoEV9EVQP83dHsdoqYaTuWpZdwAd37UwfA==", + "path": "sixlabors.imagesharp/3.1.6", + "hashPath": "sixlabors.imagesharp.3.1.6.nupkg.sha512" + }, + "SixLabors.ImageSharp.Drawing/2.1.5": { + "type": "package", + "serviceable": true, + "sha512": "sha512-cER1JfvshYDmTxw+gUy/x5e1xoNWhrD6s3AFf8rRUx9hWSXYdOFreQfUrM/QooMj0rF7+hkVtvGnV3EdMx4dxA==", + "path": "sixlabors.imagesharp.drawing/2.1.5", + "hashPath": "sixlabors.imagesharp.drawing.2.1.5.nupkg.sha512" + }, + "Syroot.BinaryData/5.2.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-3E4l6a20cnsgywGdurRfrtv3+au02AsKb3yEmcsskgJrJsRv2UaD9MaSFOJLloAjP5ZI8Tjn6YSWF3k5pfdrkg==", + "path": "syroot.binarydata/5.2.2", + "hashPath": "syroot.binarydata.5.2.2.nupkg.sha512" + }, + "Syroot.BinaryData.Core/5.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-zMrTKIa+zwvFqkahbDdCzGW0Jq7BVE44gzuK0OT0ia+DpRtZTcwqOzSMiLs7Z2urY9vMqh5yTMLGVUal0dkQAA==", + "path": "syroot.binarydata.core/5.2.0", + "hashPath": "syroot.binarydata.core.5.2.0.nupkg.sha512" + }, + "Syroot.BinaryData.Memory/5.2.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-SZhuJbqrXrECebYwWIRfuvWAlw0wcEvpM98ZDdMl0JSwgocvzGqFC7lP9j8oeeHjmNxOJ+cxNH3wVkGNwf0Q9A==", + "path": "syroot.binarydata.memory/5.2.2", + "hashPath": "syroot.binarydata.memory.5.2.2.nupkg.sha512" + }, + "PDTools.Hashing/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "PDTools.Utils/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/PDTools.Files/bin/Debug/net9.0/PDTools.Files.dll b/PDTools.Files/bin/Debug/net9.0/PDTools.Files.dll new file mode 100644 index 00000000..4aaf9b74 Binary files /dev/null and b/PDTools.Files/bin/Debug/net9.0/PDTools.Files.dll differ diff --git a/PDTools.Files/bin/Debug/net9.0/PDTools.Files.pdb b/PDTools.Files/bin/Debug/net9.0/PDTools.Files.pdb new file mode 100644 index 00000000..599d3c81 Binary files /dev/null and b/PDTools.Files/bin/Debug/net9.0/PDTools.Files.pdb differ diff --git a/PDTools.Files/bin/Debug/net9.0/PDTools.Hashing.dll b/PDTools.Files/bin/Debug/net9.0/PDTools.Hashing.dll new file mode 100644 index 00000000..a7018544 Binary files /dev/null and b/PDTools.Files/bin/Debug/net9.0/PDTools.Hashing.dll differ diff --git a/PDTools.Files/bin/Debug/net9.0/PDTools.Hashing.pdb b/PDTools.Files/bin/Debug/net9.0/PDTools.Hashing.pdb new file mode 100644 index 00000000..286334e0 Binary files /dev/null and b/PDTools.Files/bin/Debug/net9.0/PDTools.Hashing.pdb differ diff --git a/PDTools.Files/bin/Debug/net9.0/PDTools.Utils.dll b/PDTools.Files/bin/Debug/net9.0/PDTools.Utils.dll new file mode 100644 index 00000000..009f6dfa Binary files /dev/null and b/PDTools.Files/bin/Debug/net9.0/PDTools.Utils.dll differ diff --git a/PDTools.Files/bin/Debug/net9.0/PDTools.Utils.pdb b/PDTools.Files/bin/Debug/net9.0/PDTools.Utils.pdb new file mode 100644 index 00000000..7ff6622b Binary files /dev/null and b/PDTools.Files/bin/Debug/net9.0/PDTools.Utils.pdb differ diff --git a/PDTools.Files/bin/Release/net9.0/PDTools.Files.deps.json b/PDTools.Files/bin/Release/net9.0/PDTools.Files.deps.json new file mode 100644 index 00000000..ef148e43 --- /dev/null +++ b/PDTools.Files/bin/Release/net9.0/PDTools.Files.deps.json @@ -0,0 +1,261 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v9.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v9.0": { + "PDTools.Files/1.0.0": { + "dependencies": { + "BCnEncoder.Net": "2.1.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.1", + "PDTools.Hashing": "1.0.0", + "PDTools.Utils": "1.0.0", + "Pfim": "0.11.3", + "SharpZipLib": "1.4.2", + "SixLabors.ImageSharp": "3.1.6", + "SixLabors.ImageSharp.Drawing": "2.1.5", + "Syroot.BinaryData": "5.2.2", + "Syroot.BinaryData.Memory": "5.2.2" + }, + "runtime": { + "PDTools.Files.dll": {} + } + }, + "BCnEncoder.Net/2.1.0": { + "dependencies": { + "Microsoft.Toolkit.HighPerformance": "7.0.2" + }, + "runtime": { + "lib/netstandard2.1/BCnEncoder.dll": { + "assemblyVersion": "2.1.0.0", + "fileVersion": "2.1.0.0" + } + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": { + "runtime": { + "lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.124.61010" + } + } + }, + "Microsoft.Extensions.Logging.Abstractions/9.0.1": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1" + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Logging.Abstractions.dll": { + "assemblyVersion": "9.0.0.0", + "fileVersion": "9.0.124.61010" + } + } + }, + "Microsoft.Toolkit.HighPerformance/7.0.2": { + "runtime": { + "lib/net5.0/Microsoft.Toolkit.HighPerformance.dll": { + "assemblyVersion": "7.0.0.0", + "fileVersion": "7.0.2.1" + } + } + }, + "Pfim/0.11.3": { + "runtime": { + "lib/netstandard2.0/Pfim.dll": { + "assemblyVersion": "0.11.3.0", + "fileVersion": "0.11.3.0" + } + } + }, + "SharpZipLib/1.4.2": { + "runtime": { + "lib/net6.0/ICSharpCode.SharpZipLib.dll": { + "assemblyVersion": "1.4.2.13", + "fileVersion": "1.4.2.13" + } + } + }, + "SixLabors.Fonts/2.0.8": { + "runtime": { + "lib/net6.0/SixLabors.Fonts.dll": { + "assemblyVersion": "2.0.0.0", + "fileVersion": "2.0.8.0" + } + } + }, + "SixLabors.ImageSharp/3.1.6": { + "runtime": { + "lib/net6.0/SixLabors.ImageSharp.dll": { + "assemblyVersion": "3.0.0.0", + "fileVersion": "3.1.6.0" + } + } + }, + "SixLabors.ImageSharp.Drawing/2.1.5": { + "dependencies": { + "SixLabors.Fonts": "2.0.8", + "SixLabors.ImageSharp": "3.1.6" + }, + "runtime": { + "lib/net6.0/SixLabors.ImageSharp.Drawing.dll": { + "assemblyVersion": "2.0.0.0", + "fileVersion": "2.1.5.0" + } + } + }, + "Syroot.BinaryData/5.2.2": { + "dependencies": { + "Syroot.BinaryData.Core": "5.2.0" + }, + "runtime": { + "lib/netstandard2.0/Syroot.BinaryData.dll": { + "assemblyVersion": "5.2.2.0", + "fileVersion": "5.2.2.0" + } + } + }, + "Syroot.BinaryData.Core/5.2.0": { + "runtime": { + "lib/netcoreapp2.1/Syroot.BinaryData.Core.dll": { + "assemblyVersion": "5.2.0.0", + "fileVersion": "5.2.0.0" + } + } + }, + "Syroot.BinaryData.Memory/5.2.2": { + "dependencies": { + "Syroot.BinaryData.Core": "5.2.0" + }, + "runtime": { + "lib/netcoreapp2.1/Syroot.BinaryData.Memory.dll": { + "assemblyVersion": "5.2.2.0", + "fileVersion": "5.2.2.0" + } + } + }, + "PDTools.Hashing/1.0.0": { + "runtime": { + "PDTools.Hashing.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" + } + } + }, + "PDTools.Utils/1.0.0": { + "dependencies": { + "Syroot.BinaryData": "5.2.2", + "Syroot.BinaryData.Memory": "5.2.2" + }, + "runtime": { + "PDTools.Utils.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" + } + } + } + } + }, + "libraries": { + "PDTools.Files/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "BCnEncoder.Net/2.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-bBKYUDhpYenmp8vy5BVUHmS2LFym0NMga9V+S1eMJ+4zQLBwDw63Bs83AKOyqBKC2AqqgxXdTFA2a2V+SmGoIw==", + "path": "bcnencoder.net/2.1.0", + "hashPath": "bcnencoder.net.2.1.0.nupkg.sha512" + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Tr74eP0oQ3AyC24ch17N8PuEkrPbD0JqIfENCYqmgKYNOmL8wQKzLJu3ObxTUDrjnn4rHoR1qKa37/eQyHmCDA==", + "path": "microsoft.extensions.dependencyinjection.abstractions/9.0.1", + "hashPath": "microsoft.extensions.dependencyinjection.abstractions.9.0.1.nupkg.sha512" + }, + "Microsoft.Extensions.Logging.Abstractions/9.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-w2gUqXN/jNIuvqYwX3lbXagsizVNXYyt6LlF57+tMve4JYCEgCMMAjRce6uKcDASJgpMbErRT1PfHy2OhbkqEA==", + "path": "microsoft.extensions.logging.abstractions/9.0.1", + "hashPath": "microsoft.extensions.logging.abstractions.9.0.1.nupkg.sha512" + }, + "Microsoft.Toolkit.HighPerformance/7.0.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-tG6v0AhDw/yJMtiH/tI4F6I/+sNIfu4++6WFfN73FyNcPh31et33w45wo8Lx1sfxcfLtkYk+w2fvU/MubYg4VQ==", + "path": "microsoft.toolkit.highperformance/7.0.2", + "hashPath": "microsoft.toolkit.highperformance.7.0.2.nupkg.sha512" + }, + "Pfim/0.11.3": { + "type": "package", + "serviceable": true, + "sha512": "sha512-UNVStuGHVIGyBlQaLX8VY6KpzZm/pG2zpV8ewNSXNFKFVPn8dLQKJITfps3lwUMzwTL+Do7RrMUvgQ1ZsPTu4w==", + "path": "pfim/0.11.3", + "hashPath": "pfim.0.11.3.nupkg.sha512" + }, + "SharpZipLib/1.4.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==", + "path": "sharpziplib/1.4.2", + "hashPath": "sharpziplib.1.4.2.nupkg.sha512" + }, + "SixLabors.Fonts/2.0.8": { + "type": "package", + "serviceable": true, + "sha512": "sha512-psTLKJVKaD8PKm+Bz/mSSNo4m0VCTYoJ97TF/ynxEWkn4sFUipI0rzE2V/KBtfU0TOn/gF/+1XBOEvusGZZMhA==", + "path": "sixlabors.fonts/2.0.8", + "hashPath": "sixlabors.fonts.2.0.8.nupkg.sha512" + }, + "SixLabors.ImageSharp/3.1.6": { + "type": "package", + "serviceable": true, + "sha512": "sha512-dHQ5jugF9v+5/LCVHCWVzaaIL6WOehqJy6eju/0VFYFPEj2WtqkGPoEV9EVQP83dHsdoqYaTuWpZdwAd37UwfA==", + "path": "sixlabors.imagesharp/3.1.6", + "hashPath": "sixlabors.imagesharp.3.1.6.nupkg.sha512" + }, + "SixLabors.ImageSharp.Drawing/2.1.5": { + "type": "package", + "serviceable": true, + "sha512": "sha512-cER1JfvshYDmTxw+gUy/x5e1xoNWhrD6s3AFf8rRUx9hWSXYdOFreQfUrM/QooMj0rF7+hkVtvGnV3EdMx4dxA==", + "path": "sixlabors.imagesharp.drawing/2.1.5", + "hashPath": "sixlabors.imagesharp.drawing.2.1.5.nupkg.sha512" + }, + "Syroot.BinaryData/5.2.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-3E4l6a20cnsgywGdurRfrtv3+au02AsKb3yEmcsskgJrJsRv2UaD9MaSFOJLloAjP5ZI8Tjn6YSWF3k5pfdrkg==", + "path": "syroot.binarydata/5.2.2", + "hashPath": "syroot.binarydata.5.2.2.nupkg.sha512" + }, + "Syroot.BinaryData.Core/5.2.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-zMrTKIa+zwvFqkahbDdCzGW0Jq7BVE44gzuK0OT0ia+DpRtZTcwqOzSMiLs7Z2urY9vMqh5yTMLGVUal0dkQAA==", + "path": "syroot.binarydata.core/5.2.0", + "hashPath": "syroot.binarydata.core.5.2.0.nupkg.sha512" + }, + "Syroot.BinaryData.Memory/5.2.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-SZhuJbqrXrECebYwWIRfuvWAlw0wcEvpM98ZDdMl0JSwgocvzGqFC7lP9j8oeeHjmNxOJ+cxNH3wVkGNwf0Q9A==", + "path": "syroot.binarydata.memory/5.2.2", + "hashPath": "syroot.binarydata.memory.5.2.2.nupkg.sha512" + }, + "PDTools.Hashing/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "PDTools.Utils/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/PDTools.Files/bin/Release/net9.0/PDTools.Files.dll b/PDTools.Files/bin/Release/net9.0/PDTools.Files.dll new file mode 100644 index 00000000..bc7a4c4a Binary files /dev/null and b/PDTools.Files/bin/Release/net9.0/PDTools.Files.dll differ diff --git a/PDTools.Files/bin/Release/net9.0/PDTools.Files.pdb b/PDTools.Files/bin/Release/net9.0/PDTools.Files.pdb new file mode 100644 index 00000000..d9f8c321 Binary files /dev/null and b/PDTools.Files/bin/Release/net9.0/PDTools.Files.pdb differ diff --git a/PDTools.Files/bin/Release/net9.0/PDTools.Hashing.dll b/PDTools.Files/bin/Release/net9.0/PDTools.Hashing.dll new file mode 100644 index 00000000..afc02412 Binary files /dev/null and b/PDTools.Files/bin/Release/net9.0/PDTools.Hashing.dll differ diff --git a/PDTools.Files/bin/Release/net9.0/PDTools.Hashing.pdb b/PDTools.Files/bin/Release/net9.0/PDTools.Hashing.pdb new file mode 100644 index 00000000..64acadf7 Binary files /dev/null and b/PDTools.Files/bin/Release/net9.0/PDTools.Hashing.pdb differ diff --git a/PDTools.Files/bin/Release/net9.0/PDTools.Utils.dll b/PDTools.Files/bin/Release/net9.0/PDTools.Utils.dll new file mode 100644 index 00000000..c7fab3f8 Binary files /dev/null and b/PDTools.Files/bin/Release/net9.0/PDTools.Utils.dll differ diff --git a/PDTools.Files/bin/Release/net9.0/PDTools.Utils.pdb b/PDTools.Files/bin/Release/net9.0/PDTools.Utils.pdb new file mode 100644 index 00000000..c70d2e66 Binary files /dev/null and b/PDTools.Files/bin/Release/net9.0/PDTools.Utils.pdb differ diff --git a/PDTools.Files/obj/Debug/net8.0/PDTools.Files.assets.cache b/PDTools.Files/obj/Debug/net8.0/PDTools.Files.assets.cache new file mode 100644 index 00000000..eb465979 Binary files /dev/null and b/PDTools.Files/obj/Debug/net8.0/PDTools.Files.assets.cache differ diff --git a/PDTools.Files/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs b/PDTools.Files/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs new file mode 100644 index 00000000..ea1f2eae --- /dev/null +++ b/PDTools.Files/obj/Debug/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0")] diff --git a/PDTools.Files/obj/Debug/net9.0/PDTools..56880567.Up2Date b/PDTools.Files/obj/Debug/net9.0/PDTools..56880567.Up2Date new file mode 100644 index 00000000..e69de29b diff --git a/PDTools.Files/obj/Debug/net9.0/PDTools.Files.AssemblyInfo.cs b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.AssemblyInfo.cs new file mode 100644 index 00000000..e842cfe7 --- /dev/null +++ b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("PDTools.Files")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("PDTools.Files")] +[assembly: System.Reflection.AssemblyTitleAttribute("PDTools.Files")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Generated by the MSBuild WriteCodeFragment class. + diff --git a/PDTools.Files/obj/Debug/net9.0/PDTools.Files.AssemblyInfoInputs.cache b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.AssemblyInfoInputs.cache new file mode 100644 index 00000000..b4b83123 --- /dev/null +++ b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +3ff4dd46b05af0ef62af7f20eadd21143361817e26a902607f70235d35ef7f4e diff --git a/PDTools.Files/obj/Debug/net9.0/PDTools.Files.GeneratedMSBuildEditorConfig.editorconfig b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 00000000..7e08071d --- /dev/null +++ b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,18 @@ +is_global = true +build_property.TargetFramework = net9.0 +build_property.TargetFrameworkIdentifier = .NETCoreApp +build_property.TargetFrameworkVersion = v9.0 +build_property.TargetPlatformMinVersion = +build_property.UsingMicrosoftNETSdkWeb = +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property.EnforceExtendedAnalyzerRules = +build_property.EntryPointFilePath = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = PDTools.Files +build_property.ProjectDir = C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.EffectiveAnalysisLevelStyle = 9.0 +build_property.EnableCodeStyleSeverity = diff --git a/PDTools.Files/obj/Debug/net9.0/PDTools.Files.assets.cache b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.assets.cache new file mode 100644 index 00000000..5d7f523e Binary files /dev/null and b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.assets.cache differ diff --git a/PDTools.Files/obj/Debug/net9.0/PDTools.Files.csproj.AssemblyReference.cache b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.csproj.AssemblyReference.cache new file mode 100644 index 00000000..7be1c1fc Binary files /dev/null and b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.csproj.AssemblyReference.cache differ diff --git a/PDTools.Files/obj/Debug/net9.0/PDTools.Files.csproj.CoreCompileInputs.cache b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.csproj.CoreCompileInputs.cache new file mode 100644 index 00000000..c3f73f4d --- /dev/null +++ b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +63435f30b9fb076f518130c1da3ec0afa854c1b1a350d16fd9fe270a0225895a diff --git a/PDTools.Files/obj/Debug/net9.0/PDTools.Files.csproj.FileListAbsolute.txt b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.csproj.FileListAbsolute.txt new file mode 100644 index 00000000..e1113a79 --- /dev/null +++ b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.csproj.FileListAbsolute.txt @@ -0,0 +1,17 @@ +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Debug\net9.0\PDTools.Files.deps.json +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Debug\net9.0\PDTools.Files.dll +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Debug\net9.0\PDTools.Files.pdb +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Debug\net9.0\PDTools.Hashing.dll +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Debug\net9.0\PDTools.Utils.dll +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Debug\net9.0\PDTools.Hashing.pdb +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Debug\net9.0\PDTools.Utils.pdb +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Debug\net9.0\PDTools.Files.csproj.AssemblyReference.cache +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Debug\net9.0\PDTools.Files.GeneratedMSBuildEditorConfig.editorconfig +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Debug\net9.0\PDTools.Files.AssemblyInfoInputs.cache +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Debug\net9.0\PDTools.Files.AssemblyInfo.cs +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Debug\net9.0\PDTools.Files.csproj.CoreCompileInputs.cache +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Debug\net9.0\PDTools..56880567.Up2Date +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Debug\net9.0\PDTools.Files.dll +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Debug\net9.0\refint\PDTools.Files.dll +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Debug\net9.0\PDTools.Files.pdb +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Debug\net9.0\ref\PDTools.Files.dll diff --git a/PDTools.Files/obj/Debug/net9.0/PDTools.Files.dll b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.dll new file mode 100644 index 00000000..4aaf9b74 Binary files /dev/null and b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.dll differ diff --git a/PDTools.Files/obj/Debug/net9.0/PDTools.Files.pdb b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.pdb new file mode 100644 index 00000000..599d3c81 Binary files /dev/null and b/PDTools.Files/obj/Debug/net9.0/PDTools.Files.pdb differ diff --git a/PDTools.Files/obj/Debug/net9.0/ref/PDTools.Files.dll b/PDTools.Files/obj/Debug/net9.0/ref/PDTools.Files.dll new file mode 100644 index 00000000..8b4e0b77 Binary files /dev/null and b/PDTools.Files/obj/Debug/net9.0/ref/PDTools.Files.dll differ diff --git a/PDTools.Files/obj/Debug/net9.0/refint/PDTools.Files.dll b/PDTools.Files/obj/Debug/net9.0/refint/PDTools.Files.dll new file mode 100644 index 00000000..8b4e0b77 Binary files /dev/null and b/PDTools.Files/obj/Debug/net9.0/refint/PDTools.Files.dll differ diff --git a/PDTools.Files/obj/PDTools.Files.csproj.nuget.dgspec.json b/PDTools.Files/obj/PDTools.Files.csproj.nuget.dgspec.json new file mode 100644 index 00000000..a42876c4 --- /dev/null +++ b/PDTools.Files/obj/PDTools.Files.csproj.nuget.dgspec.json @@ -0,0 +1,290 @@ +{ + "format": 1, + "restore": { + "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\PDTools.Files.csproj": {} + }, + "projects": { + "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\PDTools.Files.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\PDTools.Files.csproj", + "projectName": "PDTools.Files", + "projectPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\PDTools.Files.csproj", + "packagesPath": "C:\\Users\\User\\.nuget\\packages\\", + "outputPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\obj\\", + "projectStyle": "PackageReference", + "configFilePaths": [ + "C:\\Users\\User\\AppData\\Roaming\\NuGet\\NuGet.Config" + ], + "originalTargetFrameworks": [ + "net9.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net9.0": { + "framework": "net9.0", + "targetAlias": "net9.0", + "projectReferences": { + "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Hashing\\PDTools.Hashing.csproj": { + "projectPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Hashing\\PDTools.Hashing.csproj" + }, + "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Utils\\PDTools.Utils.csproj": { + "projectPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Utils\\PDTools.Utils.csproj" + } + } + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.300" + }, + "frameworks": { + "net9.0": { + "framework": "net9.0", + "targetAlias": "net9.0", + "dependencies": { + "BCnEncoder.Net": { + "target": "Package", + "version": "[2.1.0, )" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "target": "Package", + "version": "[9.0.1, )" + }, + "Pfim": { + "target": "Package", + "version": "[0.11.3, )" + }, + "SharpZipLib": { + "target": "Package", + "version": "[1.4.2, )" + }, + "SixLabors.ImageSharp": { + "target": "Package", + "version": "[3.1.6, )" + }, + "SixLabors.ImageSharp.Drawing": { + "target": "Package", + "version": "[2.1.5, )" + }, + "Syroot.BinaryData": { + "target": "Package", + "version": "[5.2.2, )" + }, + "Syroot.BinaryData.Memory": { + "target": "Package", + "version": "[5.2.2, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "downloadDependencies": [ + { + "name": "Microsoft.AspNetCore.App.Ref", + "version": "[9.0.16, 9.0.16]" + }, + { + "name": "Microsoft.NETCore.App.Ref", + "version": "[9.0.16, 9.0.16]" + }, + { + "name": "Microsoft.WindowsDesktop.App.Ref", + "version": "[9.0.16, 9.0.16]" + } + ], + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.300/PortableRuntimeIdentifierGraph.json" + } + } + }, + "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Hashing\\PDTools.Hashing.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Hashing\\PDTools.Hashing.csproj", + "projectName": "PDTools.Hashing", + "projectPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Hashing\\PDTools.Hashing.csproj", + "packagesPath": "C:\\Users\\User\\.nuget\\packages\\", + "outputPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Hashing\\obj\\", + "projectStyle": "PackageReference", + "configFilePaths": [ + "C:\\Users\\User\\AppData\\Roaming\\NuGet\\NuGet.Config" + ], + "originalTargetFrameworks": [ + "net9.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net9.0": { + "framework": "net9.0", + "targetAlias": "net9.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.300" + }, + "frameworks": { + "net9.0": { + "framework": "net9.0", + "targetAlias": "net9.0", + "dependencies": { + "System.Memory": { + "target": "Package", + "version": "[4.5.5, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "downloadDependencies": [ + { + "name": "Microsoft.AspNetCore.App.Ref", + "version": "[9.0.16, 9.0.16]" + }, + { + "name": "Microsoft.NETCore.App.Ref", + "version": "[9.0.16, 9.0.16]" + }, + { + "name": "Microsoft.WindowsDesktop.App.Ref", + "version": "[9.0.16, 9.0.16]" + } + ], + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.300/PortableRuntimeIdentifierGraph.json" + } + } + }, + "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Utils\\PDTools.Utils.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Utils\\PDTools.Utils.csproj", + "projectName": "PDTools.Utils", + "projectPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Utils\\PDTools.Utils.csproj", + "packagesPath": "C:\\Users\\User\\.nuget\\packages\\", + "outputPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Utils\\obj\\", + "projectStyle": "PackageReference", + "configFilePaths": [ + "C:\\Users\\User\\AppData\\Roaming\\NuGet\\NuGet.Config" + ], + "originalTargetFrameworks": [ + "net9.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net9.0": { + "framework": "net9.0", + "targetAlias": "net9.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.300" + }, + "frameworks": { + "net9.0": { + "framework": "net9.0", + "targetAlias": "net9.0", + "dependencies": { + "Syroot.BinaryData": { + "target": "Package", + "version": "[5.2.2, )" + }, + "Syroot.BinaryData.Memory": { + "target": "Package", + "version": "[5.2.2, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "downloadDependencies": [ + { + "name": "Microsoft.AspNetCore.App.Ref", + "version": "[9.0.16, 9.0.16]" + }, + { + "name": "Microsoft.NETCore.App.Ref", + "version": "[9.0.16, 9.0.16]" + }, + { + "name": "Microsoft.WindowsDesktop.App.Ref", + "version": "[9.0.16, 9.0.16]" + } + ], + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.300/PortableRuntimeIdentifierGraph.json" + } + } + } + } +} \ No newline at end of file diff --git a/PDTools.Files/obj/PDTools.Files.csproj.nuget.g.props b/PDTools.Files/obj/PDTools.Files.csproj.nuget.g.props new file mode 100644 index 00000000..444c68fd --- /dev/null +++ b/PDTools.Files/obj/PDTools.Files.csproj.nuget.g.props @@ -0,0 +1,18 @@ + + + + True + NuGet + $(MSBuildThisFileDirectory)project.assets.json + $(UserProfile)\.nuget\packages\ + C:\Users\User\.nuget\packages\ + PackageReference + 7.0.0 + + + + + + + + \ No newline at end of file diff --git a/PDTools.Files/obj/PDTools.Files.csproj.nuget.g.targets b/PDTools.Files/obj/PDTools.Files.csproj.nuget.g.targets new file mode 100644 index 00000000..c7aaeb91 --- /dev/null +++ b/PDTools.Files/obj/PDTools.Files.csproj.nuget.g.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/PDTools.Files/obj/Release/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs b/PDTools.Files/obj/Release/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs new file mode 100644 index 00000000..ea1f2eae --- /dev/null +++ b/PDTools.Files/obj/Release/net9.0/.NETCoreApp,Version=v9.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0")] diff --git a/PDTools.Files/obj/Release/net9.0/PDTools..56880567.Up2Date b/PDTools.Files/obj/Release/net9.0/PDTools..56880567.Up2Date new file mode 100644 index 00000000..e69de29b diff --git a/PDTools.Files/obj/Release/net9.0/PDTools.Files.AssemblyInfo.cs b/PDTools.Files/obj/Release/net9.0/PDTools.Files.AssemblyInfo.cs new file mode 100644 index 00000000..fba4ab41 --- /dev/null +++ b/PDTools.Files/obj/Release/net9.0/PDTools.Files.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("PDTools.Files")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("PDTools.Files")] +[assembly: System.Reflection.AssemblyTitleAttribute("PDTools.Files")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Generated by the MSBuild WriteCodeFragment class. + diff --git a/PDTools.Files/obj/Release/net9.0/PDTools.Files.AssemblyInfoInputs.cache b/PDTools.Files/obj/Release/net9.0/PDTools.Files.AssemblyInfoInputs.cache new file mode 100644 index 00000000..b72a2b29 --- /dev/null +++ b/PDTools.Files/obj/Release/net9.0/PDTools.Files.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +2ae2567d1b8568a89e738f795b1f6d8da26abe1c028abdb8f4409e3e4c8e0026 diff --git a/PDTools.Files/obj/Release/net9.0/PDTools.Files.GeneratedMSBuildEditorConfig.editorconfig b/PDTools.Files/obj/Release/net9.0/PDTools.Files.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 00000000..7e08071d --- /dev/null +++ b/PDTools.Files/obj/Release/net9.0/PDTools.Files.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,18 @@ +is_global = true +build_property.TargetFramework = net9.0 +build_property.TargetFrameworkIdentifier = .NETCoreApp +build_property.TargetFrameworkVersion = v9.0 +build_property.TargetPlatformMinVersion = +build_property.UsingMicrosoftNETSdkWeb = +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property.EnforceExtendedAnalyzerRules = +build_property.EntryPointFilePath = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = PDTools.Files +build_property.ProjectDir = C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.EffectiveAnalysisLevelStyle = 9.0 +build_property.EnableCodeStyleSeverity = diff --git a/PDTools.Files/obj/Release/net9.0/PDTools.Files.assets.cache b/PDTools.Files/obj/Release/net9.0/PDTools.Files.assets.cache new file mode 100644 index 00000000..7398cbfe Binary files /dev/null and b/PDTools.Files/obj/Release/net9.0/PDTools.Files.assets.cache differ diff --git a/PDTools.Files/obj/Release/net9.0/PDTools.Files.csproj.AssemblyReference.cache b/PDTools.Files/obj/Release/net9.0/PDTools.Files.csproj.AssemblyReference.cache new file mode 100644 index 00000000..1d59f9bf Binary files /dev/null and b/PDTools.Files/obj/Release/net9.0/PDTools.Files.csproj.AssemblyReference.cache differ diff --git a/PDTools.Files/obj/Release/net9.0/PDTools.Files.csproj.CoreCompileInputs.cache b/PDTools.Files/obj/Release/net9.0/PDTools.Files.csproj.CoreCompileInputs.cache new file mode 100644 index 00000000..2b5fb496 --- /dev/null +++ b/PDTools.Files/obj/Release/net9.0/PDTools.Files.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +d3136e038a6397daadae72a4a78c33ef3e68fade8a68bad794e75399ee0d4ec6 diff --git a/PDTools.Files/obj/Release/net9.0/PDTools.Files.csproj.FileListAbsolute.txt b/PDTools.Files/obj/Release/net9.0/PDTools.Files.csproj.FileListAbsolute.txt new file mode 100644 index 00000000..3ff89f36 --- /dev/null +++ b/PDTools.Files/obj/Release/net9.0/PDTools.Files.csproj.FileListAbsolute.txt @@ -0,0 +1,17 @@ +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Release\net9.0\PDTools.Files.deps.json +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Release\net9.0\PDTools.Files.dll +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Release\net9.0\PDTools.Files.pdb +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Release\net9.0\PDTools.Hashing.dll +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Release\net9.0\PDTools.Utils.dll +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Release\net9.0\PDTools.Hashing.pdb +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\bin\Release\net9.0\PDTools.Utils.pdb +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Release\net9.0\PDTools.Files.csproj.AssemblyReference.cache +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Release\net9.0\PDTools.Files.GeneratedMSBuildEditorConfig.editorconfig +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Release\net9.0\PDTools.Files.AssemblyInfoInputs.cache +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Release\net9.0\PDTools.Files.AssemblyInfo.cs +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Release\net9.0\PDTools.Files.csproj.CoreCompileInputs.cache +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Release\net9.0\PDTools..56880567.Up2Date +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Release\net9.0\PDTools.Files.dll +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Release\net9.0\refint\PDTools.Files.dll +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Release\net9.0\PDTools.Files.pdb +C:\Users\User\Desktop\gtps2modeltool2\PDTools\PDTools.Files\obj\Release\net9.0\ref\PDTools.Files.dll diff --git a/PDTools.Files/obj/Release/net9.0/PDTools.Files.dll b/PDTools.Files/obj/Release/net9.0/PDTools.Files.dll new file mode 100644 index 00000000..bc7a4c4a Binary files /dev/null and b/PDTools.Files/obj/Release/net9.0/PDTools.Files.dll differ diff --git a/PDTools.Files/obj/Release/net9.0/PDTools.Files.pdb b/PDTools.Files/obj/Release/net9.0/PDTools.Files.pdb new file mode 100644 index 00000000..d9f8c321 Binary files /dev/null and b/PDTools.Files/obj/Release/net9.0/PDTools.Files.pdb differ diff --git a/PDTools.Files/obj/Release/net9.0/ref/PDTools.Files.dll b/PDTools.Files/obj/Release/net9.0/ref/PDTools.Files.dll new file mode 100644 index 00000000..9830b6be Binary files /dev/null and b/PDTools.Files/obj/Release/net9.0/ref/PDTools.Files.dll differ diff --git a/PDTools.Files/obj/Release/net9.0/refint/PDTools.Files.dll b/PDTools.Files/obj/Release/net9.0/refint/PDTools.Files.dll new file mode 100644 index 00000000..9830b6be Binary files /dev/null and b/PDTools.Files/obj/Release/net9.0/refint/PDTools.Files.dll differ diff --git a/PDTools.Files/obj/project.assets.json b/PDTools.Files/obj/project.assets.json new file mode 100644 index 00000000..d774ff5e --- /dev/null +++ b/PDTools.Files/obj/project.assets.json @@ -0,0 +1,722 @@ +{ + "version": 4, + "targets": { + "net9.0": { + "BCnEncoder.Net/2.1.0": { + "type": "package", + "dependencies": { + "Microsoft.Toolkit.HighPerformance": "7.0.2" + }, + "compile": { + "lib/netstandard2.1/BCnEncoder.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.1/BCnEncoder.dll": { + "related": ".xml" + } + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": { + "type": "package", + "compile": { + "lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net8.0/_._": {} + } + }, + "Microsoft.Extensions.Logging.Abstractions/9.0.1": { + "type": "package", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1" + }, + "compile": { + "lib/net9.0/Microsoft.Extensions.Logging.Abstractions.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net9.0/Microsoft.Extensions.Logging.Abstractions.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net8.0/Microsoft.Extensions.Logging.Abstractions.targets": {} + } + }, + "Microsoft.Toolkit.HighPerformance/7.0.2": { + "type": "package", + "compile": { + "lib/net5.0/Microsoft.Toolkit.HighPerformance.dll": { + "related": ".pdb;.xml" + } + }, + "runtime": { + "lib/net5.0/Microsoft.Toolkit.HighPerformance.dll": { + "related": ".pdb;.xml" + } + } + }, + "Pfim/0.11.3": { + "type": "package", + "compile": { + "lib/netstandard2.0/Pfim.dll": {} + }, + "runtime": { + "lib/netstandard2.0/Pfim.dll": {} + } + }, + "SharpZipLib/1.4.2": { + "type": "package", + "compile": { + "lib/net6.0/ICSharpCode.SharpZipLib.dll": { + "related": ".pdb;.xml" + } + }, + "runtime": { + "lib/net6.0/ICSharpCode.SharpZipLib.dll": { + "related": ".pdb;.xml" + } + } + }, + "SixLabors.Fonts/2.0.8": { + "type": "package", + "compile": { + "lib/net6.0/SixLabors.Fonts.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/SixLabors.Fonts.dll": { + "related": ".xml" + } + } + }, + "SixLabors.ImageSharp/3.1.6": { + "type": "package", + "compile": { + "lib/net6.0/SixLabors.ImageSharp.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/SixLabors.ImageSharp.dll": { + "related": ".xml" + } + }, + "build": { + "build/SixLabors.ImageSharp.props": {} + } + }, + "SixLabors.ImageSharp.Drawing/2.1.5": { + "type": "package", + "dependencies": { + "SixLabors.Fonts": "2.0.8", + "SixLabors.ImageSharp": "3.1.6" + }, + "compile": { + "lib/net6.0/SixLabors.ImageSharp.Drawing.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/SixLabors.ImageSharp.Drawing.dll": { + "related": ".xml" + } + } + }, + "Syroot.BinaryData/5.2.2": { + "type": "package", + "dependencies": { + "Syroot.BinaryData.Core": "5.2.0", + "System.Memory": "4.5.2" + }, + "compile": { + "lib/netstandard2.0/Syroot.BinaryData.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netstandard2.0/Syroot.BinaryData.dll": { + "related": ".xml" + } + } + }, + "Syroot.BinaryData.Core/5.2.0": { + "type": "package", + "compile": { + "lib/netcoreapp2.1/Syroot.BinaryData.Core.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netcoreapp2.1/Syroot.BinaryData.Core.dll": { + "related": ".xml" + } + } + }, + "Syroot.BinaryData.Memory/5.2.2": { + "type": "package", + "dependencies": { + "Syroot.BinaryData.Core": "5.2.0", + "System.Memory": "4.5.2", + "System.Runtime.CompilerServices.Unsafe": "4.5.2" + }, + "compile": { + "lib/netcoreapp2.1/Syroot.BinaryData.Memory.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netcoreapp2.1/Syroot.BinaryData.Memory.dll": { + "related": ".xml" + } + } + }, + "System.Memory/4.5.5": { + "type": "package", + "compile": { + "ref/netcoreapp2.1/_._": {} + }, + "runtime": { + "lib/netcoreapp2.1/_._": {} + } + }, + "System.Runtime.CompilerServices.Unsafe/4.5.2": { + "type": "package", + "compile": { + "ref/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/netcoreapp2.0/System.Runtime.CompilerServices.Unsafe.dll": { + "related": ".xml" + } + } + }, + "PDTools.Hashing/1.0.0": { + "type": "project", + "framework": ".NETCoreApp,Version=v9.0", + "dependencies": { + "System.Memory": "4.5.5" + }, + "compile": { + "bin/placeholder/PDTools.Hashing.dll": {} + }, + "runtime": { + "bin/placeholder/PDTools.Hashing.dll": {} + } + }, + "PDTools.Utils/1.0.0": { + "type": "project", + "framework": ".NETCoreApp,Version=v9.0", + "dependencies": { + "Syroot.BinaryData": "5.2.2", + "Syroot.BinaryData.Memory": "5.2.2" + }, + "compile": { + "bin/placeholder/PDTools.Utils.dll": {} + }, + "runtime": { + "bin/placeholder/PDTools.Utils.dll": {} + } + } + } + }, + "libraries": { + "BCnEncoder.Net/2.1.0": { + "sha512": "bBKYUDhpYenmp8vy5BVUHmS2LFym0NMga9V+S1eMJ+4zQLBwDw63Bs83AKOyqBKC2AqqgxXdTFA2a2V+SmGoIw==", + "type": "package", + "path": "bcnencoder.net/2.1.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "bcnencoder.net.2.1.0.nupkg.sha512", + "bcnencoder.net.nuspec", + "lib/netstandard2.1/BCnEncoder.dll", + "lib/netstandard2.1/BCnEncoder.xml" + ] + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": { + "sha512": "Tr74eP0oQ3AyC24ch17N8PuEkrPbD0JqIfENCYqmgKYNOmL8wQKzLJu3ObxTUDrjnn4rHoR1qKa37/eQyHmCDA==", + "type": "package", + "path": "microsoft.extensions.dependencyinjection.abstractions/9.0.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "PACKAGE.md", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/Microsoft.Extensions.DependencyInjection.Abstractions.targets", + "buildTransitive/net462/_._", + "buildTransitive/net8.0/_._", + "buildTransitive/netcoreapp2.0/Microsoft.Extensions.DependencyInjection.Abstractions.targets", + "lib/net462/Microsoft.Extensions.DependencyInjection.Abstractions.dll", + "lib/net462/Microsoft.Extensions.DependencyInjection.Abstractions.xml", + "lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll", + "lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml", + "lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll", + "lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml", + "lib/netstandard2.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll", + "lib/netstandard2.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml", + "lib/netstandard2.1/Microsoft.Extensions.DependencyInjection.Abstractions.dll", + "lib/netstandard2.1/Microsoft.Extensions.DependencyInjection.Abstractions.xml", + "microsoft.extensions.dependencyinjection.abstractions.9.0.1.nupkg.sha512", + "microsoft.extensions.dependencyinjection.abstractions.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "Microsoft.Extensions.Logging.Abstractions/9.0.1": { + "sha512": "w2gUqXN/jNIuvqYwX3lbXagsizVNXYyt6LlF57+tMve4JYCEgCMMAjRce6uKcDASJgpMbErRT1PfHy2OhbkqEA==", + "type": "package", + "path": "microsoft.extensions.logging.abstractions/9.0.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "PACKAGE.md", + "THIRD-PARTY-NOTICES.TXT", + "analyzers/dotnet/roslyn3.11/cs/Microsoft.Extensions.Logging.Generators.dll", + "analyzers/dotnet/roslyn3.11/cs/cs/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/de/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/es/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/fr/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/it/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/ja/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/ko/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/pl/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/pt-BR/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/ru/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/tr/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/zh-Hans/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn3.11/cs/zh-Hant/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/Microsoft.Extensions.Logging.Generators.dll", + "analyzers/dotnet/roslyn4.0/cs/cs/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/de/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/es/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/fr/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/it/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/ja/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/ko/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/pl/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/pt-BR/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/ru/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/tr/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/zh-Hans/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.0/cs/zh-Hant/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Logging.Generators.dll", + "analyzers/dotnet/roslyn4.4/cs/cs/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/de/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/es/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/fr/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/it/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/ja/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/ko/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/pl/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/pt-BR/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/ru/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/tr/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/zh-Hans/Microsoft.Extensions.Logging.Generators.resources.dll", + "analyzers/dotnet/roslyn4.4/cs/zh-Hant/Microsoft.Extensions.Logging.Generators.resources.dll", + "buildTransitive/net461/Microsoft.Extensions.Logging.Abstractions.targets", + "buildTransitive/net462/Microsoft.Extensions.Logging.Abstractions.targets", + "buildTransitive/net8.0/Microsoft.Extensions.Logging.Abstractions.targets", + "buildTransitive/netcoreapp2.0/Microsoft.Extensions.Logging.Abstractions.targets", + "buildTransitive/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.targets", + "lib/net462/Microsoft.Extensions.Logging.Abstractions.dll", + "lib/net462/Microsoft.Extensions.Logging.Abstractions.xml", + "lib/net8.0/Microsoft.Extensions.Logging.Abstractions.dll", + "lib/net8.0/Microsoft.Extensions.Logging.Abstractions.xml", + "lib/net9.0/Microsoft.Extensions.Logging.Abstractions.dll", + "lib/net9.0/Microsoft.Extensions.Logging.Abstractions.xml", + "lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.dll", + "lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.xml", + "microsoft.extensions.logging.abstractions.9.0.1.nupkg.sha512", + "microsoft.extensions.logging.abstractions.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "Microsoft.Toolkit.HighPerformance/7.0.2": { + "sha512": "tG6v0AhDw/yJMtiH/tI4F6I/+sNIfu4++6WFfN73FyNcPh31et33w45wo8Lx1sfxcfLtkYk+w2fvU/MubYg4VQ==", + "type": "package", + "path": "microsoft.toolkit.highperformance/7.0.2", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "images/nuget.png", + "lib/net5.0/Microsoft.Toolkit.HighPerformance.dll", + "lib/net5.0/Microsoft.Toolkit.HighPerformance.pdb", + "lib/net5.0/Microsoft.Toolkit.HighPerformance.xml", + "lib/netcoreapp2.1/Microsoft.Toolkit.HighPerformance.dll", + "lib/netcoreapp2.1/Microsoft.Toolkit.HighPerformance.pdb", + "lib/netcoreapp2.1/Microsoft.Toolkit.HighPerformance.xml", + "lib/netcoreapp3.1/Microsoft.Toolkit.HighPerformance.dll", + "lib/netcoreapp3.1/Microsoft.Toolkit.HighPerformance.pdb", + "lib/netcoreapp3.1/Microsoft.Toolkit.HighPerformance.xml", + "lib/netstandard1.4/Microsoft.Toolkit.HighPerformance.dll", + "lib/netstandard1.4/Microsoft.Toolkit.HighPerformance.pdb", + "lib/netstandard1.4/Microsoft.Toolkit.HighPerformance.xml", + "lib/netstandard2.0/Microsoft.Toolkit.HighPerformance.dll", + "lib/netstandard2.0/Microsoft.Toolkit.HighPerformance.pdb", + "lib/netstandard2.0/Microsoft.Toolkit.HighPerformance.xml", + "lib/netstandard2.1/Microsoft.Toolkit.HighPerformance.dll", + "lib/netstandard2.1/Microsoft.Toolkit.HighPerformance.pdb", + "lib/netstandard2.1/Microsoft.Toolkit.HighPerformance.xml", + "license.md", + "microsoft.toolkit.highperformance.7.0.2.nupkg.sha512", + "microsoft.toolkit.highperformance.nuspec" + ] + }, + "Pfim/0.11.3": { + "sha512": "UNVStuGHVIGyBlQaLX8VY6KpzZm/pG2zpV8ewNSXNFKFVPn8dLQKJITfps3lwUMzwTL+Do7RrMUvgQ1ZsPTu4w==", + "type": "package", + "path": "pfim/0.11.3", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/netstandard2.0/Pfim.dll", + "pfim.0.11.3.nupkg.sha512", + "pfim.nuspec" + ] + }, + "SharpZipLib/1.4.2": { + "sha512": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==", + "type": "package", + "path": "sharpziplib/1.4.2", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "images/sharpziplib-nuget-256x256.png", + "lib/net6.0/ICSharpCode.SharpZipLib.dll", + "lib/net6.0/ICSharpCode.SharpZipLib.pdb", + "lib/net6.0/ICSharpCode.SharpZipLib.xml", + "lib/netstandard2.0/ICSharpCode.SharpZipLib.dll", + "lib/netstandard2.0/ICSharpCode.SharpZipLib.pdb", + "lib/netstandard2.0/ICSharpCode.SharpZipLib.xml", + "lib/netstandard2.1/ICSharpCode.SharpZipLib.dll", + "lib/netstandard2.1/ICSharpCode.SharpZipLib.pdb", + "lib/netstandard2.1/ICSharpCode.SharpZipLib.xml", + "sharpziplib.1.4.2.nupkg.sha512", + "sharpziplib.nuspec" + ] + }, + "SixLabors.Fonts/2.0.8": { + "sha512": "psTLKJVKaD8PKm+Bz/mSSNo4m0VCTYoJ97TF/ynxEWkn4sFUipI0rzE2V/KBtfU0TOn/gF/+1XBOEvusGZZMhA==", + "type": "package", + "path": "sixlabors.fonts/2.0.8", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE", + "lib/net6.0/SixLabors.Fonts.dll", + "lib/net6.0/SixLabors.Fonts.xml", + "sixlabors.fonts.128.png", + "sixlabors.fonts.2.0.8.nupkg.sha512", + "sixlabors.fonts.nuspec" + ] + }, + "SixLabors.ImageSharp/3.1.6": { + "sha512": "dHQ5jugF9v+5/LCVHCWVzaaIL6WOehqJy6eju/0VFYFPEj2WtqkGPoEV9EVQP83dHsdoqYaTuWpZdwAd37UwfA==", + "type": "package", + "path": "sixlabors.imagesharp/3.1.6", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE", + "build/SixLabors.ImageSharp.props", + "lib/net6.0/SixLabors.ImageSharp.dll", + "lib/net6.0/SixLabors.ImageSharp.xml", + "sixlabors.imagesharp.128.png", + "sixlabors.imagesharp.3.1.6.nupkg.sha512", + "sixlabors.imagesharp.nuspec" + ] + }, + "SixLabors.ImageSharp.Drawing/2.1.5": { + "sha512": "cER1JfvshYDmTxw+gUy/x5e1xoNWhrD6s3AFf8rRUx9hWSXYdOFreQfUrM/QooMj0rF7+hkVtvGnV3EdMx4dxA==", + "type": "package", + "path": "sixlabors.imagesharp.drawing/2.1.5", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE", + "lib/net6.0/SixLabors.ImageSharp.Drawing.dll", + "lib/net6.0/SixLabors.ImageSharp.Drawing.xml", + "sixlabors.imagesharp.drawing.128.png", + "sixlabors.imagesharp.drawing.2.1.5.nupkg.sha512", + "sixlabors.imagesharp.drawing.nuspec" + ] + }, + "Syroot.BinaryData/5.2.2": { + "sha512": "3E4l6a20cnsgywGdurRfrtv3+au02AsKb3yEmcsskgJrJsRv2UaD9MaSFOJLloAjP5ZI8Tjn6YSWF3k5pfdrkg==", + "type": "package", + "path": "syroot.binarydata/5.2.2", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/net452/Syroot.BinaryData.dll", + "lib/net452/Syroot.BinaryData.xml", + "lib/netstandard2.0/Syroot.BinaryData.dll", + "lib/netstandard2.0/Syroot.BinaryData.xml", + "syroot.binarydata.5.2.2.nupkg.sha512", + "syroot.binarydata.nuspec" + ] + }, + "Syroot.BinaryData.Core/5.2.0": { + "sha512": "zMrTKIa+zwvFqkahbDdCzGW0Jq7BVE44gzuK0OT0ia+DpRtZTcwqOzSMiLs7Z2urY9vMqh5yTMLGVUal0dkQAA==", + "type": "package", + "path": "syroot.binarydata.core/5.2.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/net452/Syroot.BinaryData.Core.dll", + "lib/net452/Syroot.BinaryData.Core.xml", + "lib/netcoreapp2.1/Syroot.BinaryData.Core.dll", + "lib/netcoreapp2.1/Syroot.BinaryData.Core.xml", + "lib/netstandard2.0/Syroot.BinaryData.Core.dll", + "lib/netstandard2.0/Syroot.BinaryData.Core.xml", + "syroot.binarydata.core.5.2.0.nupkg.sha512", + "syroot.binarydata.core.nuspec" + ] + }, + "Syroot.BinaryData.Memory/5.2.2": { + "sha512": "SZhuJbqrXrECebYwWIRfuvWAlw0wcEvpM98ZDdMl0JSwgocvzGqFC7lP9j8oeeHjmNxOJ+cxNH3wVkGNwf0Q9A==", + "type": "package", + "path": "syroot.binarydata.memory/5.2.2", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/netcoreapp2.1/Syroot.BinaryData.Memory.dll", + "lib/netcoreapp2.1/Syroot.BinaryData.Memory.xml", + "lib/netstandard2.0/Syroot.BinaryData.Memory.dll", + "lib/netstandard2.0/Syroot.BinaryData.Memory.xml", + "syroot.binarydata.memory.5.2.2.nupkg.sha512", + "syroot.binarydata.memory.nuspec" + ] + }, + "System.Memory/4.5.5": { + "sha512": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==", + "type": "package", + "path": "system.memory/4.5.5", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/net461/System.Memory.dll", + "lib/net461/System.Memory.xml", + "lib/netcoreapp2.1/_._", + "lib/netstandard1.1/System.Memory.dll", + "lib/netstandard1.1/System.Memory.xml", + "lib/netstandard2.0/System.Memory.dll", + "lib/netstandard2.0/System.Memory.xml", + "ref/netcoreapp2.1/_._", + "system.memory.4.5.5.nupkg.sha512", + "system.memory.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "System.Runtime.CompilerServices.Unsafe/4.5.2": { + "sha512": "wprSFgext8cwqymChhrBLu62LMg/1u92bU+VOwyfBimSPVFXtsNqEWC92Pf9ofzJFlk4IHmJA75EDJn1b2goAQ==", + "type": "package", + "path": "system.runtime.compilerservices.unsafe/4.5.2", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "lib/netcoreapp2.0/System.Runtime.CompilerServices.Unsafe.dll", + "lib/netcoreapp2.0/System.Runtime.CompilerServices.Unsafe.xml", + "lib/netstandard1.0/System.Runtime.CompilerServices.Unsafe.dll", + "lib/netstandard1.0/System.Runtime.CompilerServices.Unsafe.xml", + "lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll", + "lib/netstandard2.0/System.Runtime.CompilerServices.Unsafe.xml", + "ref/netstandard1.0/System.Runtime.CompilerServices.Unsafe.dll", + "ref/netstandard1.0/System.Runtime.CompilerServices.Unsafe.xml", + "ref/netstandard2.0/System.Runtime.CompilerServices.Unsafe.dll", + "ref/netstandard2.0/System.Runtime.CompilerServices.Unsafe.xml", + "system.runtime.compilerservices.unsafe.4.5.2.nupkg.sha512", + "system.runtime.compilerservices.unsafe.nuspec", + "useSharedDesignerContext.txt", + "version.txt" + ] + }, + "PDTools.Hashing/1.0.0": { + "type": "project", + "path": "../PDTools.Hashing/PDTools.Hashing.csproj", + "msbuildProject": "../PDTools.Hashing/PDTools.Hashing.csproj" + }, + "PDTools.Utils/1.0.0": { + "type": "project", + "path": "../PDTools.Utils/PDTools.Utils.csproj", + "msbuildProject": "../PDTools.Utils/PDTools.Utils.csproj" + } + }, + "projectFileDependencyGroups": { + "net9.0": [ + "BCnEncoder.Net >= 2.1.0", + "Microsoft.Extensions.Logging.Abstractions >= 9.0.1", + "PDTools.Hashing >= 1.0.0", + "PDTools.Utils >= 1.0.0", + "Pfim >= 0.11.3", + "SharpZipLib >= 1.4.2", + "SixLabors.ImageSharp >= 3.1.6", + "SixLabors.ImageSharp.Drawing >= 2.1.5", + "Syroot.BinaryData >= 5.2.2", + "Syroot.BinaryData.Memory >= 5.2.2" + ] + }, + "packageFolders": { + "C:\\Users\\User\\.nuget\\packages\\": {} + }, + "project": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\PDTools.Files.csproj", + "projectName": "PDTools.Files", + "projectPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\PDTools.Files.csproj", + "packagesPath": "C:\\Users\\User\\.nuget\\packages\\", + "outputPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\obj\\", + "projectStyle": "PackageReference", + "configFilePaths": [ + "C:\\Users\\User\\AppData\\Roaming\\NuGet\\NuGet.Config" + ], + "originalTargetFrameworks": [ + "net9.0" + ], + "sources": { + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net9.0": { + "framework": "net9.0", + "targetAlias": "net9.0", + "projectReferences": { + "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Hashing\\PDTools.Hashing.csproj": { + "projectPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Hashing\\PDTools.Hashing.csproj" + }, + "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Utils\\PDTools.Utils.csproj": { + "projectPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Utils\\PDTools.Utils.csproj" + } + } + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.300" + }, + "frameworks": { + "net9.0": { + "framework": "net9.0", + "targetAlias": "net9.0", + "dependencies": { + "BCnEncoder.Net": { + "target": "Package", + "version": "[2.1.0, )" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "target": "Package", + "version": "[9.0.1, )" + }, + "Pfim": { + "target": "Package", + "version": "[0.11.3, )" + }, + "SharpZipLib": { + "target": "Package", + "version": "[1.4.2, )" + }, + "SixLabors.ImageSharp": { + "target": "Package", + "version": "[3.1.6, )" + }, + "SixLabors.ImageSharp.Drawing": { + "target": "Package", + "version": "[2.1.5, )" + }, + "Syroot.BinaryData": { + "target": "Package", + "version": "[5.2.2, )" + }, + "Syroot.BinaryData.Memory": { + "target": "Package", + "version": "[5.2.2, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "downloadDependencies": [ + { + "name": "Microsoft.AspNetCore.App.Ref", + "version": "[9.0.16, 9.0.16]" + }, + { + "name": "Microsoft.NETCore.App.Ref", + "version": "[9.0.16, 9.0.16]" + }, + { + "name": "Microsoft.WindowsDesktop.App.Ref", + "version": "[9.0.16, 9.0.16]" + } + ], + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.300/PortableRuntimeIdentifierGraph.json" + } + } + }, + "logs": [ + { + "code": "NU1903", + "level": "Warning", + "warningLevel": 1, + "message": "Package 'SixLabors.ImageSharp' 3.1.6 has a known high severity vulnerability, https://github.com/advisories/GHSA-2cmq-823j-5qj8", + "libraryId": "SixLabors.ImageSharp", + "targetGraphs": [ + "net9.0" + ] + }, + { + "code": "NU1902", + "level": "Warning", + "warningLevel": 1, + "message": "Package 'SixLabors.ImageSharp' 3.1.6 has a known moderate severity vulnerability, https://github.com/advisories/GHSA-rxmq-m78w-7wmc", + "libraryId": "SixLabors.ImageSharp", + "targetGraphs": [ + "net9.0" + ] + } + ] +} \ No newline at end of file diff --git a/PDTools.Files/obj/project.nuget.cache b/PDTools.Files/obj/project.nuget.cache new file mode 100644 index 00000000..5827fe52 --- /dev/null +++ b/PDTools.Files/obj/project.nuget.cache @@ -0,0 +1,51 @@ +{ + "version": 2, + "dgSpecHash": "+AeklT/moLU=", + "success": true, + "projectFilePath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\PDTools.Files.csproj", + "expectedPackageFiles": [ + "C:\\Users\\User\\.nuget\\packages\\bcnencoder.net\\2.1.0\\bcnencoder.net.2.1.0.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\microsoft.extensions.dependencyinjection.abstractions\\9.0.1\\microsoft.extensions.dependencyinjection.abstractions.9.0.1.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\microsoft.extensions.logging.abstractions\\9.0.1\\microsoft.extensions.logging.abstractions.9.0.1.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\microsoft.toolkit.highperformance\\7.0.2\\microsoft.toolkit.highperformance.7.0.2.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\pfim\\0.11.3\\pfim.0.11.3.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\sharpziplib\\1.4.2\\sharpziplib.1.4.2.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\sixlabors.fonts\\2.0.8\\sixlabors.fonts.2.0.8.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\sixlabors.imagesharp\\3.1.6\\sixlabors.imagesharp.3.1.6.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\sixlabors.imagesharp.drawing\\2.1.5\\sixlabors.imagesharp.drawing.2.1.5.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\syroot.binarydata\\5.2.2\\syroot.binarydata.5.2.2.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\syroot.binarydata.core\\5.2.0\\syroot.binarydata.core.5.2.0.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\syroot.binarydata.memory\\5.2.2\\syroot.binarydata.memory.5.2.2.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\system.memory\\4.5.5\\system.memory.4.5.5.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\system.runtime.compilerservices.unsafe\\4.5.2\\system.runtime.compilerservices.unsafe.4.5.2.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\microsoft.netcore.app.ref\\9.0.16\\microsoft.netcore.app.ref.9.0.16.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\microsoft.windowsdesktop.app.ref\\9.0.16\\microsoft.windowsdesktop.app.ref.9.0.16.nupkg.sha512", + "C:\\Users\\User\\.nuget\\packages\\microsoft.aspnetcore.app.ref\\9.0.16\\microsoft.aspnetcore.app.ref.9.0.16.nupkg.sha512" + ], + "logs": [ + { + "code": "NU1903", + "level": "Warning", + "message": "Package 'SixLabors.ImageSharp' 3.1.6 has a known high severity vulnerability, https://github.com/advisories/GHSA-2cmq-823j-5qj8", + "projectPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\PDTools.Files.csproj", + "warningLevel": 1, + "filePath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\PDTools.Files.csproj", + "libraryId": "SixLabors.ImageSharp", + "targetGraphs": [ + "net9.0" + ] + }, + { + "code": "NU1902", + "level": "Warning", + "message": "Package 'SixLabors.ImageSharp' 3.1.6 has a known moderate severity vulnerability, https://github.com/advisories/GHSA-rxmq-m78w-7wmc", + "projectPath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\PDTools.Files.csproj", + "warningLevel": 1, + "filePath": "C:\\Users\\User\\Desktop\\gtps2modeltool2\\PDTools\\PDTools.Files\\PDTools.Files.csproj", + "libraryId": "SixLabors.ImageSharp", + "targetGraphs": [ + "net9.0" + ] + } + ] +} \ No newline at end of file