From 31a1a4f42f36df0bd2c51e6e045e97b2b8b880d7 Mon Sep 17 00:00:00 2001 From: gopicolo Date: Tue, 27 Jan 2026 01:07:48 -0300 Subject: [PATCH 1/3] Add PSP GIM support and support for guyzware Inc psp visual novels --- ArcFormats/ArcFormats.csproj | 2 + ArcFormats/Guyzware/ArcDat.cs | 93 +++++++++ ArcFormats/Psp/ArcGim.cs | 374 ++++++++++++++++++++++++++++++++++ 3 files changed, 469 insertions(+) create mode 100644 ArcFormats/Guyzware/ArcDat.cs create mode 100644 ArcFormats/Psp/ArcGim.cs diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index bde0d065b..035121934 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -161,6 +161,7 @@ + @@ -225,6 +226,7 @@ + diff --git a/ArcFormats/Guyzware/ArcDat.cs b/ArcFormats/Guyzware/ArcDat.cs new file mode 100644 index 000000000..1747bba0a --- /dev/null +++ b/ArcFormats/Guyzware/ArcDat.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using GameRes.Utility; + +namespace GameRes.Formats.Guyzware +{ + [Export(typeof(ArchiveFormat))] + public class GdpOpener : ArchiveFormat + { + public override string Tag => "DAT/GDP"; + public override string Description => "Guyzware engine)"; + public override uint Signature => 0; + public override bool IsHierarchic => false; + public override bool CanWrite => false; + + public override ArcFile TryOpen(ArcView file) + { + int count = file.View.ReadInt32(0); + if (!IsSaneCount(count)) + return null; + + var dir = new List(count); + long index_offset = 4; + + // heuristic: small first byte = variable + byte checkByte = file.View.ReadByte(index_offset); + bool isVariableLength = checkByte < 64; + + for (int i = 0; i < count; i++) + { + if (index_offset >= file.MaxOffset) + break; + + string name; + uint offset, size; + + if (isVariableLength) + { + // Variable length: + // [1 byte length (ignored)] [Shift-JIS string] [00] + + index_offset++; // skip length byte + + var nameBytes = new List(); + + while (index_offset < file.MaxOffset) + { + byte b = file.View.ReadByte(index_offset++); + if (b == 0) + break; + + nameBytes.Add(b); + } + + name = Encodings.cp932.GetString(nameBytes.ToArray()); + } + else + { + // Fixed 32 bytes + + name = file.View.ReadString(index_offset, 32, Encodings.cp932); + name = name.TrimEnd('\0', ' '); + index_offset += 32; + } + + offset = file.View.ReadUInt32(index_offset); + size = file.View.ReadUInt32(index_offset + 4); + index_offset += 8; + + var entry = new Entry + { + Name = name, + Offset = offset, + Size = size, + }; + + if (!entry.CheckPlacement(file.MaxOffset)) + return null; + + dir.Add(entry); + } + + return new ArcFile(file, this, dir); + } + + public override Stream OpenEntry(ArcFile arc, Entry entry) + { + return arc.File.CreateStream(entry.Offset, entry.Size); + } + } +} diff --git a/ArcFormats/Psp/ArcGim.cs b/ArcFormats/Psp/ArcGim.cs new file mode 100644 index 000000000..8e693261a --- /dev/null +++ b/ArcFormats/Psp/ArcGim.cs @@ -0,0 +1,374 @@ +using System; +using System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using GameRes.Utility; + +namespace GameRes.Formats.Psp +{ + internal class GimMetaData : ImageMetaData + { + public int ImageInfoOffset; + public int PaletteInfoOffset; + public int PaletteBlockEnd; + public bool IsLittleEndian; + public int Format; + public int Order; // 0=Linear, 1=Swizzled + public int ImgDataRelOffset; + public int PalDataRelOffset; + public int BufferWidth; // Aligned width + } + + [Export(typeof(ImageFormat))] + public class GimFormat : ImageFormat + { + public override string Tag { get { return "GIM"; } } + public override string Description { get { return "Sony GIM Image"; } } + public override uint Signature { get { return 0; } } + + public override ImageMetaData ReadMetaData(IBinaryStream file) + { + var header = file.ReadBytes(16); + if (header.Length < 16) return null; + + // Header check for Endianness detection + bool littleEndian = true; + if (header[0] == 'M' && header[1] == 'I' && header[2] == 'G') littleEndian = true; + else if (header[0] == 'G' && header[1] == 'I' && header[2] == 'M') littleEndian = false; + else return null; + + // Read the entire stream into an array to facilitate navigation + // GIMs are small, so this is safe and fast. + byte[] data = new byte[file.Length]; + file.Position = 0; + file.Read(data, 0, (int)file.Length); + + int imageInfoOffset = -1; + int paletteInfoOffset = -1; + int paletteBlockEnd = -1; + int offset = 0x10; + int loop = 0; + + // Block scanning loop (Verviewer style) + while (offset + 0x10 <= data.Length && loop < 128) + { + ushort id = ReadUInt16(data, offset, littleEndian); + // 0x02=EndFile, 0x03=EndImage (Skip), 0x04=Image, 0x05=Palette, 0xFF=FileInfo + if (id == 0xFF) break; + + uint size = ReadUInt32(data, offset + 4, littleEndian); + uint next = ReadUInt32(data, offset + 8, littleEndian); + uint headerSize = ReadUInt32(data, offset + 0xC, littleEndian); + + if (size < headerSize || headerSize == 0) break; // Invalid data + + // "Next" logic: If 0, usually the next block is sequential (size) + int nextOffset = (next != 0) ? (int)next : (int)size; + + int blockStart = offset; + int blockEnd = blockStart + (int)size; + int subHeader = blockStart + (int)headerSize; + + if (blockEnd > data.Length) break; + + if (id == 4) // Image Section + { + if (imageInfoOffset < 0) imageInfoOffset = subHeader; + } + else if (id == 5) // Palette Section + { + if (paletteInfoOffset < 0) + { + paletteInfoOffset = subHeader; + paletteBlockEnd = blockEnd; + } + } + + offset = blockStart + nextOffset; + loop++; + } + + if (imageInfoOffset < 0) return null; + + ushort imgFormat = ReadUInt16(data, imageInfoOffset + 4, littleEndian); + ushort pixelOrder = ReadUInt16(data, imageInfoOffset + 6, littleEndian); + ushort width = ReadUInt16(data, imageInfoOffset + 8, littleEndian); + ushort height = ReadUInt16(data, imageInfoOffset + 0xA, littleEndian); + ushort bpp = ReadUInt16(data, imageInfoOffset + 0xC, littleEndian); + uint imgRel = ReadUInt32(data, imageInfoOffset + 0x1C, littleEndian); + uint palRel = 0; + + if (paletteInfoOffset > 0) + palRel = ReadUInt32(data, paletteInfoOffset + 0x1C, littleEndian); + + // Texture Alignment (Buffer Width) + // PSP aligns in blocks of 16 bytes (128 bits) + int align = 16; + int pixelsPerBlock = (align * 8) / Math.Max(1, (int)bpp); + int bufferWidth = (width + pixelsPerBlock - 1) & ~(pixelsPerBlock - 1); + + return new GimMetaData + { + Width = width, + Height = height, + BPP = bpp, + Format = imgFormat, + Order = pixelOrder, + BufferWidth = bufferWidth, + ImageInfoOffset = imageInfoOffset, + PaletteInfoOffset = paletteInfoOffset, + PaletteBlockEnd = paletteBlockEnd, + ImgDataRelOffset = (int)imgRel, + PalDataRelOffset = (int)palRel, + IsLittleEndian = littleEndian + }; + } + + public override ImageData Read(IBinaryStream file, ImageMetaData info) + { + var meta = (GimMetaData)info; + + byte[] data = new byte[file.Length]; + file.Position = 0; + file.Read(data, 0, (int)file.Length); + + int imgDataOffset = meta.ImageInfoOffset + meta.ImgDataRelOffset; + + // Safe pixel buffer reading + int width = (int)meta.Width; + int height = (int)meta.Height; + int bpp = meta.BPP; + int bufferWidth = meta.BufferWidth; + + // Internal PSP Stride (can be larger than image width) + int bufferStride = (bufferWidth * bpp) / 8; + int totalBytes = bufferStride * height; + + if (imgDataOffset + totalBytes > data.Length) + totalBytes = Math.Max(0, data.Length - imgDataOffset); + + byte[] pixels = new byte[totalBytes]; + Buffer.BlockCopy(data, imgDataOffset, pixels, 0, totalBytes); + + // --- UNSWIZZLE --- + if (meta.Order == 1) + { + pixels = UnswizzlePSP(pixels, width, height, bufferWidth, bpp); + } + else + { + // If linear but with lateral padding, remove it + if (bufferWidth != width) + pixels = RemovePadding(pixels, width, height, bufferWidth, bpp); + + // If 4bpp Linear, we still need to fix Nibbles + if (bpp == 4) + SwapNibbles(pixels); + } + + // --- PALETTE --- + BitmapPalette palette = null; + if (meta.Format == 0x04 || meta.Format == 0x05) + { + if (meta.PaletteInfoOffset > 0 && meta.PaletteBlockEnd > 0) + { + int palDataOffset = meta.PaletteInfoOffset + meta.PalDataRelOffset; + ushort palFmt = ReadUInt16(data, meta.PaletteInfoOffset + 4, meta.IsLittleEndian); + int entrySize = (palFmt == 3) ? 4 : 2; + + int palBytes = meta.PaletteBlockEnd - palDataOffset; + int colorCount = palBytes / entrySize; + + // Limit colors by BPP + if (meta.Format == 0x04) colorCount = Math.Min(colorCount, 16); + else if (meta.Format == 0x05) colorCount = Math.Min(colorCount, 256); + + if (palDataOffset + (colorCount * entrySize) <= data.Length) + { + Color[] colors = new Color[colorCount]; + for(int i=0; i= data.Length) return 0; + return littleEndian + ? (ushort)(data[offset] | (data[offset + 1] << 8)) + : (ushort)((data[offset] << 8) | data[offset + 1]); + } + + static uint ReadUInt32(byte[] data, int offset, bool littleEndian) + { + if (offset + 3 >= data.Length) return 0; + return littleEndian + ? (uint)(data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) | (data[offset + 3] << 24)) + : (uint)((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]); + } + + static byte[] UnswizzlePSP(byte[] src, int width, int height, int bufferWidth, int bpp) + { + // 4BPP TRICK: Treat 4BPP as 8BPP with half width for coordinate calculation. + // This moves bytes correctly. THEN we swap nibbles. + int procBpp = bpp; + int procWidth = width; + int procBufferWidth = bufferWidth; + + if (bpp == 4) + { + procBpp = 8; + procWidth = width / 2; + procBufferWidth = bufferWidth / 2; + } + + int blockWidth = 16; + int blockHeight = 8; + + if (procBpp == 16) { blockWidth = 16; blockHeight = 4; } + else if (procBpp == 32) { blockWidth = 8; blockHeight = 4; } + + int dstStride = (procWidth * procBpp) / 8; + byte[] dst = new byte[dstStride * height]; + int bppByte = procBpp / 8; + + for (int y = 0; y < height; y++) + { + for (int x = 0; x < procWidth; x++) + { + int bx = x / blockWidth; + int by = y / blockHeight; + int mx = x % blockWidth; + int my = y % blockHeight; + + int blocksPerRow = procBufferWidth / blockWidth; + int blockSize = blockWidth * blockHeight * bppByte; + int blockIdx = (by * blocksPerRow) + bx; + int srcOffset = (blockIdx * blockSize) + ((my * blockWidth + mx) * bppByte); + int dstOffset = (y * dstStride) + (x * bppByte); + + if (srcOffset + bppByte <= src.Length && dstOffset + bppByte <= dst.Length) + { + // For 4bpp, bppByte is 1 (because we simulate 8bpp). + byte val = src[srcOffset]; + + // 4BPP LINES FIX: + // Unswizzle moved the correct Byte to the correct place. + // Now we need to invert nibbles (High/Low) because WPF reads differently from PSP. + if (bpp == 4) + { + val = (byte)(((val & 0x0F) << 4) | ((val & 0xF0) >> 4)); + } + + dst[dstOffset] = val; + + // For other formats (>8bpp), normal copy + if (bpp > 8) + { + for(int k=1; k> 4)); + } + } + + Color DecodePspColor(ushort v, int fmt) + { + int r=0, g=0, b=0, a=255; + if (fmt == 0) { // 5650 + r=(v&0x1F)<<3; g=((v>>5)&0x3F)<<2; b=((v>>11)&0x1F)<<3; + } else if (fmt == 1) { // 5551 + r=(v&0x1F)<<3; g=((v>>5)&0x1F)<<3; b=((v>>10)&0x1F)<<3; a=(v>>15)!=0?255:0; + } else if (fmt == 2) { // 4444 + r=(v&0xF)<<4; g=((v>>4)&0xF)<<4; b=((v>>8)&0xF)<<4; a=((v>>12)&0xF)<<4; + } + // Swap R and B to fix inverted colors + return Color.FromArgb((byte)a, (byte)b, (byte)g, (byte)r); + } + + byte[] Convert16to32(byte[] inp, int w, int h, int fmt, bool le) + { + byte[] outp = new byte[w * h * 4]; + for(int i=0; i=inp.Length) break; + ushort v = le + ? (ushort)(inp[i*2] | (inp[i*2+1] << 8)) + : (ushort)((inp[i*2] << 8) | inp[i*2+1]); + Color c = DecodePspColor(v, fmt); + int o = i*4; + outp[o]=c.B; outp[o+1]=c.G; outp[o+2]=c.R; outp[o+3]=c.A; + } + return outp; + } + } +} \ No newline at end of file From 2dd7052e020a6dc465d333b9d5d3690401673c77 Mon Sep 17 00:00:00 2001 From: gopicolo Date: Tue, 27 Jan 2026 13:45:25 -0300 Subject: [PATCH 2/3] Fix typo in Guyzware description --- ArcFormats/Guyzware/ArcDat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArcFormats/Guyzware/ArcDat.cs b/ArcFormats/Guyzware/ArcDat.cs index 1747bba0a..6dfd61f14 100644 --- a/ArcFormats/Guyzware/ArcDat.cs +++ b/ArcFormats/Guyzware/ArcDat.cs @@ -10,7 +10,7 @@ namespace GameRes.Formats.Guyzware public class GdpOpener : ArchiveFormat { public override string Tag => "DAT/GDP"; - public override string Description => "Guyzware engine)"; + public override string Description => "Guyzware engine" public override uint Signature => 0; public override bool IsHierarchic => false; public override bool CanWrite => false; From fce112d99bd621d9edba1e1454c56ca856e61316 Mon Sep 17 00:00:00 2001 From: gopicolo Date: Tue, 27 Jan 2026 13:48:13 -0300 Subject: [PATCH 3/3] Fix typo in Guyzware description --- ArcFormats/Guyzware/ArcDat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ArcFormats/Guyzware/ArcDat.cs b/ArcFormats/Guyzware/ArcDat.cs index 6dfd61f14..85c1c8efe 100644 --- a/ArcFormats/Guyzware/ArcDat.cs +++ b/ArcFormats/Guyzware/ArcDat.cs @@ -10,7 +10,7 @@ namespace GameRes.Formats.Guyzware public class GdpOpener : ArchiveFormat { public override string Tag => "DAT/GDP"; - public override string Description => "Guyzware engine" + public override string Description => "Guyzware engine"; public override uint Signature => 0; public override bool IsHierarchic => false; public override bool CanWrite => false;