From 4c6b53aa26185dafe4851ae1379f18fa588ef969 Mon Sep 17 00:00:00 2001 From: Elem8100 <49195287+Elem8100@users.noreply.github.com> Date: Fri, 14 Jan 2022 20:35:16 +0800 Subject: [PATCH] Add files via upload --- WzComparerR2.WzLib/WzComparerR2.WzLib.csproj | 70 ++ WzComparerR2.WzLib/Wz_Crypto.cs | 329 ++++++++ WzComparerR2.WzLib/Wz_Directory.cs | 28 + WzComparerR2.WzLib/Wz_File.cs | 762 ++++++++++++++++++ WzComparerR2.WzLib/Wz_Header.cs | 154 ++++ WzComparerR2.WzLib/Wz_Image.cs | 403 ++++++++++ WzComparerR2.WzLib/Wz_Node.cs | 770 +++++++++++++++++++ WzComparerR2.WzLib/Wz_Png.cs | 562 ++++++++++++++ WzComparerR2.WzLib/Wz_Sound.cs | 196 +++++ WzComparerR2.WzLib/Wz_SoundType.cs | 13 + WzComparerR2.WzLib/Wz_Structure.cs | 269 +++++++ WzComparerR2.WzLib/Wz_Type.cs | 30 + WzComparerR2.WzLib/Wz_Uol.cs | 78 ++ WzComparerR2.WzLib/Wz_Vector.cs | 42 + 14 files changed, 3706 insertions(+) create mode 100644 WzComparerR2.WzLib/WzComparerR2.WzLib.csproj create mode 100644 WzComparerR2.WzLib/Wz_Crypto.cs create mode 100644 WzComparerR2.WzLib/Wz_Directory.cs create mode 100644 WzComparerR2.WzLib/Wz_File.cs create mode 100644 WzComparerR2.WzLib/Wz_Header.cs create mode 100644 WzComparerR2.WzLib/Wz_Image.cs create mode 100644 WzComparerR2.WzLib/Wz_Node.cs create mode 100644 WzComparerR2.WzLib/Wz_Png.cs create mode 100644 WzComparerR2.WzLib/Wz_Sound.cs create mode 100644 WzComparerR2.WzLib/Wz_SoundType.cs create mode 100644 WzComparerR2.WzLib/Wz_Structure.cs create mode 100644 WzComparerR2.WzLib/Wz_Type.cs create mode 100644 WzComparerR2.WzLib/Wz_Uol.cs create mode 100644 WzComparerR2.WzLib/Wz_Vector.cs diff --git a/WzComparerR2.WzLib/WzComparerR2.WzLib.csproj b/WzComparerR2.WzLib/WzComparerR2.WzLib.csproj new file mode 100644 index 0000000..a723bc2 --- /dev/null +++ b/WzComparerR2.WzLib/WzComparerR2.WzLib.csproj @@ -0,0 +1,70 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {0E9801FD-44A2-4AF8-AE91-D6E74BAD56B2} + Library + Properties + WzComparerR2.WzLib + WzComparerR2.WzLib + v4.0 + 512 + + + + true + full + false + bin\Debug\ + TRACE;DEBUG + prompt + 4 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + + + + + + + Properties\CommonAssemblyInfo.cs + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/WzComparerR2.WzLib/Wz_Crypto.cs b/WzComparerR2.WzLib/Wz_Crypto.cs new file mode 100644 index 0000000..f4f0bbc --- /dev/null +++ b/WzComparerR2.WzLib/Wz_Crypto.cs @@ -0,0 +1,329 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Security.Cryptography; +using System.Collections.Specialized; +using System.Text.RegularExpressions; + +namespace WzComparerR2.WzLib +{ + public class Wz_Crypto + { + public Wz_Crypto() + { + this.keys_bms = new Wz_CryptoKey(iv_bms); + this.keys_kms = new Wz_CryptoKey(iv_kms); + this.keys_gms = new Wz_CryptoKey(iv_gms); + this.listwz = false; + this.EncType = Wz_CryptoKeyType.Unknown; + this.List = new StringCollection(); + } + + public void Reset() + { + this.encryption_detected = false; + this.listwz = false; + this.EncType = Wz_CryptoKeyType.Unknown; + this.List.Clear(); + } + + public bool list_contains(string name) + { + bool contains = this.List.Contains(name); + if (contains) + this.List.Remove(name); + return contains; + // foreach (string list_entry in this.list) + // { + // // if (list_entry.Contains(Name)) + // if (list_entry == Name) + // { + // this.list.Remove(list_entry); + // return true; + // } + // } + // return false; + } + + public void LoadListWz(string path) + { + path = Path.Combine(path, "List.wz"); + if (File.Exists(path)) + { + this.listwz = true; + using (FileStream list_file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + BinaryReader listwz = new BinaryReader(list_file); + int length = (int)list_file.Length; + int len = 0; + byte b = 0; + string folder = ""; + list_file.Position += 4; + byte check_for_d = listwz.ReadByte(); + + if ((char)(check_for_d ^ this.keys_gms[0]) == 'd') + { + this.EncType = Wz_CryptoKeyType.GMS; + } + else if ((char)(check_for_d ^ this.keys_kms[0]) == 'd') + { + this.EncType = Wz_CryptoKeyType.KMS; + } + + list_file.Position = 0; + while (list_file.Position < length) + { + len = listwz.ReadInt32() * 2; + for (int i = 0; i < len; i += 2) + { + b = (byte)(listwz.ReadByte() ^ this.keys[i]); + folder += (char)(b); + list_file.Position++; + } + list_file.Position += 2; + folder.Replace(".im/", ".img"); + this.List.Add(folder); + folder = ""; + } + this.List.Remove("dummy"); + } + } + } + + public void DetectEncryption(Wz_File f) + { + int old_off = (int)f.FileStream.Position; + f.FileStream.Position = f.Header.DataStartPosition; + if (f.ReadInt32() <= 0) //只有文件头 无法预判 + { + return; + } + f.FileStream.Position++; + int len = (int)(-f.BReader.ReadSByte()); + byte[] bytes = f.BReader.ReadBytes(len); + + for (int i = 0; i < len; i++) + { + bytes[i] ^= (byte)(0xAA + i); + } + + StringBuilder sb = new StringBuilder(); + if (!this.encryption_detected) + { + //测试bms + sb.Clear(); + for (int i = 0; i < len; i++) + { + sb.Append((char)(keys_bms[i] ^ bytes[i])); + } + if (IsLegalNodeName(sb.ToString())) + { + this.EncType = Wz_CryptoKeyType.BMS; + this.encryption_detected = true; + goto lbl_end; + } + + //测试kms + sb.Clear(); + for (int i = 0; i < len; i++) + { + sb.Append((char)(keys_kms[i] ^ bytes[i])); + } + if (IsLegalNodeName(sb.ToString())) + { + this.EncType = Wz_CryptoKeyType.KMS; + this.encryption_detected = true; + goto lbl_end; + } + + //测试gms + sb.Clear(); + for (int i = 0; i < len; i++) + { + sb.Append((char)(keys_gms[i] ^ bytes[i])); + } + if (IsLegalNodeName(sb.ToString())) + { + this.EncType = Wz_CryptoKeyType.GMS; + this.encryption_detected = true; + goto lbl_end; + } + } + + lbl_end: + f.FileStream.Position = old_off; + } + + public bool IsLegalNodeName(string nodeName) + { + return nodeName.EndsWith(".img") || nodeName.EndsWith(".lua") || Regex.IsMatch(nodeName, @"^[A-Za-z-9_]+$"); + } + + static readonly byte[] iv_gms = { 0x4d, 0x23, 0xc7, 0x2b }; + static readonly byte[] iv_kms = { 0xb9, 0x7d, 0x63, 0xe9 }; + static readonly byte[] iv_bms = { 0x00, 0x00, 0x00, 0x00 }; + + private Wz_CryptoKey keys_bms, keys_gms, keys_kms; + private Wz_CryptoKeyType enc_type; + + public bool encryption_detected = false; + public bool listwz = false; + + public Wz_CryptoKey keys { get; private set; } + public StringCollection List { get; private set; } + + public Wz_CryptoKeyType EncType + { + get { return enc_type; } + set + { + enc_type = value; + switch (enc_type) + { + case Wz_CryptoKeyType.Unknown: + this.keys = null; + break; + + case Wz_CryptoKeyType.BMS: + this.keys = keys_bms; + break; + + case Wz_CryptoKeyType.KMS: + this.keys = keys_kms; + break; + + case Wz_CryptoKeyType.GMS: + this.keys = keys_gms; + break; + } + } + } + + public enum Wz_CryptoKeyType + { + Unknown = 0, + BMS = 1, + KMS = 2, + GMS = 3 + } + + public class Wz_CryptoKey + { + public Wz_CryptoKey(byte[] iv) + { + this.iv = iv; + if (iv == null || BitConverter.ToInt32(iv, 0) == 0) + { + this.isEmptyIV = true; + } + } + + private byte[] keys; + private byte[] iv; + private bool isEmptyIV; + + public byte this[int index] + { + get + { + if (isEmptyIV) + { + return 0; + } + if (keys == null || keys.Length <= index) + { + EnsureKeySize(index + 1); + } + return this.keys[index]; + } + } + + public void EnsureKeySize(int size) + { + if (isEmptyIV) + { + return; + } + if (this.keys != null && this.keys.Length >= size) + { + return; + } + + size = (int)Math.Ceiling(1.0 * size / 4096) * 4096; + int startIndex = 0; + + if (this.keys == null) + { + keys = new byte[size]; + } + else + { + startIndex = this.keys.Length; + Array.Resize(ref this.keys, size); + } + + Rijndael aes = Rijndael.Create(); + aes.KeySize = 256; + aes.BlockSize = 128; + aes.Key = aesKey; + aes.Mode = CipherMode.ECB; + MemoryStream ms = new MemoryStream(keys, startIndex, keys.Length - startIndex, true); + CryptoStream s = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write); + + for (int i = startIndex; i < size; i += 16) + { + if (i == 0) + { + byte[] block = new byte[16]; + for (int j = 0; j < block.Length; j++) + { + block[j] = iv[j % 4]; + } + s.Write(block, 0, block.Length); + } + else + { + s.Write(keys, i - 16, 16); + } + } + + s.Flush(); + ms.Close(); + } + + public unsafe void Decrypt(byte[] buffer, int startIndex, int length) + { + if (isEmptyIV) + return; + + this.EnsureKeySize(length); + + fixed (byte* pBuffer = buffer, pKeys = this.keys) + { + int i = 0; + byte* pData = pBuffer + startIndex; + + for (int i1 = length / 4 * 4; i < i1; i += 4, pData += 4) + { + *((int*)pData) ^= *(int*)(pKeys + i); + } + + for (; i < length; i++, pData++) + { + *pData ^= *(pKeys + i); + } + } + } + + static readonly byte[] aesKey = {0x13, 0x00, 0x00, 0x00, + 0x08, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x00, 0x00, + 0xB4, 0x00, 0x00, 0x00, + 0x1B, 0x00, 0x00, 0x00, + 0x0F, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x00, + 0x52, 0x00, 0x00, 0x00 }; + } + } +} diff --git a/WzComparerR2.WzLib/Wz_Directory.cs b/WzComparerR2.WzLib/Wz_Directory.cs new file mode 100644 index 0000000..1354ace --- /dev/null +++ b/WzComparerR2.WzLib/Wz_Directory.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace WzComparerR2.WzLib +{ + public class Wz_Directory + { + public Wz_Directory(string name, int size, int cs32, uint hashOff, uint hashPos, Wz_File wz_f) + { + this.Name = name; + this.WzFile = wz_f; + this.Size = size; + this.Checksum = cs32; + this.HashedOffset = hashOff; + this.HashedOffsetPosition = hashPos; + } + + public string Name { get; set; } + public Wz_File WzFile { get; set; } + public int Size { get; set; } + public int Checksum { get; set; } + public uint HashedOffset { get; set; } + public uint HashedOffsetPosition { get; set; } + public long Offset { get; set; } + } +} diff --git a/WzComparerR2.WzLib/Wz_File.cs b/WzComparerR2.WzLib/Wz_File.cs new file mode 100644 index 0000000..42b313f --- /dev/null +++ b/WzComparerR2.WzLib/Wz_File.cs @@ -0,0 +1,762 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace WzComparerR2.WzLib +{ + public class Wz_File : IDisposable + { + public Wz_File(string fileName, Wz_Structure wz) + { + this.imageCount = 0; + this.wzStructure = wz; + this.fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + this.bReader = new BinaryReader(this.FileStream); + this.loaded = this.GetHeader(fileName); + this.stringTable = new Dictionary(); + this.directories = new List(); + } + + private FileStream fileStream; + private BinaryReader bReader; + private Wz_Structure wzStructure; + private Wz_Header header; + private Wz_Node node; + private int imageCount; + private bool loaded; + private bool isSubDir; + private Wz_Type type; + private List mergedWzFiles; + private Wz_File ownerWzFile; + private readonly List directories; + + public Encoding TextEncoding { get; set; } + + public readonly object ReadLock = new object(); + + internal Dictionary stringTable; + internal byte[] tempBuffer; + + public FileStream FileStream + { + get { return fileStream; } + } + + public BinaryReader BReader + { + get { return bReader; } + } + + public Wz_Structure WzStructure + { + get { return wzStructure; } + set { wzStructure = value; } + } + + public Wz_Header Header + { + get { return header; } + private set { header = value; } + } + + public Wz_Node Node + { + get { return node; } + set { node = value; } + } + + public int ImageCount + { + get { return imageCount; } + } + + public bool Loaded + { + get { return loaded; } + } + + public bool IsSubDir + { + get { return this.isSubDir; } + } + + public Wz_Type Type + { + get { return type; } + set { type = value; } + } + + public IEnumerable MergedWzFiles + { + get { return this.mergedWzFiles ?? Enumerable.Empty(); } + } + + public Wz_File OwnerWzFile + { + get { return this.ownerWzFile; } + } + + public void Close() + { + if (this.bReader != null) + this.bReader.Close(); + if (this.fileStream != null) + this.fileStream.Close(); + } + + void IDisposable.Dispose() + { + this.Close(); + } + + private bool GetHeader(string fileName) + { + this.fileStream.Position = 0; + long filesize = this.FileStream.Length; + if (filesize < 4) { goto __failed; } + + string signature = new string(this.BReader.ReadChars(4)); + if (signature != "PKG1") { goto __failed; } + + long dataSize = this.BReader.ReadInt64(); + int headerSize = this.BReader.ReadInt32(); + string copyright = new string(this.BReader.ReadChars(headerSize - (int)this.FileStream.Position)); + + // encver detecting: + // Since KMST1132, wz removed the 2 bytes encver, and use a fixed wzver '777'. + // Here we try to read the first 2 bytes from data part and guess if it looks like an encver. + bool encverMissing = false; + int encver = -1; + if (dataSize >= 2) + { + this.fileStream.Position = headerSize; + encver = this.BReader.ReadUInt16(); + // encver always less than 256 + if (encver > 0xff) + { + encverMissing = true; + } + else if (encver == 0x80) + { + // there's an exceptional case that the first field of data part is a compressed int which determined property count, + // if the value greater than 127 and also to be a multiple of 256, the first 5 bytes will become to + // 80 00 xx xx xx + // so we additional check the int value, at most time the child node count in a wz won't greater than 65536. + if (dataSize >= 5) + { + this.fileStream.Position = headerSize; + int propCount = this.ReadInt32(); + if (propCount > 0 && (propCount & 0xff) == 0 && propCount <= 0xffff) + { + encverMissing = true; + } + } + } + } + else + { + // Obviously, if data part have only 1 byte, encver must be deleted. + encverMissing = true; + } + + int dataStartPos = headerSize + (encverMissing ? 0 : 2); + this.Header = new Wz_Header(signature, copyright, fileName, headerSize, dataSize, filesize, dataStartPos); + + if (encverMissing) + { + // not sure if nexon will change this magic version, just hard coded. + this.Header.SetWzVersion(777); + this.Header.VersionChecked = true; + } + else + { + this.Header.SetOrdinalVersionDetector(encver); + } + + return true; + + __failed: + this.header = new Wz_Header(null, null, fileName, 0, 0, filesize, 0); + return false; + } + + public int ReadInt32() + { + int s = this.BReader.ReadSByte(); + return (s == -128) ? this.BReader.ReadInt32() : s; + } + + public long ReadInt64() + { + int s = this.BReader.ReadSByte(); + return (s == -128) ? this.BReader.ReadInt64() : s; + } + + public float ReadSingle() + { + float fl = this.BReader.ReadSByte(); + return (fl == -128) ? this.BReader.ReadSingle() : fl; + } + + public string ReadString(long offset) + { + byte b = this.BReader.ReadByte(); + switch (b) + { + case 0x00: + case 0x73: + return ReadString(); + + case 0x01: + case 0x1B: + return ReadStringAt(offset + this.BReader.ReadInt32()); + + case 0x04: + this.FileStream.Position += 8; + break; + + default: + throw new Exception("문자열을 읽는 중 오류: " + this.FileStream.Name + " " + this.FileStream.Position); + } + return string.Empty; + } + + public string ReadStringAt(long offset) + { + long oldoffset = this.FileStream.Position; + string str; + if (!stringTable.TryGetValue(offset, out str)) + { + this.FileStream.Position = offset; + str = ReadString(); + stringTable[offset] = str; + this.FileStream.Position = oldoffset; + } + return str; + } + + public unsafe string ReadString() + { + int size = this.BReader.ReadSByte(); + string result = null; + if (size < 0) + { + byte mask = 0xAA; + size = (size == -128) ? this.BReader.ReadInt32() : -size; + + var buffer = GetStringBuffer(size); + this.fileStream.Read(buffer, 0, size); + this.WzStructure.encryption.keys.Decrypt(buffer, 0, size); + + fixed (byte* pData = buffer) + { + for (int i = 0; i < size; i++) + { + pData[i] ^= mask; + unchecked { mask++; } + } + + var enc = this.TextEncoding ?? Encoding.Default; + result = enc.GetString(buffer, 0, size); + } + } + else if (size > 0) + { + ushort mask = 0xAAAA; + if (size == 127) + { + size = this.BReader.ReadInt32(); + } + + var buffer = GetStringBuffer(size * 2); + this.fileStream.Read(buffer, 0, size * 2); + this.WzStructure.encryption.keys.Decrypt(buffer, 0, size * 2); + + fixed (byte* pData = buffer) + { + ushort* pChar = (ushort*)pData; + for (int i = 0; i < size; i++) + { + pChar[i] ^= mask; + unchecked { mask++; } + } + + result = new string((char*)pChar, 0, size); + } + } + else + { + return string.Empty; + } + + //memory optimize + if (result.Length <= 4) + { + for (int i = 0; i < result.Length; i++) + { + if (result[i] >= 0x80) + { + return result; + } + } + return string.Intern(result); + } + else + { + return result; + } + } + + /// + /// 为字符串解密提供缓冲区。 + /// + /// + /// + private byte[] GetStringBuffer(int size) + { + if (size <= 4096) + { + if (tempBuffer == null || tempBuffer.Length < size) + { + Array.Resize(ref tempBuffer, size); + } + return tempBuffer; + } + else + { + return new byte[size]; + } + } + + public uint CalcOffset(uint filePos, uint hashedOffset) + { + uint offset = (uint)(filePos - 0x3C) ^ 0xFFFFFFFF; + int distance; + + offset *= this.Header.HashVersion; + offset -= 0x581C3F6D; + distance = (int)offset & 0x1F; + offset = (offset << distance) | (offset >> (32 - distance)); + offset ^= hashedOffset; + offset += 0x78; + + return offset; + } + + public void GetDirTree(Wz_Node parent, bool useBaseWz = false, bool loadWzAsFolder = false) + { + List dirs = new List(); + string name = null; + int size = 0; + int cs32 = 0; + uint pos = 0, hashOffset = 0; + //int offs = 0; + + int count = ReadInt32(); + + for (int i = 0; i < count; i++) + { + switch ((int)this.BReader.ReadByte()) + { + case 0x02: + foreach (Wz_Crypto.Wz_CryptoKeyType encType in new[] { Wz_Crypto.Wz_CryptoKeyType.BMS, Wz_Crypto.Wz_CryptoKeyType.KMS, Wz_Crypto.Wz_CryptoKeyType.GMS, this.WzStructure.encryption.EncType }) + { + long oldoffset = this.FileStream.Position; + int stroffset = this.Header.HeaderSize + 1 + this.BReader.ReadInt32(); + name = this.ReadStringAt(stroffset); + if (this.WzStructure.encryption.IsLegalNodeName(name)) + { + break; + } + this.FileStream.Position = oldoffset; + stringTable.Remove(stroffset); + this.WzStructure.encryption.EncType = encType; + } + goto case 0xffff; + case 0x04: + foreach (Wz_Crypto.Wz_CryptoKeyType encType in new[] { Wz_Crypto.Wz_CryptoKeyType.BMS, Wz_Crypto.Wz_CryptoKeyType.KMS, Wz_Crypto.Wz_CryptoKeyType.GMS, this.WzStructure.encryption.EncType }) + { + long oldoffset = this.FileStream.Position; + name = this.ReadString(); + if (this.WzStructure.encryption.IsLegalNodeName(name)) + { + break; + } + this.FileStream.Position = oldoffset; + this.WzStructure.encryption.EncType = encType; + } + goto case 0xffff; + + case 0xffff: + size = this.ReadInt32(); + cs32 = this.ReadInt32(); + pos = (uint)this.bReader.BaseStream.Position; + hashOffset = this.bReader.ReadUInt32(); + + Wz_Image img = new Wz_Image(name, size, cs32, hashOffset, pos, this); + Wz_Node childNode = parent.Nodes.Add(name); + childNode.Value = img; + img.OwnerNode = childNode; + + this.imageCount++; + break; + + case 0x03: + name = this.ReadString(); + size = this.ReadInt32(); + cs32 = this.ReadInt32(); + pos = (uint)this.bReader.BaseStream.Position; + hashOffset = this.bReader.ReadUInt32(); + this.directories.Add(new Wz_Directory(name, size, cs32, hashOffset, pos, this)); + dirs.Add(name); + break; + } + } + + int dirCount = dirs.Count; + bool willLoadBaseWz = useBaseWz ? parent.Text.Equals("base.wz", StringComparison.OrdinalIgnoreCase) : false; + + var baseFolder = Path.GetDirectoryName(this.header.FileName); + + if (willLoadBaseWz && this.WzStructure.AutoDetectExtFiles) + { + for (int i = 0; i < dirCount; i++) + { + //检测文件名 + var m = Regex.Match(dirs[i], @"^([A-Za-z]+)$"); + if (m.Success) + { + string wzTypeName = m.Result("$1"); + + //检测扩展wz文件 + for (int fileID = 2; ; fileID++) + { + string extDirName = wzTypeName + fileID; + string extWzFile = Path.Combine(baseFolder, extDirName + ".wz"); + if (File.Exists(extWzFile)) + { + if (!dirs.Take(dirCount).Any(dir => extDirName.Equals(dir, StringComparison.OrdinalIgnoreCase))) + { + dirs.Add(extDirName); + } + } + else + { + break; + } + } + //检测KMST1058的wz文件 + for (int fileID = 1; ; fileID++) + { + string extDirName = wzTypeName + fileID.ToString("D3"); + string extWzFile = Path.Combine(baseFolder, extDirName + ".wz"); + if (File.Exists(extWzFile)) + { + if (!dirs.Take(dirCount).Any(dir => extDirName.Equals(dir, StringComparison.OrdinalIgnoreCase))) + { + dirs.Add(extDirName); + } + } + else + { + break; + } + } + } + } + } + + for (int i = 0; i < dirs.Count; i++) + { + string dir = dirs[i]; + Wz_Node t = parent.Nodes.Add(dir); + if (i < dirCount) + { + GetDirTree(t, false); + } + + if (t.Nodes.Count == 0) + { + this.WzStructure.has_basewz |= willLoadBaseWz; + + try + { + if (loadWzAsFolder) + { + string wzFolder = willLoadBaseWz ? Path.Combine(Path.GetDirectoryName(baseFolder), dir) : Path.Combine(baseFolder, dir); + if (Directory.Exists(wzFolder)) + { + this.wzStructure.LoadWzFolder(wzFolder, ref t, false); + if (!willLoadBaseWz) + { + var dirWzFile = t.GetValue(); + dirWzFile.Type = Wz_Type.Unknown; + dirWzFile.isSubDir = true; + } + } + } + else if (willLoadBaseWz) + { + string filePath = Path.Combine(baseFolder, dir + ".wz"); + if (File.Exists(filePath)) + { + this.WzStructure.LoadFile(filePath, t, false, loadWzAsFolder); + } + } + } + catch (Exception ex) + { + } + } + } + + parent.Nodes.Trim(); + } + + private string getFullPath(Wz_Node parent, string name) + { + List path = new List(5); + path.Add(name.ToLower()); + while (parent != null && !(parent.Value is Wz_File)) + { + path.Insert(0, parent.Text.ToLower()); + parent = parent.ParentNode; + } + if (parent != null) + { + path.Insert(0, parent.Text.ToLower().Replace(".wz", "")); + } + return string.Join("/", path.ToArray()); + } + + public void DetectWzType() + { + this.type = Wz_Type.Unknown; + if (this.node == null) + { + return; + } + + if (this.node.Nodes["smap.img"] != null + || this.node.Nodes["zmap.img"] != null) + { + this.type = Wz_Type.Base; + } + else if (this.node.Nodes["00002000.img"] != null + || this.node.Nodes["Accessory"] != null + || this.node.Nodes["Weapon"] != null) + { + this.type = Wz_Type.Character; + } + else if (this.node.Nodes["BasicEff.img"] != null + || this.node.Nodes["SetItemInfoEff.img"] != null) + { + this.type = Wz_Type.Effect; + } + else if (this.node.Nodes["Commodity.img"] != null + || this.node.Nodes["Curse.img"] != null) + { + this.type = Wz_Type.Etc; + } + else if (this.node.Nodes["Cash"] != null + || this.node.Nodes["Consume"] != null) + { + this.type = Wz_Type.Item; + } + else if (this.node.Nodes["Back"] != null + || this.node.Nodes["Obj"] != null + || this.node.Nodes["Physics.img"] != null) + { + this.type = Wz_Type.Map; + } + else if (this.node.Nodes["PQuest.img"] != null + || this.node.Nodes["QuestData"] != null) + { + this.type = Wz_Type.Quest; + } + else if (this.node.Nodes["Attacktype.img"] != null + || this.node.Nodes["Recipe_9200.img"] != null) + { + this.type = Wz_Type.Skill; + } + else if (this.node.Nodes["Bgm00.img"] != null + || this.node.Nodes["BgmUI.img"] != null) + { + this.type = Wz_Type.Sound; + } + else if (this.node.Nodes["MonsterBook.img"] != null + || this.node.Nodes["EULA.img"] != null) + { + this.type = Wz_Type.String; + } + else if (this.node.Nodes["CashShop.img"] != null + || this.node.Nodes["UIWindow.img"] != null) + { + this.type = Wz_Type.UI; + } + + if (this.type == Wz_Type.Unknown) //用文件名来判断 + { + string wzName = this.node.Text; + + Match m = Regex.Match(wzName, @"^([A-Za-z]+)_?(\d+)?(?:\.wz)?$"); + if (m.Success) + { + wzName = m.Result("$1"); + } + this.type = Enum.TryParse(wzName, true, out var result) ? result : Wz_Type.Unknown; + } + } + + public void DetectWzVersion() + { + bool DetectWithWzImage(Wz_Image testWzImg) + { + while (this.header.TryGetNextVersion()) + { + uint offs = CalcOffset(testWzImg.HashedOffsetPosition, testWzImg.HashedOffset); + + if (offs < this.header.HeaderSize || offs + testWzImg.Size > this.fileStream.Length) //img块越界 + { + continue; + } + + this.fileStream.Position = offs; + switch (this.fileStream.ReadByte()) + { + case 0x73: + case 0x1b: + //试读img第一个string + break; + default: + continue; + } + + testWzImg.Offset = offs; + if (testWzImg.TryExtract()) //试读成功 + { + testWzImg.Unextract(); + this.header.VersionChecked = true; + break; + } + } + return this.header.VersionChecked; + } + + bool DetectWithAllWzDir() + { + while (this.header.TryGetNextVersion()) + { + bool isSuccess = true; + foreach (var testDir in this.directories) + { + uint offs = CalcOffset(testDir.HashedOffsetPosition, testDir.HashedOffset); + + if (offs < this.header.HeaderSize || offs + 1 > this.fileStream.Length) // dir offset out of file size. + { + isSuccess = false; + break; + } + + this.fileStream.Position = offs; + if (this.fileStream.ReadByte() != 0) // dir data only contains one byte: 0x00 + { + isSuccess = false; + break; + } + } + + if (isSuccess) + { + this.header.VersionChecked = true; + break; + } + } + + return this.header.VersionChecked; + } + + List imgList = EnumerableAllWzImage(this.node).Where(_img => _img.WzFile == this).ToList(); + + if (this.header.VersionChecked) + { + foreach (var img in imgList) + { + img.Offset = CalcOffset(img.HashedOffsetPosition, img.HashedOffset); + } + } + else + { + //选择最小的img作为实验品 + Wz_Image minSizeImg = imgList.Where(_img => _img.Size >= 20).DefaultIfEmpty().Aggregate((_img1, _img2) => _img1.Size < _img2.Size ? _img1 : _img2); + + if (minSizeImg == null && imgList.Count > 0) + { + minSizeImg = imgList[0]; + } + + if (minSizeImg != null) + { + DetectWithWzImage(minSizeImg); + } + else if (this.directories.Count > 0) + { + DetectWithAllWzDir(); + } + + if (this.header.VersionChecked) //重新计算全部img + { + foreach (var img in imgList) + { + img.Offset = CalcOffset(img.HashedOffsetPosition, img.HashedOffset); + } + } + else //最终测试失败 那就失败吧.. + { + this.header.VersionChecked = true; + } + } + } + + public void MergeWzFile(Wz_File wz_File) + { + var children = wz_File.node.Nodes.ToList(); + wz_File.node.Nodes.Clear(); + foreach (var child in children) + { + this.node.Nodes.Add(child); + } + + if (this.mergedWzFiles == null) + { + this.mergedWzFiles = new List(); + } + this.mergedWzFiles.Add(wz_File); + + wz_File.ownerWzFile = this; + } + + private IEnumerable EnumerableAllWzImage(Wz_Node parentNode) + { + foreach (var node in parentNode.Nodes) + { + Wz_Image img = node.Value as Wz_Image; + if (img != null) + { + yield return img; + } + + if (!(node.Value is Wz_File) && node.Nodes.Count > 0) + { + foreach (var imgChild in EnumerableAllWzImage(node)) + { + yield return imgChild; + } + } + } + } + } +} diff --git a/WzComparerR2.WzLib/Wz_Header.cs b/WzComparerR2.WzLib/Wz_Header.cs new file mode 100644 index 0000000..8e0ada8 --- /dev/null +++ b/WzComparerR2.WzLib/Wz_Header.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WzComparerR2.WzLib +{ + public class Wz_Header + { + public Wz_Header(string signature, string copyright, string file_name, int head_size, long data_size, long file_size, long dataStartPosition) + { + this.Signature = signature; + this.Copyright = copyright; + this.FileName = file_name; + this.HeaderSize = head_size; + this.DataSize = data_size; + this.FileSize = file_size; + this.DataStartPosition = dataStartPosition; + this.VersionChecked = false; + } + + public string Signature { get; private set; } + public string Copyright { get; private set; } + public string FileName { get; private set; } + + public int HeaderSize { get; private set; } + public long DataSize { get; private set; } + public long FileSize { get; private set; } + public long DataStartPosition { get; private set; } + + public bool VersionChecked { get; set; } + + public int WzVersion => this.versionDetector?.WzVersion ?? 0; + public uint HashVersion => this.versionDetector?.HashVersion ?? 0; + public bool TryGetNextVersion() => this.versionDetector?.TryGetNextVersion() ?? false; + + private IWzVersionDetector versionDetector; + + public void SetWzVersion(int wzVersion) + { + this.versionDetector = new FixedVersion(wzVersion); + } + + public void SetOrdinalVersionDetector(int encryptedVersion) + { + this.versionDetector = new OrdinalVersionDetector(encryptedVersion); + } + + public static int CalcHashVersion(int wzVersion) + { + int sum = 0; + string versionStr = wzVersion.ToString(System.Globalization.CultureInfo.InvariantCulture); + for (int j = 0; j < versionStr.Length; j++) + { + sum <<= 5; + sum += (int)versionStr[j] + 1; + } + + return sum; + } + + public interface IWzVersionDetector + { + bool TryGetNextVersion(); + int WzVersion { get; } + uint HashVersion { get; } + } + + public class FixedVersion : IWzVersionDetector + { + public FixedVersion(int wzVersion) + { + this.WzVersion = wzVersion; + this.HashVersion = (uint)CalcHashVersion(wzVersion); + } + + private bool hasReturned; + + + public int WzVersion { get; private set; } + + public uint HashVersion { get; private set; } + + public bool TryGetNextVersion() + { + if (!hasReturned) + { + hasReturned = true; + return true; + } + + return false; + } + } + + public class OrdinalVersionDetector : IWzVersionDetector + { + public OrdinalVersionDetector(int encryptVersion) + { + this.EncryptedVersion = encryptVersion; + this.versionTest = new List(); + this.hasVersionTest = new List(); + this.startVersion = -1; + } + + public int EncryptedVersion { get; private set; } + + private int startVersion; + private List versionTest; + private List hasVersionTest; + + public int WzVersion + { + get + { + int idx = this.versionTest.Count - 1; + return idx > -1 ? this.versionTest[idx] : 0; + } + } + + public uint HashVersion + { + get + { + int idx = this.hasVersionTest.Count - 1; + return idx > -1 ? this.hasVersionTest[idx] : 0; + } + } + + public bool TryGetNextVersion() + { + for (int i = startVersion + 1; i < Int16.MaxValue; i++) + { + int sum = CalcHashVersion(i); + int enc = 0xff + ^ ((sum >> 24) & 0xFF) + ^ ((sum >> 16) & 0xFF) + ^ ((sum >> 8) & 0xFF) + ^ ((sum) & 0xFF); + + // if encver does not passed, try every version one by one + if (enc == this.EncryptedVersion) + { + this.versionTest.Add(i); + this.hasVersionTest.Add((uint)sum); + startVersion = i; + return true; + } + } + + return false; + } + } + } +} diff --git a/WzComparerR2.WzLib/Wz_Image.cs b/WzComparerR2.WzLib/Wz_Image.cs new file mode 100644 index 0000000..757b16d --- /dev/null +++ b/WzComparerR2.WzLib/Wz_Image.cs @@ -0,0 +1,403 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; + +namespace WzComparerR2.WzLib +{ + public class Wz_Image + { + public Wz_Image(string name, int size, int cs32, uint hashOff, uint hashPos, Wz_File wz_f) + { + this.Name = name; + this.WzFile = wz_f; + this.Size = size; + this.Checksum = cs32; + this.HashedOffset = hashOff; + this.HashedOffsetPosition = hashPos; + this.Node = new Wz_ImageNode(name, this); + this.Encryption = null; + + this.extr = false; + this.chec = false; + } + + private bool extr; + private bool chec; + + public string Name { get; set; } + public Wz_File WzFile { get; set; } + public int Size { get; set; } + public int Checksum { get; set; } + public uint HashedOffset { get; set; } + public uint HashedOffsetPosition { get; set; } + public long Offset { get; set; } + + public Wz_Node Node { get; private set; } + + public Wz_Node OwnerNode { get; set; } + + public Wz_Crypto Encryption { get; private set; } + + public bool IsChecksumChecked + { + get { return this.chec; } + internal set { this.chec = value; } + } + public bool IsLuaImage + { + get { return this.Name.EndsWith(".lua"); } + } + + public bool TryExtract() + { + Exception ex; + return TryExtract(out ex); + } + + public bool TryExtract(out Exception e) + { + if (!this.extr) + { + bool disabledChec = this.WzFile?.WzStructure?.ImgCheckDisabled ?? false; + if (!disabledChec && !this.chec) + { + if (this.Checksum != CalcCheckSum()) + { + e = new ArgumentException("checksum error"); + return false; + } + this.chec = true; + } + if (this.Encryption == null) + { + if (!this.IsLuaImage) + { + try + { + this.TryDetectEnc(); + if (this.Encryption == null) + { + e = null; + return false; + } + } + catch (Exception ex) + { + e = ex; + this.Unextract(); + return false; + } + } + } + + try + { + lock (this.WzFile.ReadLock) + { + this.WzFile.FileStream.Position = this.Offset; + if (!this.IsLuaImage) + { + ExtractImg(this.Offset, this.Node, 0); + this.WzFile.stringTable.Clear(); + } + else + { + ExtractLua(); + } + this.extr = true; + } + } + catch (Exception ex) + { + e = ex; + this.Unextract(); + return false; + } + } + e = null; + return true; + } + + public void Unextract() + { + this.extr = false; + this.Node.Nodes.Clear(); + } + + public unsafe int CalcCheckSum() + { + lock (this.WzFile.ReadLock) + { + this.WzFile.FileStream.Position = this.Offset; + int cs = 0; + byte[] buffer = new byte[4096]; + int count; + int size = this.Size; + while ((count = this.WzFile.FileStream.Read(buffer, 0, Math.Min(size, buffer.Length))) > 0) + { + fixed (byte* pBuffer = buffer) + { + int* p = (int*)pBuffer; + int i, j = count / 4; + for (i = 0; i < j; i++) + { + int data = *(p + i); + cs += (data & 0xff) + (data >> 8 & 0xff) + (data >> 16 & 0xff) + (data >> 24 & 0xff); + } + for (i = i * 4; i < count; i++) + { + cs += buffer[i]; + } + } + + size -= count; + } + return cs; + } + } + + private void ExtractImg(long offset, Wz_Node parent, long eob) + { + int entries = 0; + string tag = this.WzFile.ReadString(offset); + switch (tag) + { + case "Property": + this.WzFile.FileStream.Position += 2; + entries = this.WzFile.ReadInt32(); + for (int i = 0; i < entries; i++) + { + ExtractValue(offset, parent); + } + break; + + case "Shape2D#Vector2D": + parent.Value = new Wz_Vector(this.WzFile.ReadInt32(), this.WzFile.ReadInt32()); + break; + + case "Canvas": + this.WzFile.FileStream.Position++; + if (this.WzFile.BReader.ReadByte() == 0x01) + { + this.WzFile.FileStream.Position += 2; + entries = this.WzFile.ReadInt32(); + for (int i = 0; i < entries; i++) + { + ExtractValue(offset, parent); + } + } + int w = this.WzFile.ReadInt32(); + int h = this.WzFile.ReadInt32(); + int form = this.WzFile.ReadInt32() + this.WzFile.BReader.ReadByte(); + this.WzFile.FileStream.Position += 4; + int bufsize = this.WzFile.BReader.ReadInt32(); + parent.Value = new Wz_Png(w, h, bufsize - 1, form, (uint)this.WzFile.FileStream.Position + 1, this); + this.WzFile.FileStream.Position += bufsize; + break; + + case "Shape2D#Convex2D": + entries = this.WzFile.ReadInt32(); + for (int i = 0; i < entries; i++) + { + ExtractImg(offset, parent, 0); + } + break; + + case "Sound_DX8": + this.WzFile.FileStream.Position++; + int len = this.WzFile.ReadInt32(); + int ms = this.WzFile.ReadInt32(); + int headerLen = (int)(eob - len - this.WzFile.FileStream.Position); + byte[] header = this.WzFile.BReader.ReadBytes(headerLen); + parent.Value = new Wz_Sound((uint)(eob - len), len, header, ms, this); + this.WzFile.FileStream.Position = eob; + break; + + case "UOL": + this.WzFile.FileStream.Position++; + parent.Value = new Wz_Uol(this.WzFile.ReadString(offset)); + break; + + default: + throw new Exception("unknown wz tag: " + tag); + } + } + + private void TryDetectEnc() + { + Wz_Crypto crypto = this.WzFile.WzStructure.encryption; + + if (crypto.EncType != Wz_Crypto.Wz_CryptoKeyType.Unknown) + { + if (IsIllegalTag()) + { + this.Encryption = new Wz_Crypto() { EncType = crypto.EncType }; + return; + } + } + var oldenc = crypto.EncType; + crypto.EncType = Wz_Crypto.Wz_CryptoKeyType.KMS; + if (IsIllegalTag()) + { + this.Encryption = new Wz_Crypto() { EncType = crypto.EncType }; + return; + } + + crypto.EncType = Wz_Crypto.Wz_CryptoKeyType.GMS; + if (IsIllegalTag()) + { + this.Encryption = new Wz_Crypto() { EncType = crypto.EncType }; + return; + } + + crypto.EncType = Wz_Crypto.Wz_CryptoKeyType.BMS; + if (IsIllegalTag()) + { + this.Encryption = new Wz_Crypto() { EncType = crypto.EncType }; + return; + } + + crypto.EncType = oldenc; + } + + private bool IsIllegalTag() + { + this.WzFile.FileStream.Position = this.Offset; + this.WzFile.stringTable.Remove(Offset); + switch (this.WzFile.ReadString(Offset)) + { + case "Property": + case "Shape2D#Vector2D": + case "Canvas": + case "Shape2D#Convex2D": + case "Sound_DX8": + case "UOL": + return true; + default: + return false; + } + } + + + private void ExtractValue(long offset, Wz_Node parent) + { + parent = parent.Nodes.Add(this.WzFile.ReadString(offset)); + byte flag = this.WzFile.BReader.ReadByte(); + switch (flag) + { + case 0x00: + parent.Value = null; + break; + + case 0x02: + case 0x0B: + parent.Value = this.WzFile.BReader.ReadInt16(); + break; + + case 0x03: + case 0x13: + // case 0x14: + parent.Value = this.WzFile.ReadInt32(); + break; + + case 0x14: + parent.Value = this.WzFile.ReadInt64(); + break; + + case 0x04: + parent.Value = this.WzFile.ReadSingle(); + break; + + case 0x05: + parent.Value = this.WzFile.BReader.ReadDouble(); + break; + + case 0x08: + parent.Value = this.WzFile.ReadString(offset); + break; + + case 0x09: + ExtractImg(offset, parent, this.WzFile.BReader.ReadInt32() + this.WzFile.FileStream.Position); + break; + + default: + throw new Exception("读取值错误." + flag + " at Offset: " + this.WzFile.FileStream.Position); + } + } + + private void ExtractLua() + { + while(this.WzFile.FileStream.Position < this.Offset + this.Size) + { + var flag = this.WzFile.BReader.ReadByte(); + + switch (flag) + { + case 0x01: + ExtractLuaValue(this.Node); + break; + + default: + throw new Exception("读取Lua错误." + flag + " at Offset: " + this.WzFile.FileStream.Position); + } + } + } + + private void ExtractLuaValue(Wz_Node parent) + { + int len = this.WzFile.ReadInt32(); + byte[] data = this.WzFile.BReader.ReadBytes(len); + if (this.Encryption == null) + { + TryDetectLuaEnc(data); + } + this.Encryption.keys.Decrypt(data, 0, data.Length); + string luaCode = Encoding.UTF8.GetString(data); + parent.Value = luaCode; + } + + private void TryDetectLuaEnc(byte[] luaBinary) + { + Wz_Crypto crypto = this.WzFile.WzStructure.encryption; + byte[] tempBuffer = new byte[Math.Min(luaBinary.Length, 64)]; + char[] tempStr = new char[tempBuffer.Length]; + + //测试各种加密方式 判断符合度最高的 + int maxCharCount = 0; + var maxCharEnc = Wz_Crypto.Wz_CryptoKeyType.Unknown; + + foreach (var enc in new[] { + Wz_Crypto.Wz_CryptoKeyType.GMS, + Wz_Crypto.Wz_CryptoKeyType.KMS, + Wz_Crypto.Wz_CryptoKeyType.BMS + }) + { + Buffer.BlockCopy(luaBinary, 0, tempBuffer, 0, tempBuffer.Length); + crypto.EncType = enc; + crypto.keys.Decrypt(tempBuffer, 0, tempBuffer.Length); + int count = Encoding.UTF8.GetChars(tempBuffer, 0, tempBuffer.Length, tempStr, 0); + int asciiCount = tempStr.Take(count).Count(chr => 32 <= chr && chr <= 127); + + if (maxCharCount < asciiCount) + { + maxCharEnc = enc; + maxCharCount = asciiCount; + } + } + + crypto.EncType = maxCharEnc; + this.Encryption = new Wz_Crypto() { EncType = crypto.EncType }; + } + + internal class Wz_ImageNode : Wz_Node + { + public Wz_ImageNode(string nodeText, Wz_Image image) : base(nodeText) + { + this.Image = image; + } + + public Wz_Image Image { get; private set; } + } + } +} diff --git a/WzComparerR2.WzLib/Wz_Node.cs b/WzComparerR2.WzLib/Wz_Node.cs new file mode 100644 index 0000000..b3170d4 --- /dev/null +++ b/WzComparerR2.WzLib/Wz_Node.cs @@ -0,0 +1,770 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Runtime.InteropServices; +using System.Xml; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace WzComparerR2.WzLib +{ + public class Wz_Node : ICloneable, IComparable, IComparable + { + public Wz_Node() + { + this.nodes = new WzNodeCollection(this); + } + + public Wz_Node(string nodeText) + : this() + { + this.text = nodeText; + } + + //fields + private object value; + private string text; + private WzNodeCollection nodes; + private Wz_Node parentNode; + + //properties + public object Value + { + get { return value; } + set { this.value = value; } + } + + public string Text + { + get { return this.text; } + set { this.text = value; } + } + + public string FullPath + { + get + { + Stack path = new Stack(); + Wz_Node node = this; + do + { + path.Push(node.text); + node = node.parentNode; + } while (node != null); + return string.Join("\\", path.ToArray()); + } + } + + public string FullPathToFile + { + get + { + Stack path = new Stack(); + Wz_Node node = this; + do + { + if (node.value is Wz_File wzf && !wzf.IsSubDir) + { + if (node.text.EndsWith(".wz", StringComparison.OrdinalIgnoreCase)) + { + path.Push(node.text.Substring(0, node.text.Length - 3)); + } + else + { + path.Push(node.text); + } + break; + } + + path.Push(node.text); + + var img = node.GetValue(); + if (img != null) + { + node = img.OwnerNode; + } + + if (node != null) + { + node = node.parentNode; + } + } while (node != null); + return string.Join("\\", path.ToArray()); + } + } + + public WzNodeCollection Nodes + { + get { return this.nodes; } + } + + public Wz_Node ParentNode + { + get { return parentNode; } + private set { parentNode = value; } + } + + //methods + public override string ToString() + { + return this.Text + " " + (this.value != null ? this.value.ToString() : "-") + " " + this.nodes.Count; + } + + public Wz_Node FindNodeByPath(string fullPath) + { + return FindNodeByPath(fullPath, false); + } + + public Wz_Node FindNodeByPath(string fullPath, bool extractImage) + { + string[] patten = fullPath.Split('\\'); + return FindNodeByPath(extractImage, patten); + } + + public Wz_Node FindNodeByPath(bool extractImage, params string[] fullPath) + { + return FindNodeByPath(extractImage, false, fullPath); + } + + public Wz_Node FindNodeByPath(bool extractImage, bool ignoreCase, params string[] fullPath) + { + Wz_Node node = this; + + Wz_Image img; + + //首次解压 + if (extractImage && (img = this.GetValue()) != null) + { + if (img.TryExtract()) + { + node = img.Node; + } + } + + foreach (string txt in fullPath) + { + if (ignoreCase) + { + bool find = false; + + foreach (Wz_Node subNode in node.nodes) + { + if (string.Equals(subNode.text, txt, StringComparison.OrdinalIgnoreCase)) + { + find = true; + node = subNode; + } + } + if (!find) + node = null; + } + else + { + node = node.nodes[txt]; + } + + if (node == null) + return null; + + if (extractImage) + { + img = node.GetValue(); + if (img != null && img.TryExtract()) //判断是否是img + { + node = img.Node; + } + } + } + return node; + } + + public T GetValue(T defaultValue) + { + var typeT = typeof(T); + if (typeof(Wz_Image) == typeT) + { + if (this is Wz_Image.Wz_ImageNode) + { + return (T)(object)(((Wz_Image.Wz_ImageNode)this).Image); + } + else + { + return (this.value is T) ? (T)this.value : default(T); + } + } + if (this.value == null) + return defaultValue; + if (this.value is T) + return (T)this.value; + + + if (this.value is string s) + { + if (ObjectConverter.TryParse(s, out T result, out bool hasTryParse)) + { + return result; + } + if (hasTryParse) + { + return defaultValue; + } + } + + if (this.value is IConvertible iconvertible) + { + if (typeT.IsGenericType && typeT.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + typeT = typeT.GetGenericArguments()[0]; + } + + try + { + T result = (T)iconvertible.ToType(typeT, null); + return result; + } + catch + { + } + } + return defaultValue; + } + + public T GetValue() + { + return GetValue(default(T)); + } + + //innerClass + public class WzNodeCollection : IEnumerable + { + public WzNodeCollection(Wz_Node owner) + + { + this.owner = owner; + this.innerCollection = null; + } + + private readonly Wz_Node owner; + private InnerCollection innerCollection; + + public Wz_Node this[int index] + { + get { return this.innerCollection?[index]; } + } + + public Wz_Node this[string key] + { + get { return this.innerCollection?[key]; } + } + + public int Count + { + get { return this.innerCollection?.Count ?? 0; } + } + + public Wz_Node Add(string nodeText) + { + this.EnsureInnerCollection(); + return this.innerCollection.Add(nodeText); + } + + public void Add(Wz_Node item) + { + this.EnsureInnerCollection(); + this.innerCollection.Add(item); + } + + public void Sort() + { + this.innerCollection?.Sort(); + } + + public void Sort(Func getKeyFunc) where T : IComparable + { + if (getKeyFunc == null) + { + this.Sort(); + } + else if (this.innerCollection != null) + { + this.innerCollection.Sort(getKeyFunc); + } + } + + public void Trim() + { + this.innerCollection?.Trim(); + } + + public void Clear() + { + this.innerCollection?.Clear(); + } + + public IEnumerator GetEnumerator() + { + return this.innerCollection?.GetEnumerator() ?? System.Linq.Enumerable.Empty().GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + private void EnsureInnerCollection() + { + if (this.innerCollection == null) + { + this.innerCollection = new InnerCollection(this.owner); + } + } + + private class InnerCollection : KeyedCollection + { + public InnerCollection(Wz_Node owner) + : base(null, 12) + { + this.parentNode = owner; + } + + public Wz_Node Add(string nodeText) + { + Wz_Node newNode = new Wz_Node(nodeText); + this.Add(newNode); + return newNode; + } + + public new void Add(Wz_Node item) + { + base.Add(item); + if (item.parentNode != null) + { + int index = item.parentNode.nodes.innerCollection.Items.IndexOf(item); + if (index > -1) + { + item.parentNode.nodes.innerCollection.RemoveItem(index); + } + } + item.parentNode = this.parentNode; + } + + protected override void RemoveItem(int index) + { + var item = this[index]; + if (item != null) + { + item.parentNode = null; + } + base.RemoveItem(index); + } + + private readonly Wz_Node parentNode; + + public void Sort() + { + (base.Items as List)?.Sort(); + } + + public void Sort(Func getKeyFunc) where T : IComparable + { + ListSorter.Sort(base.Items as List, getKeyFunc); + } + + public void Trim() + { + (base.Items as List)?.TrimExcess(); + } + + public new Wz_Node this[string key] + { + get + { + if (key == null) + { + return null; + } + if (this.Dictionary != null) + { + Wz_Node node; + this.Dictionary.TryGetValue(key, out node); + return node; + } + else + { + List list = this.Items as List; + foreach (var node in list) + { + if (this.Comparer.Equals(this.GetKeyForItem(node), key)) + { + return node; + } + } + return null; + } + } + } + + protected override string GetKeyForItem(Wz_Node item) + { + return item.text; + } + } + + internal static class ListSorter + { + public static void Sort(List list, Func getKeyFunc) + { + T[] innerArray = list.GetType() + .GetField("_items", BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(list) as T[]; + + TKey[] keys = new TKey[list.Count]; + + for (int i = 0; i < keys.Length; i++) + { + keys[i] = getKeyFunc(innerArray[i]); + } + + Array.Sort(keys, innerArray, 0, keys.Length); + } + } + } + + public object Clone() + { + Wz_Node newNode = new Wz_Node(this.text); + newNode.value = this.value; + foreach (Wz_Node node in this.nodes) + { + Wz_Node newChild = node.Clone() as Wz_Node; + newNode.nodes.Add(newChild); + } + return newNode; + } + + int IComparable.CompareTo(object obj) + { + return ((IComparable)this).CompareTo(obj as Wz_Node); + } + + int IComparable.CompareTo(Wz_Node other) + { + if (other != null) + { + //return string.Compare(this.Text, other.Text, StringComparison.Ordinal); + return StrCmpLogicalW(this.Text, other.Text); + } + else + { + return 1; + } + } + + [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)] + static extern int StrCmpLogicalW(string psz1, string psz2); + } + + public static class Wz_NodeExtension + { + public static T GetValueEx(this Wz_Node node, T defaultValue) + { + if (node == null) + return defaultValue; + return node.GetValue(defaultValue); + } + + public static T? GetValueEx(this Wz_Node node) where T : struct + { + if (node == null) + return null; + return node.GetValue(); + } + + public static Wz_Node ResolveUol(this Wz_Node node) + { + if (node == null) + return null; + Wz_Uol uol; + while ((uol = node?.GetValueEx(null)) != null) + { + node = uol.HandleUol(node); + } + return node; + } + + /// + /// 搜索node所属的wz_file,若搜索不到则返回null。 + /// + /// 要搜索的wznode。 + /// + public static Wz_File GetNodeWzFile(this Wz_Node node, bool returnClosestWzFile = false) + { + Wz_File wzfile = null; + while (node != null) + { + if ((wzfile = node.Value as Wz_File) != null) + { + if (wzfile.OwnerWzFile != null) + { + wzfile = wzfile.OwnerWzFile; + node = wzfile.Node; + } + if (!wzfile.IsSubDir || returnClosestWzFile) + { + break; + } + } + else if (node.Value is Wz_Image wzImg + || (wzImg = (node as Wz_Image.Wz_ImageNode)?.Image) != null) + { + wzfile = GetImageWzFile(wzImg, returnClosestWzFile); + break; + } + node = node.ParentNode; + } + return wzfile; + } + + public static Wz_File GetImageWzFile(this Wz_Image wzImg, bool returnClosestWzFile = false) + { + if (!returnClosestWzFile && wzImg.WzFile != null) + { + return GetNodeWzFile(wzImg.WzFile.Node, returnClosestWzFile); + } + + return wzImg.WzFile; + } + + public static int GetMergedVersion(this Wz_File wzFile) + { + if (wzFile.Header.WzVersion != 0) + { + return wzFile.Header.WzVersion; + } + foreach (var subFile in wzFile.MergedWzFiles) + { + if (subFile.Header.WzVersion != 0) + { + return subFile.Header.WzVersion; + } + } + return 0; + } + + public static Wz_Image GetNodeWzImage(this Wz_Node node) + { + Wz_Image wzImg = null; + while (node != null) + { + if ((wzImg = node.Value as Wz_Image) != null + || (wzImg = (node as Wz_Image.Wz_ImageNode)?.Image) != null) + { + break; + } + node = node.ParentNode; + } + return wzImg; + } + + public static void DumpAsXml(this Wz_Node node, XmlWriter writer) + { + object value = node.Value; + + if (value == null || value is Wz_Image) + { + writer.WriteStartElement("dir"); + writer.WriteAttributeString("name", node.Text); + } + else if (value is Wz_Png) + { + var png = (Wz_Png)value; + writer.WriteStartElement("png"); + writer.WriteAttributeString("name", node.Text); + using (var bmp = png.ExtractPng()) + { + using (var ms = new MemoryStream()) + { + bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Png); + byte[] data = ms.ToArray(); + writer.WriteAttributeString("value", Convert.ToBase64String(data)); + } + } + } + else if (value is Wz_Uol) + { + var uol = (Wz_Uol)value; + writer.WriteStartElement("uol"); + writer.WriteAttributeString("name", node.Text); + writer.WriteAttributeString("value", uol.Uol); + } + else if (value is Wz_Vector) + { + var vector = (Wz_Vector)value; + writer.WriteStartElement("vector"); + writer.WriteAttributeString("name", node.Text); + writer.WriteAttributeString("value", $"{vector.X}, {vector.Y}"); + } + else if (value is Wz_Sound) + { + var sound = (Wz_Sound)value; + writer.WriteStartElement("sound"); + writer.WriteAttributeString("name", node.Text); + byte[] data = sound.ExtractSound(); + if (data == null) + { + data = new byte[sound.DataLength]; + sound.WzFile.FileStream.Seek(sound.Offset, SeekOrigin.Begin); + sound.WzFile.FileStream.Read(data, 0, sound.DataLength); + } + writer.WriteAttributeString("value", Convert.ToBase64String(data)); + } + else + { + var tag = value.GetType().Name.ToLower(); + writer.WriteStartElement(tag); + writer.WriteAttributeString("name", node.Text); + writer.WriteAttributeString("value", value.ToString()); + } + + //输出子节点 + foreach (var child in node.Nodes) + { + DumpAsXml(child, writer); + } + + //结束标识 + writer.WriteEndElement(); + } + + public static void SortByImgID(this Wz_Node.WzNodeCollection nodes) + { + if (regexImgID == null) + { + regexImgID = new Regex(@"^(\d+)\.img$", RegexOptions.Compiled); + } + + nodes.Sort(GetKey); + } + + private static Regex regexImgID; + + private static SortKey GetKey(Wz_Node node) + { + var key = new SortKey(); + var m = regexImgID.Match(node.Text); + if (m.Success) + { + key.HasID = Int32.TryParse(m.Result("$1"), out key.ImgID); + } + key.Text = node.Text; + return key; + } + + private struct SortKey : IComparable + { + public bool HasID; + public int ImgID; + public string Text; + + public int CompareTo(SortKey other) + { + if (this.HasID && other.HasID) return this.ImgID.CompareTo(other.ImgID); + return StringComparer.Ordinal.Compare(this.Text, other.Text); + } + } + } + + public static class ObjectConverter + { + private static readonly Dictionary cache = new Dictionary(); + private delegate bool TryParseFunc(string s, out T value); + + public static bool TryParse(string s, out T value, out bool hasTryParse) + { + var typeT = typeof(T); + + TryParseFunc tryParseFunc = null; + if (!cache.TryGetValue(typeT, out var dele)) + { + bool isNullable = false; + Type innerType; + if (typeT.IsGenericType && typeT.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + isNullable = true; + innerType = typeT.GetGenericArguments()[0]; + } + else + { + innerType = typeT; + } + + var methodInfo = innerType.GetMethod("TryParse", + BindingFlags.Static | BindingFlags.Public, + null, + new[] { typeof(string), innerType.MakeByRefType() }, + null); + + if (methodInfo != null && methodInfo.ReturnType == typeof(bool)) + { + if (isNullable) + { + dele = Delegate.CreateDelegate(typeof(TryParseFunc<>).MakeGenericType(innerType), methodInfo); + var proxyType = typeof(NullableTryParse<>).MakeGenericType(innerType); + var proxyInstance = Activator.CreateInstance(proxyType, dele); + var proxyParseFunc = proxyType.GetMethod("TryParse", BindingFlags.Public | BindingFlags.Instance); + cache[typeT] = tryParseFunc = (TryParseFunc)Delegate.CreateDelegate(typeof(TryParseFunc), proxyInstance, proxyParseFunc); + } + else + { + cache[typeT] = tryParseFunc = (TryParseFunc)Delegate.CreateDelegate(typeof(TryParseFunc), methodInfo); + } + } + else + { + cache[typeT] = null; + } + } + else + { + tryParseFunc = dele as TryParseFunc; + } + + if (tryParseFunc != null) + { + hasTryParse = true; + return tryParseFunc(s, out value); + } + else + { + hasTryParse = false; + value = default(T); + return false; + } + } + + private class NullableTryParse where T : struct + { + public NullableTryParse(TryParseFunc func) + { + this.func = func; + } + + private readonly TryParseFunc func; + + public bool TryParse(string s, out T? value) + { + if (this.func(s, out var v)) + { + value = v; + return true; + } + else + { + value = default(T?); + return false; + } + } + } + } +} \ No newline at end of file diff --git a/WzComparerR2.WzLib/Wz_Png.cs b/WzComparerR2.WzLib/Wz_Png.cs new file mode 100644 index 0000000..44f0cad --- /dev/null +++ b/WzComparerR2.WzLib/Wz_Png.cs @@ -0,0 +1,562 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Drawing; +using System.IO; +using System.IO.Compression; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; + +namespace WzComparerR2.WzLib +{ + public class Wz_Png + { + public Wz_Png(int w, int h, int data_length, int form, uint offs, Wz_Image wz_i) + { + this.w = w; + this.h = h; + this.data_length = data_length; + this.form = form; + this.offs = offs; + this.wz_i = wz_i; + } + + private int w; + private int h; + private int data_length; + private int form; + private uint offs; + private Wz_Image wz_i; + + /// + /// 获取或设置图片的宽度。 + /// + public int Width + { + get { return w; } + set { w = value; } + } + + /// + /// 获取或设置图片的高度。 + /// + public int Height + { + get { return h; } + set { h = value; } + } + + /// + /// 获取或设置数据块的长度。 + /// + public int DataLength + { + get { return data_length; } + set { data_length = value; } + } + + /// + /// 获取或设置数据块对于文件的偏移。 + /// + public uint Offset + { + get { return offs; } + set { offs = value; } + } + + /// + /// 获取或设置图片的数据压缩方式。 + /// + public int Form + { + get { return form; } + set { form = value; } + } + + /// + /// 获取或设置图片所属的WzFile + /// + public Wz_File WzFile + { + get { return wz_i.WzFile; } + set { wz_i.WzFile = value; } + } + + /// + /// 获取或设置图片所属的WzImage + /// + public Wz_Image WzImage + { + get { return wz_i; } + set { wz_i = value; } + } + + public byte[] GetRawData() + { + lock (this.WzFile.ReadLock) + { + DeflateStream zlib; + byte[] plainData = null; + + this.WzFile.FileStream.Position = this.Offset; + + if (this.WzFile.BReader.ReadUInt16() == 0x9C78) + { + byte[] buffer = this.WzFile.BReader.ReadBytes(this.data_length - 2); + MemoryStream dataStream = new MemoryStream(buffer); + + zlib = new DeflateStream(dataStream, CompressionMode.Decompress); + } + else + { + this.WzFile.FileStream.Position -= 2; + MemoryStream dataStream = new MemoryStream(this.DataLength); + int blocksize = 0; + int endPosition = (int)(this.DataLength + this.WzFile.FileStream.Position); + + var encKeys = this.WzImage.Encryption.keys; + + while (this.WzFile.FileStream.Position < endPosition) + { + blocksize = this.WzFile.BReader.ReadInt32(); + byte[] dataBlock = this.WzFile.BReader.ReadBytes(blocksize); + encKeys.Decrypt(dataBlock, 0, dataBlock.Length); + + dataStream.Write(dataBlock, 0, dataBlock.Length); + } + dataStream.Position = 2; + zlib = new DeflateStream(dataStream, CompressionMode.Decompress); + } + + switch (this.Form) + { + case 1: + case 257: + case 513: + plainData = new byte[this.w * this.h * 2]; + zlib.Read(plainData, 0, plainData.Length); + break; + + case 2: + plainData = new byte[this.w * this.h * 4]; + zlib.Read(plainData, 0, plainData.Length); + break; + + case 3: + plainData = new byte[((int)Math.Ceiling(this.w / 4.0)) * 4 * ((int)Math.Ceiling(this.h / 4.0)) * 4 / 8]; + zlib.Read(plainData, 0, plainData.Length); + break; + + case 517: + plainData = new byte[this.w * this.h / 128]; + zlib.Read(plainData, 0, plainData.Length); + break; + + case 1026: + case 2050: + plainData = new byte[this.w * this.h]; + zlib.Read(plainData, 0, plainData.Length); + break; + + default: + var msOut = new MemoryStream(); + zlib.CopyTo(msOut); + plainData = msOut.ToArray(); + break; + } + if (zlib != null) + { + zlib.Close(); + } + return plainData; + } + } + + public Bitmap ExtractPng() + { + byte[] pixel = this.GetRawData(); + if (pixel == null) + { + return null; + } + Bitmap pngDecoded = null; + BitmapData bmpdata; + byte[] argb; + + switch (this.form) + { + case 1: //16位argba4444 + argb = GetPixelDataBgra4444(pixel, this.w, this.h); + pngDecoded = new Bitmap(this.w, this.h, PixelFormat.Format32bppArgb); + bmpdata = pngDecoded.LockBits(new Rectangle(Point.Empty, pngDecoded.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + Marshal.Copy(argb, 0, bmpdata.Scan0, argb.Length); + pngDecoded.UnlockBits(bmpdata); + break; + + case 2: //32位argb8888 + pngDecoded = new Bitmap(this.w, this.h, PixelFormat.Format32bppArgb); + bmpdata = pngDecoded.LockBits(new Rectangle(Point.Empty, pngDecoded.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + Marshal.Copy(pixel, 0, bmpdata.Scan0, pixel.Length); + pngDecoded.UnlockBits(bmpdata); + break; + + case 3: //黑白缩略图 + argb = GetPixelDataForm3(pixel, this.w, this.h); + pngDecoded = new Bitmap(this.w, this.h, PixelFormat.Format32bppArgb); + bmpdata = pngDecoded.LockBits(new Rectangle(Point.Empty, pngDecoded.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + Marshal.Copy(argb, 0, bmpdata.Scan0, argb.Length); + pngDecoded.UnlockBits(bmpdata); + break; + + case 257: //16位argb1555 + pngDecoded = new Bitmap(this.w, this.h, PixelFormat.Format16bppArgb1555); + bmpdata = pngDecoded.LockBits(new Rectangle(Point.Empty, pngDecoded.Size), ImageLockMode.WriteOnly, PixelFormat.Format16bppArgb1555); + CopyBmpDataWithStride(pixel, pngDecoded.Width * 2, bmpdata); + pngDecoded.UnlockBits(bmpdata); + break; + + case 513: //16位rgb565 + pngDecoded = new Bitmap(this.w, this.h, PixelFormat.Format16bppRgb565); + bmpdata = pngDecoded.LockBits(new Rectangle(new Point(), pngDecoded.Size), ImageLockMode.WriteOnly, PixelFormat.Format16bppRgb565); + CopyBmpDataWithStride(pixel, pngDecoded.Width * 2, bmpdata); + pngDecoded.UnlockBits(bmpdata); + break; + + case 517: //16位rgb565缩略图 + argb = GetPixelDataForm517(pixel, this.w, this.h); + pngDecoded = new Bitmap(this.w, this.h, PixelFormat.Format16bppRgb565); + bmpdata = pngDecoded.LockBits(new Rectangle(0, 0, this.w, this.h), ImageLockMode.WriteOnly, PixelFormat.Format16bppRgb565); + CopyBmpDataWithStride(pixel, pngDecoded.Width * 2, bmpdata); + pngDecoded.UnlockBits(bmpdata); + break; + /* pngDecoded = new Bitmap(this.w, this.h); + pngSize = this.w * this.h / 128; + plainData = new byte[pngSize]; + zlib.Read(plainData, 0, pngSize); + byte iB = 0; + for (int i = 0; i < pngSize; i++) + { + for (byte j = 0; j < 8; j++) + { + iB = Convert.ToByte(((plainData[i] & (0x01 << (7 - j))) >> (7 - j)) * 0xFF); + for (int k = 0; k < 16; k++) + { + if (x == this.w) { x = 0; y++; } + pngDecoded.SetPixel(x, y, Color.FromArgb(0xFF, iB, iB, iB)); + x++; + } + } + } + break;*/ + + case 1026: //dxt3 + argb = GetPixelDataDXT3(pixel, this.w, this.h); + pngDecoded = new Bitmap(this.w, this.h, PixelFormat.Format32bppArgb); + bmpdata = pngDecoded.LockBits(new Rectangle(new Point(), pngDecoded.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + Marshal.Copy(argb, 0, bmpdata.Scan0, argb.Length); + pngDecoded.UnlockBits(bmpdata); + break; + + case 2050: //dxt5 + argb = GetPixelDataDXT5(pixel, this.w, this.h); + pngDecoded = new Bitmap(this.w, this.h, PixelFormat.Format32bppArgb); + bmpdata = pngDecoded.LockBits(new Rectangle(new Point(), pngDecoded.Size), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + Marshal.Copy(argb, 0, bmpdata.Scan0, argb.Length); + pngDecoded.UnlockBits(bmpdata); + break; + } + + return pngDecoded; + } + + public static byte[] GetPixelDataBgra4444(byte[] rawData, int width, int height) + { + byte[] argb = new byte[width * height * 4]; + { + int p; + for (int i = 0; i < rawData.Length; i++) + { + p = rawData[i] & 0x0F; p |= (p << 4); argb[i * 2] = (byte)p; + p = rawData[i] & 0xF0; p |= (p >> 4); argb[i * 2 + 1] = (byte)p; + } + } + return argb; + } + + public static byte[] GetPixelDataDXT3(byte[] rawData, int width, int height) + { + byte[] pixel = new byte[width * height * 4]; + + Color[] colorTable = new Color[4]; + int[] colorIdxTable = new int[16]; + byte[] alphaTable = new byte[16]; + for (int y = 0; y < height; y += 4) + { + for (int x = 0; x < width; x += 4) + { + int off = x * 4 + y * width; + ExpandAlphaTableDXT3(alphaTable, rawData, off); + ushort u0 = BitConverter.ToUInt16(rawData, off + 8); + ushort u1 = BitConverter.ToUInt16(rawData, off + 10); + ExpandColorTable(colorTable, u0, u1); + ExpandColorIndexTable(colorIdxTable, rawData, off + 12); + + for (int j = 0; j < 4; j++) + { + for (int i = 0; i < 4; i++) + { + SetPixel(pixel, + x + i, + y + j, + width, + colorTable[colorIdxTable[j * 4 + i]], + alphaTable[j * 4 + i]); + } + } + } + } + + return pixel; + } + + public static byte[] GetPixelDataDXT5(byte[] rawData, int width, int height) + { + byte[] pixel = new byte[width * height * 4]; + + Color[] colorTable = new Color[4]; + int[] colorIdxTable = new int[16]; + byte[] alphaTable = new byte[8]; + int[] alphaIdxTable = new int[16]; + for (int y = 0; y < height; y += 4) + { + for (int x = 0; x < width; x += 4) + { + int off = x * 4 + y * width; + ExpandAlphaTableDXT5(alphaTable, rawData[off + 0], rawData[off + 1]); + ExpandAlphaIndexTableDXT5(alphaIdxTable, rawData, off + 2); + ushort u0 = BitConverter.ToUInt16(rawData, off + 8); + ushort u1 = BitConverter.ToUInt16(rawData, off + 10); + ExpandColorTable(colorTable, u0, u1); + ExpandColorIndexTable(colorIdxTable, rawData, off + 12); + + for (int j = 0; j < 4; j++) + { + for (int i = 0; i < 4; i++) + { + SetPixel(pixel, + x + i, + y + j, + width, + colorTable[colorIdxTable[j * 4 + i]], + alphaTable[alphaIdxTable[j * 4 + i]]); + } + } + } + } + + return pixel; + } + + public static unsafe byte[] GetPixelDataForm3(byte[] rawData, int width, int height) + { + byte[] pixel = new byte[width * height * 4]; + fixed (byte* pArray = pixel) + { + int* argb2 = (int*)pArray; + int w = ((int)Math.Ceiling(width / 4.0)); + int h = ((int)Math.Ceiling(height / 4.0)); + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + var index = (x + y * w) * 2; //原像素索引 + var index2 = x * 4 + y * width * 4; //目标像素索引 + var p = (rawData[index] & 0x0F) | ((rawData[index] & 0x0F) << 4) + | ((rawData[index] & 0xF0) | ((rawData[index] & 0xF0) >> 4)) << 8 + | ((rawData[index + 1] & 0x0F) | ((rawData[index + 1] & 0x0F) << 4)) << 16 + | ((rawData[index + 1] & 0xF0) | ((rawData[index + 1] & 0xF0) >> 4)) << 24; + + for (int i = 0; i < 4; i++) + { + if (x * 4 + i < width) + { + argb2[index2 + i] = p; + } + else + { + break; + } + } + } + //复制行 + var srcIndex = y * width * 4 * 4; + var dstIndex = srcIndex + width * 4; + for (int j = 1; j < 4; j++) + { + if (y * 4 + j < height) + { + Array.Copy(pixel, srcIndex, pixel, dstIndex, width * 4); + dstIndex += width * 4; + } + else + { + break; + } + } + } + } + return pixel; + } + + public static byte[] GetPixelDataForm517(byte[] rawData, int width, int height) + { + byte[] pixel = new byte[width * height * 2]; + int lineIndex = 0; + for (int j0 = 0, j1 = height / 16; j0 < j1; j0++) + { + var dstIndex = lineIndex; + for (int i0 = 0, i1 = width / 16; i0 < i1; i0++) + { + int idx = (i0 + j0 * i1) * 2; + byte b0 = rawData[idx]; + byte b1 = rawData[idx + 1]; + for (int k = 0; k < 16; k++) + { + pixel[dstIndex++] = b0; + pixel[dstIndex++] = b1; + } + } + + for (int k = 1; k < 16; k++) + { + Array.Copy(pixel, lineIndex, pixel, dstIndex, width * 2); + dstIndex += width * 2; + } + + lineIndex += width * 32; + } + return pixel; + } + + private static void SetPixel(byte[] pixelData, int x, int y, int width, Color color, byte alpha) + { + int offset = (y * width + x) * 4; + pixelData[offset + 0] = color.B; + pixelData[offset + 1] = color.G; + pixelData[offset + 2] = color.R; + pixelData[offset + 3] = alpha; + } + + #region DXT1 Color + private static void ExpandColorTable(Color[] color, ushort c0, ushort c1) + { + color[0] = RGB565ToColor(c0); + color[1] = RGB565ToColor(c1); + if (c0 > c1) + { + color[2] = Color.FromArgb(0xff, (color[0].R * 2 + color[1].R + 1) / 3, (color[0].G * 2 + color[1].G + 1) / 3, (color[0].B * 2 + color[1].B + 1) / 3); + color[3] = Color.FromArgb(0xff, (color[0].R + color[1].R * 2 + 1) / 3, (color[0].G + color[1].G * 2 + 1) / 3, (color[0].B + color[1].B * 2 + 1) / 3); + } + else + { + color[2] = Color.FromArgb(0xff, (color[0].R + color[1].R) / 2, (color[0].G + color[1].G) / 2, (color[0].B + color[1].B) / 2); + color[3] = Color.FromArgb(0xff, Color.Black); + } + } + + private static void ExpandColorIndexTable(int[] colorIndex, byte[] rawData, int offset) + { + for (int i = 0; i < 16; i += 4, offset++) + { + colorIndex[i + 0] = (rawData[offset] & 0x03); + colorIndex[i + 1] = (rawData[offset] & 0x0c) >> 2; + colorIndex[i + 2] = (rawData[offset] & 0x30) >> 4; + colorIndex[i + 3] = (rawData[offset] & 0xc0) >> 6; + } + } + #endregion + + #region DXT3/DXT5 Alpha + private static void ExpandAlphaTableDXT3(byte[] alpha, byte[] rawData, int offset) + { + for (int i = 0; i < 16; i += 2, offset++) + { + alpha[i + 0] = (byte)(rawData[offset] & 0x0f); + alpha[i + 1] = (byte)((rawData[offset] & 0xf0) >> 4); + } + for (int i = 0; i < 16; i++) + { + alpha[i] = (byte)(alpha[i] | (alpha[i] << 4)); + } + } + + private static void ExpandAlphaTableDXT5(byte[] alpha, byte a0, byte a1) + { + alpha[0] = a0; + alpha[1] = a1; + if (a0 > a1) + { + for(int i = 2; i < 8; i++) + { + alpha[i] = (byte)(((8 - i) * a0 + (i - 1) * a1 + 3) / 7); + } + } + else + { + for(int i = 2; i < 6; i++) + { + alpha[i] = (byte)(((6 - i) * a0 + (i - 1) * a1 + 2) / 5); + } + alpha[6] = 0; + alpha[7] = 255; + } + } + + private static void ExpandAlphaIndexTableDXT5(int[] alphaIndex, byte[] rawData, int offset) + { + for (int i = 0; i < 16; i += 8, offset += 3) + { + int flags = rawData[offset] + | (rawData[offset + 1] << 8) + | (rawData[offset + 2] << 16); + for(int j = 0; j < 8; j++) + { + int mask = 0x07 << (3 * j); + alphaIndex[i + j] = (flags & mask) >> (3 * j); + } + } + } + #endregion + + public static Color RGB565ToColor(ushort val) + { + const int rgb565_mask_r = 0xf800; + const int rgb565_mask_g = 0x07e0; + const int rgb565_mask_b = 0x001f; + int r = (val & rgb565_mask_r) >> 11; + int g = (val & rgb565_mask_g) >> 5; + int b = (val & rgb565_mask_b); + var c = Color.FromArgb( + (r << 3) | (r >> 2), + (g << 2) | (g >> 4), + (b << 3) | (b >> 2)); + return c; + } + + public static void CopyBmpDataWithStride(byte[] source, int stride, BitmapData bmpData) + { + if (bmpData.Stride == stride) + { + Marshal.Copy(source, 0, bmpData.Scan0, source.Length); + } + else + { + for (int y = 0; y < bmpData.Height; y++) + { + Marshal.Copy(source, stride * y, bmpData.Scan0 + bmpData.Stride * y, stride); + } + } + + } + } +} diff --git a/WzComparerR2.WzLib/Wz_Sound.cs b/WzComparerR2.WzLib/Wz_Sound.cs new file mode 100644 index 0000000..7a00405 --- /dev/null +++ b/WzComparerR2.WzLib/Wz_Sound.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WzComparerR2.WzLib +{ + public class Wz_Sound + { + public Wz_Sound(uint offset, int length, byte[] header, int ms, Wz_Image wz_i) + { + this.offset = offset; + this.dataLength = length; + this.header = header; + this.ms = ms; + this.wz_i = wz_i; + TryDecryptHeader(); + } + + private uint offset; + private byte[] header; + private int dataLength; + private int ms; + + private Wz_Image wz_i; + + /// + /// 获取或设置数据块对于文件的偏移。 + /// + public uint Offset + { + get { return offset; } + set { offset = value; } + } + + /// + /// 获取或设置头部字节段。 + /// + public byte[] Header + { + get { return header; } + set { header = value; } + } + + public int Frequency + { + get + { + if (header == null || header.Length < 0x3c) + { + return 0; + } + return BitConverter.ToInt32(header, 0x38); + } + } + + /// + /// 获取或设置数据块的长度。 + /// + public int DataLength + { + get { return dataLength; } + set { dataLength = value; } + } + + /// + /// 获取或设置Mp3的声音毫秒数。 + /// + public int Ms + { + get { return ms; } + set { ms = value; } + } + + /// + /// 获取或设置图片所属的WzFile。 + /// + public Wz_File WzFile + { + get { return wz_i.WzFile; } + set { wz_i.WzFile = value; } + } + + /// + /// 获取或设置图片所属的WzImage。 + /// + public Wz_Image WzImage + { + get { return wz_i; } + set { wz_i = value; } + } + + public Wz_SoundType SoundType + { + get + { + Wz_SoundType soundType; + if (this.header == null) + { + soundType = Wz_SoundType.Mp3; + } + else + { + switch (this.header.Length) + { + default: + case 0x52: + soundType = Wz_SoundType.Mp3; + break; + + case 0x46: + { + if (this.Frequency == this.dataLength) + { + soundType = Wz_SoundType.Binary; + } + else + { + soundType = Wz_SoundType.WavRaw; + } + } + break; + } + } + + return soundType; + } + } + + public byte[] ExtractSound() + { + switch (this.SoundType) + { + case Wz_SoundType.Mp3: + { + byte[] data = new byte[this.dataLength]; + this.WzFile.FileStream.Seek(this.offset, System.IO.SeekOrigin.Begin); + this.WzFile.FileStream.Read(data, 0, this.dataLength); + return data; + } + case Wz_SoundType.WavRaw: + { + byte[] data = new byte[this.dataLength + 44]; + this.WzFile.FileStream.Seek(this.offset, System.IO.SeekOrigin.Begin); + this.WzFile.FileStream.Read(data, 44, this.dataLength); + byte[] wavHeader = new byte[44]{ + 0x52,0x49,0x46,0x46, //"RIFF" + 0,0,0,0, //ChunkSize + 0x57,0x41,0x56,0x45, //"WAVE" + + 0x66,0x6d,0x74,0x20, //"fmt " + 0x10,0,0,0, //chunk1Size + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // copy16字节 + + 0x64,0x61,0x74,0x61, //"data" + 0,0,0,0 //chunk2Size + }; + Array.Copy(BitConverter.GetBytes(this.dataLength + 36), 0, wavHeader, 4, 4); + Array.Copy(this.header, 0x34, wavHeader, 20, 16); + Array.Copy(BitConverter.GetBytes(this.dataLength), 0, wavHeader, 40, 4); + Array.Copy(wavHeader, data, wavHeader.Length); + return data; + } + } + return null; + } + + private void TryDecryptHeader() + { + if (this.header == null) + { + return; + } + if (this.header.Length > 51) + { + byte waveFormatLen = this.header[51]; + if (this.header.Length != 52 + waveFormatLen) //长度错误 + { + return; + } + int cbSize = BitConverter.ToUInt16(this.header, 52 + 16); + if (cbSize + 18 != waveFormatLen) + { + byte[] tempHeader = new byte[waveFormatLen]; + Buffer.BlockCopy(this.header, 52, tempHeader, 0, tempHeader.Length); + var enc = this.WzImage.Encryption; + enc.keys.Decrypt(tempHeader, 0, tempHeader.Length); //解密 + cbSize = BitConverter.ToUInt16(tempHeader, 16); //重新验证 + if (cbSize + 18 == waveFormatLen) + { + Buffer.BlockCopy(tempHeader, 0, this.header, 52, tempHeader.Length); + } + } + } + } + } +} diff --git a/WzComparerR2.WzLib/Wz_SoundType.cs b/WzComparerR2.WzLib/Wz_SoundType.cs new file mode 100644 index 0000000..6de7f7a --- /dev/null +++ b/WzComparerR2.WzLib/Wz_SoundType.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WzComparerR2.WzLib +{ + public enum Wz_SoundType + { + Mp3 = 0, + WavRaw = 1, + Binary = 2, + } +} diff --git a/WzComparerR2.WzLib/Wz_Structure.cs b/WzComparerR2.WzLib/Wz_Structure.cs new file mode 100644 index 0000000..521296e --- /dev/null +++ b/WzComparerR2.WzLib/Wz_Structure.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Linq; + +namespace WzComparerR2.WzLib +{ + public class Wz_Structure + { + public Wz_Structure() + { + this.wz_files = new List(); + this.encryption = new Wz_Crypto(); + this.img_number = 0; + this.has_basewz = false; + this.TextEncoding = Wz_Structure.DefaultEncoding; + this.AutoDetectExtFiles = Wz_Structure.DefaultAutoDetectExtFiles; + this.ImgCheckDisabled = Wz_Structure.DefaultImgCheckDisabled; + } + + public List wz_files; + public Wz_Crypto encryption; + public Wz_Node WzNode; + public int img_number; + public bool has_basewz; + public bool sorted; //暂时弃用 + + public Encoding TextEncoding { get; set; } + public bool AutoDetectExtFiles { get; set; } + public bool ImgCheckDisabled { get; set; } + + public void Clear() + { + foreach (Wz_File f in this.wz_files) + { + f.Close(); + } + this.wz_files.Clear(); + this.encryption.Reset(); + this.img_number = 0; + this.has_basewz = false; + this.WzNode = null; + this.sorted = false; + } + + public void calculate_img_count() + { + foreach (Wz_File f in this.wz_files) + { + this.img_number += f.ImageCount; + } + } + + public void Load(string fileName, bool useBaseWz = false) + { + //现在我们已经不需要list了 + this.WzNode = new Wz_Node(Path.GetFileName(fileName)); + if (Path.GetFileName(fileName).ToLower() == "list.wz") + { + this.encryption.LoadListWz(Path.GetDirectoryName(fileName)); + foreach (string list in this.encryption.List) + { + WzNode.Nodes.Add(list); + } + } + else + { + LoadFile(fileName, WzNode, useBaseWz); + } + calculate_img_count(); + } + + public Wz_File LoadFile(string fileName, Wz_Node node, bool useBaseWz = false, bool loadWzAsFolder = false) + { + Wz_File file = null; + + try + { + file = new Wz_File(fileName, this); + if (!file.Loaded) + { + throw new Exception("The file is not a valid wz file."); + } + this.wz_files.Add(file); + file.TextEncoding = this.TextEncoding; + if (!this.encryption.encryption_detected) + { + this.encryption.DetectEncryption(file); + } + node.Value = file; + file.Node = node; + file.FileStream.Position = file.Header.DataStartPosition; + file.GetDirTree(node, useBaseWz, loadWzAsFolder); + file.DetectWzType(); + file.DetectWzVersion(); + return file; + } + catch + { + if (file != null) + { + file.Close(); + this.wz_files.Remove(file); + } + throw; + } + } + + public void LoadImg(string fileName) + { + this.WzNode = new Wz_Node(Path.GetFileName(fileName)); + this.LoadImg(fileName, WzNode); + } + + public void LoadImg(string fileName, Wz_Node node) + { + Wz_File file = null; + + try + { + file = new Wz_File(fileName, this); + file.TextEncoding = this.TextEncoding; + var imgNode = new Wz_Node(node.Text); + //跳过checksum检测 + var img = new Wz_Image(node.Text, (int)file.FileStream.Length, 0, 0, 0, file) + { + OwnerNode = imgNode, + Offset = 0, + IsChecksumChecked = true + }; + imgNode.Value = img; + node.Nodes.Add(imgNode); + this.wz_files.Add(file); + } + catch + { + file?.Close(); + throw; + } + } + + public void LoadKMST1125DataWz(string fileName) + { + LoadWzFolder(Path.GetDirectoryName(fileName), ref this.WzNode, true); + calculate_img_count(); + } + + public bool IsKMST1125WzFormat(string fileName) + { + if (!string.Equals(Path.GetExtension(fileName), ".wz", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + string iniFile = Path.ChangeExtension(fileName, ".ini"); + if (!File.Exists(iniFile)) + { + return false; + } + + // check if the file is an empty wzfile + using (var file = new Wz_File(fileName, this)) + { + if (!file.Loaded) + { + return false; + } + var tempNode = new Wz_Node(); + if (!this.encryption.encryption_detected) + { + this.encryption.DetectEncryption(file); + } + file.FileStream.Position = file.Header.DataStartPosition; + file.GetDirTree(tempNode); + return file.ImageCount == 0; + } + } + + public void LoadWzFolder(string folder, ref Wz_Node node, bool useBaseWz = false) + { + string baseName = Path.Combine(folder, Path.GetFileName(folder)); + string entryWzFileName = Path.ChangeExtension(baseName, ".wz"); + string iniFileName = Path.ChangeExtension(baseName, ".ini"); + Func extraWzFileName = _index => Path.ChangeExtension($"{baseName}_{_index:D3}", ".wz"); + + // load iniFile + int? lastWzIndex = null; + if (File.Exists(iniFileName)) + { + var iniConf = File.ReadAllLines(iniFileName).Select(row => + { + string[] columns = row.Split('|'); + string key = columns.Length > 0 ? columns[0] : null; + string value = columns.Length > 1 ? columns[1] : null; + return new KeyValuePair(key, value); + }); + if (int.TryParse(iniConf.FirstOrDefault(kv => kv.Key == "LastWzIndex").Value, out var indexFromIni)) + { + lastWzIndex = indexFromIni; + } + } + + // ini file missing or unexpected format + if (lastWzIndex == null) + { + for (int i = 0; ; i++) + { + string extraFile = extraWzFileName(i); + if (!File.Exists(extraFile)) + { + break; + } + lastWzIndex = i; + } + } + + // load entry file + if (node == null) + { + node = new Wz_Node(Path.GetFileName(entryWzFileName)); + } + var entryWzf = this.LoadFile(entryWzFileName, node, useBaseWz, true); + + // load extra file + if (lastWzIndex != null) + { + for (int i = 0, j = lastWzIndex.Value; i <= j; i++) + { + string extraFile = extraWzFileName(i); + var tempNode = new Wz_Node(Path.GetFileName(extraFile)); + var extraWzf = this.LoadFile(extraFile, tempNode, false, true); + + /* + * there is a little hack here, we'll move all img to the entry file, and each img still refers to the original wzfile. + * before: + * base.wz (Wz_File) + * |- a.img (Wz_Image) + * base_000.wz (Wz_File) + * |- b.img (Wz_Image) { wz_f = base_000.wz } + * + * after: + * base.wz (Wz_File) { mergedFiles = [base_000.wz] } + * |- a.img (Wz_Image) { wz_f = base.wz } + * |- b.img (Wz_Image) { wz_f = base_000.wz } + * + * this.wz_files references all opened files so they can be closed correctly. + */ + + entryWzf.MergeWzFile(extraWzf); + } + } + } + + #region Global Settings + public static Encoding DefaultEncoding + { + get { return _defaultEncoding ?? Encoding.Default; } + set { _defaultEncoding = value; } + } + + private static Encoding _defaultEncoding; + + public static bool DefaultAutoDetectExtFiles { get; set; } + + public static bool DefaultImgCheckDisabled { get; set; } + #endregion + } +} diff --git a/WzComparerR2.WzLib/Wz_Type.cs b/WzComparerR2.WzLib/Wz_Type.cs new file mode 100644 index 0000000..826190e --- /dev/null +++ b/WzComparerR2.WzLib/Wz_Type.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WzComparerR2.WzLib +{ + /// + /// 标识Wz_File的内部类型。 + /// + public enum Wz_Type + { + Unknown = 0, + Base, + Character, + Effect, + Etc, + Item, + Map, + Mob, + Morph, + Npc, + Quest, + Reactor, + Skill, + Sound, + String, + TamingMob, + UI, + } +} diff --git a/WzComparerR2.WzLib/Wz_Uol.cs b/WzComparerR2.WzLib/Wz_Uol.cs new file mode 100644 index 0000000..2809799 --- /dev/null +++ b/WzComparerR2.WzLib/Wz_Uol.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace WzComparerR2.WzLib +{ + public class Wz_Uol + { + public Wz_Uol(string uol) + { + this.uol = uol; + } + + private string uol; + + /// + /// 获取或设置连接路径字符串。 + /// + public string Uol + { + get { return uol; } + set { uol = value; } + } + + public Wz_Node HandleUol(Wz_Node currentNode) + { + if (currentNode == null || currentNode.ParentNode == null || string.IsNullOrEmpty(uol)) + return null; + string[] dirs = this.uol.Split('/'); + currentNode = currentNode.ParentNode; + + bool outImg = false; + + for (int i = 0; i < dirs.Length; i++) + { + string dir = dirs[i]; + if (dir == "..") + { + if (currentNode.ParentNode == null) + { + Wz_Image img = currentNode.GetValueEx(null); + if (img != null) + { + currentNode = img.OwnerNode.ParentNode; + outImg = true; + } + else + { + currentNode = null; + } + } + else + { + currentNode = currentNode.ParentNode; + } + } + else + { + var dirNode = currentNode.FindNodeByPath(dir); + + if (dirNode == null && outImg) + { + dirNode = currentNode.FindNodeByPath(true, dir + ".img"); + if (dirNode.GetValueEx(null) != null) + { + outImg = false; + } + } + + currentNode = dirNode; + } + if (currentNode == null) + return null; + } + return currentNode; + } + } +} diff --git a/WzComparerR2.WzLib/Wz_Vector.cs b/WzComparerR2.WzLib/Wz_Vector.cs new file mode 100644 index 0000000..873eab1 --- /dev/null +++ b/WzComparerR2.WzLib/Wz_Vector.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Drawing; + +namespace WzComparerR2.WzLib +{ + public class Wz_Vector + { + public Wz_Vector(int x, int y) + { + this.x = x; + this.y = y; + } + + private int x; + private int y; + + /// + /// 获取或设置向量的X值。 + /// + public int X + { + get { return x; } + set { x = value; } + } + + /// + /// 获取或设置向量的Y值。 + /// + public int Y + { + get { return y; } + set { y = value; } + } + + public static implicit operator Point(Wz_Vector vector) + { + return vector == null ? new Point() : new Point(vector.x, vector.y); + } + } +}