From f6fb7b87a6940e29e1edf35a0ebb1bcf735ae4bd Mon Sep 17 00:00:00 2001 From: genment <5207957+genment@users.noreply.github.com> Date: Sat, 12 Feb 2022 17:15:31 -0500 Subject: [PATCH] [MapleLib] Support new WZ files (GMS v230) --- MapleLib/WzLib/WzDirectory.cs | 15 +----- MapleLib/WzLib/WzFile.cs | 93 ++++++++++++++++++++++++++--------- 2 files changed, 73 insertions(+), 35 deletions(-) diff --git a/MapleLib/WzLib/WzDirectory.cs b/MapleLib/WzLib/WzDirectory.cs index 9454fca3..83f4b7ec 100644 --- a/MapleLib/WzLib/WzDirectory.cs +++ b/MapleLib/WzLib/WzDirectory.cs @@ -263,17 +263,6 @@ internal void ParseDirectory(bool lazyParse = false) } } - // Offsets are calculated manually - if (this.wzFile != null && this.wzFile.b64BitClient) // This only applies for 64-bit client based WZ files. - { - long startOffset = reader.BaseStream.Position; - foreach (WzImage image in WzImages) - { - image.Offset = (uint)startOffset; - startOffset += image.BlockSize; // TODO: Test saving the wz after it is changed. This may break - } - } - foreach (WzDirectory subdir in subDirs) { if (subdir.Checksum != 0) @@ -289,7 +278,7 @@ internal void ParseDirectory(bool lazyParse = false) /// /// /// - internal void SaveImages(BinaryWriter wzWriter, FileStream fs) + internal void SaveImages(BinaryWriter wzWriter, FileStream fs) { // List wzImageNameTracking = new List(); // Check for duplicate WZ image name that could cause errors later on. @@ -335,7 +324,7 @@ internal void SaveImages(BinaryWriter wzWriter, FileStream fs) /// Uses the default MapleStory UserKey or a custom key. /// The previously opened file stream /// - internal int GenerateDataFile(byte[] useIv, bool bIsWzUserKeyDefault, FileStream prevOpenedStream) + internal int GenerateDataFile(byte[] useIv, bool bIsWzUserKeyDefault, FileStream prevOpenedStream) { bool useCustomIv = useIv != null; // whole shit gonna be re-written if its a custom IV specified diff --git a/MapleLib/WzLib/WzFile.cs b/MapleLib/WzLib/WzFile.cs index 89d700c5..9d9d0419 100644 --- a/MapleLib/WzLib/WzFile.cs +++ b/MapleLib/WzLib/WzFile.cs @@ -40,7 +40,8 @@ public class WzFile : WzObject internal WzDirectory wzDir; internal WzHeader header; internal string name = ""; - internal short wzVersionHeader = 0; + internal ushort wzVersionHeader = 0; + internal const ushort wzVersionHeader64bit = 777; internal uint versionHash = 0; internal short mapleStoryPatchVersion = 0; internal WzMapleVersion maplepLocalVersion; @@ -119,7 +120,7 @@ public override void Dispose() /// /// /// - public WzFile(short gameVersion, WzMapleVersion version) + public WzFile(short gameVersion, WzMapleVersion version) { wzDir = new WzDirectory(); this.Header = WzHeader.GetDefault(); @@ -222,12 +223,20 @@ internal WzFileParseStatus ParseMainWzDirectory(bool lazyParse = false) byte[] unk2 = reader.ReadBytes((int)(Header.FStart - (ulong)reader.BaseStream.Position)); reader.Header = this.Header; - this.wzVersionHeader = 0; - if (!this.b64BitClient) - this.wzVersionHeader = reader.ReadInt16(); + Check64BitClient(reader); // update b64BitClient flag + + // the value of wzVersionHeader is less important. It is used for reading/writing from/to WzFile Header, and calculating the versionHash. + // it can be any number if the client is 64-bit. Assigning 777 is just for convenience when calculating the versionHash. + this.wzVersionHeader = b64BitClient ? wzVersionHeader64bit : reader.ReadUInt16(); if (mapleStoryPatchVersion == -1) { + // for 64-bit client, return immediately if version 777 works correctly. + if (b64BitClient && TryDecodeWithWZVersionNumber(reader, wzVersionHeader, wzVersionHeader64bit, lazyParse)) + { + return WzFileParseStatus.Success; + } + // Attempt to get version from MapleStory.exe first short maplestoryVerDetectedFromClient = GetMapleStoryVerFromExe(this.path, out this.mapleLocaleVersion); @@ -238,19 +247,16 @@ internal WzFileParseStatus ParseMainWzDirectory(bool lazyParse = false) for (int j = maplestoryVerDetectedFromClient; j < MAX_PATCH_VERSION; j++) { if (TryDecodeWithWZVersionNumber(reader, wzVersionHeader, j, lazyParse)) - { - return WzFileParseStatus.Success; - } + { + return WzFileParseStatus.Success; + } } //parseErrorMessage = "Error with game version hash : The specified game version is incorrect and WzLib was unable to determine the version itself"; return WzFileParseStatus.Error_Game_Ver_Hash; } else { - if (!this.b64BitClient) - { - this.versionHash = CheckAndGetVersionHash(wzVersionHeader, mapleStoryPatchVersion); - } + this.versionHash = CheckAndGetVersionHash(wzVersionHeader, mapleStoryPatchVersion); reader.Hash = this.versionHash; WzDirectory directory = new WzDirectory(reader, this.name, this.versionHash, this.WzIv, this); directory.ParseDirectory(); @@ -258,18 +264,58 @@ internal WzFileParseStatus ParseMainWzDirectory(bool lazyParse = false) } return WzFileParseStatus.Success; } - - private bool TryDecodeWithWZVersionNumber(WzBinaryReader reader, int useWzVersionHeader, int useMapleStoryPatchVersion, bool lazyParse) - { - this.mapleStoryPatchVersion = (short)useMapleStoryPatchVersion; - if (!this.b64BitClient) + /// + /// encVer detecting: + /// Since KMST1132 (GMSv230, 2022/02/09), wz removed the 2-byte encVer at 0x3C, and use a fixed encVer 777. + /// Here we try to read the first 2 bytes from data part (0x3C) and guess if it looks like an encVer. + /// + /// Credit: WzComparerR2 project + /// + private void Check64BitClient(WzBinaryReader reader) + { + if (this.Header.FSize >= 2) + { + this.wzVersionHeader = reader.ReadUInt16(); + if (this.wzVersionHeader > 0xff) + { + b64BitClient = true; + } + else if (this.wzVersionHeader == 0x80) + { + // there's an exceptional case that the first field of data part is a compressed int which determines the 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 WzFile won't greater than 65536 (0xFFFF). + if (this.Header.FSize >= 5) + { + reader.BaseStream.Position = this.header.FStart; // go back to 0x3C + int propCount = reader.ReadCompressedInt(); + if (propCount > 0 && (propCount & 0xFF) == 0 && propCount <= 0xFFFF) + { + b64BitClient = true; + } + } + } + } + else { - this.versionHash = CheckAndGetVersionHash(useWzVersionHeader, mapleStoryPatchVersion); - if (this.versionHash == 0) // ugly hack, but that's the only way if the version number isnt known (nexon stores this in the .exe) - return false; + // Obviously, if data part have only 1 byte, encVer must be deleted. + b64BitClient = true; } + // reset position + reader.BaseStream.Position = this.Header.FStart; + } + + private bool TryDecodeWithWZVersionNumber(WzBinaryReader reader, int useWzVersionHeader, int useMapleStoryPatchVersion, bool lazyParse) + { + this.mapleStoryPatchVersion = (short)useMapleStoryPatchVersion; + + this.versionHash = CheckAndGetVersionHash(useWzVersionHeader, mapleStoryPatchVersion); + if (this.versionHash == 0) // ugly hack, but that's the only way if the version number isnt known (nexon stores this in the .exe) + return false; + reader.Hash = this.versionHash; long fallbackOffsetPosition = reader.BaseStream.Position; // save position to rollback to, if should parsing fail from here WzDirectory testDirectory; @@ -443,12 +489,15 @@ private static uint CheckAndGetVersionHash(int wzVersionHeader, int maplestoryPa VersionHash = (32 * VersionHash) + (int)VersionNumberStr[i] + 1; } + if (wzVersionHeader == wzVersionHeader64bit) + return (uint)VersionHash; // always 59192 + int a = (VersionHash >> 24) & 0xFF; int b = (VersionHash >> 16) & 0xFF; int c = (VersionHash >> 8) & 0xFF; int d = VersionHash & 0xFF; int DecryptedVersionNumber = (0xff ^ a ^ b ^ c ^ d); - + if (wzVersionHeader == DecryptedVersionNumber) return (uint)VersionHash; return 0; // invalid @@ -507,7 +556,7 @@ public void SaveToDisk(string path, WzMapleVersion savingToPreferredWzVer = WzMa { wzWriter.Hash = versionHash; - uint totalLen = wzDir.GetImgOffsets(wzDir.GetOffsets(Header.FStart + 2)); + uint totalLen = wzDir.GetImgOffsets(wzDir.GetOffsets(Header.FStart + (b64BitClient ? 0 : 2u))); Header.FSize = totalLen - Header.FStart; for (int i = 0; i < 4; i++) {