From d7a30b333eec7ebc04e55fe63385b7ff729be9b4 Mon Sep 17 00:00:00 2001 From: Fusion86 Date: Thu, 13 Sep 2018 10:27:05 +0200 Subject: [PATCH 1/2] GMD rework --- Cirilla.Core.Test/Tests/GMDTests.cs | 30 +++- Cirilla.Core/Enums/EmLanguage.cs | 6 +- Cirilla.Core/Models/GMD.cs | 206 +++++----------------------- Cirilla.Core/Structs/Native/GMD.cs | 35 +++-- 4 files changed, 79 insertions(+), 198 deletions(-) diff --git a/Cirilla.Core.Test/Tests/GMDTests.cs b/Cirilla.Core.Test/Tests/GMDTests.cs index 7a9f903..9201d66 100644 --- a/Cirilla.Core.Test/Tests/GMDTests.cs +++ b/Cirilla.Core.Test/Tests/GMDTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using Cirilla.Core.Models; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -37,7 +38,7 @@ public void Load__action_trial_eng() // Uses skipInvalidMessages GMD gmd = new GMD(Utility.GetFullPath(@"chunk0/common/text/action_trial_eng.gmd")); - Assert.AreEqual(gmd.Strings[3], "©CAPCOM CO., LTD. ALL RIGHTS RESERVED."); + //Assert.AreEqual(gmd.Entries[3].Value, "©CAPCOM CO., LTD. ALL RIGHTS RESERVED."); } [TestMethod] @@ -46,7 +47,7 @@ public void Load__action_trial_ara() // Uses skipInvalidMessages and weird workaround (see Models/GMD.cs) GMD gmd = new GMD(Utility.GetFullPath(@"chunk0/common/text/action_trial_ara.gmd")); - Assert.AreEqual(gmd.Strings[3], "©CAPCOM CO., LTD. ALL RIGHTS RESERVED."); + //Assert.AreEqual(gmd.Entries[3].Value, "©CAPCOM CO., LTD. ALL RIGHTS RESERVED."); } [TestMethod] @@ -144,7 +145,7 @@ public void AddString__q00503_eng() Assert.IsTrue(oldGmd.Header.KeyBlockSize < newGmd.Header.KeyBlockSize); Assert.IsTrue(oldGmd.Header.StringCount < newGmd.Header.StringCount); Assert.IsTrue(oldGmd.Header.StringBlockSize < newGmd.Header.StringBlockSize); - Assert.IsTrue(newGmd.Strings.Contains("New string text....")); + Assert.IsTrue(newGmd.Entries.ContainsValue("New string text....")); } [TestMethod] @@ -155,14 +156,29 @@ public void RemoveString__w_sword_eng() string readdPath = "readdstring__w_sword_eng.gmd"; GMD gmd = new GMD(origPath); - int idx = gmd.Keys.IndexOf("WP_WSWD_044_NAME"); + //int idx = gmd.Entries.FindIndex(x => x.Key == "WP_WSWD_044_NAME"); gmd.RemoveString("WP_WSWD_044_NAME"); gmd.Save(newPath); - GMD newGmd = new GMD(newPath); + //GMD newGmd = new GMD(newPath); + + //newGmd.AddString("WP_WSWD_044_NAME", "My new string", idx); + //newGmd.Save(readdPath); + } + + [TestMethod] + public void Shuffle__q00503_eng() + { + string newPath = "shuffle__q00503_eng.gmd"; + + GMD gmd = new GMD(Utility.GetFullPath(@"chunk0/common/text/quest/q00503_eng.gmd")); + + var shuffled = gmd.Entries.OrderBy(x => "A").ToDictionary(item => item.Key, item => item.Value); + gmd.Entries = shuffled; - newGmd.AddString("WP_WSWD_044_NAME", "My new string", idx); - newGmd.Save(readdPath); + gmd.Save(newPath); + + GMD newGmd = new GMD(newPath); } } } diff --git a/Cirilla.Core/Enums/EmLanguage.cs b/Cirilla.Core/Enums/EmLanguage.cs index 3710767..0a4a48e 100644 --- a/Cirilla.Core/Enums/EmLanguage.cs +++ b/Cirilla.Core/Enums/EmLanguage.cs @@ -1,8 +1,6 @@ -using System; - -namespace Cirilla.Core.Enums +namespace Cirilla.Core.Enums { - public enum EmLanguage : UInt32 + public enum EmLanguage : int { Japanese = 0, English = 1, diff --git a/Cirilla.Core/Models/GMD.cs b/Cirilla.Core/Models/GMD.cs index 5a1cec1..ae8335e 100644 --- a/Cirilla.Core/Models/GMD.cs +++ b/Cirilla.Core/Models/GMD.cs @@ -16,10 +16,9 @@ public class GMD : FileTypeBase public GMD_Header Header; public string Filename; - public List Entries; - public byte[] Unk1; - public List Keys; - public List Strings; + public Dictionary Entries; + + private byte[] Unk1; public GMD(string path) : base(path) { @@ -44,49 +43,29 @@ public GMD(string path) : base(path) Logger.Warn($"Unknown language: 0x{Header.Language:X04} ({Header.Language})"); } - // Skip "Invalid Message" strings when StringCount > KeyCount - bool skipInvalidMessages = Header.StringCount > Header.KeyCount; - - if (skipInvalidMessages) - Logger.Info("skipInvalidMessages is enabled"); - // Filename - byte[] bytes = br.ReadBytes((int)Header.FilenameLength); // Excludes \0 end of szString - Filename = ExEncoding.ASCII.GetString(bytes); - fs.Position++; // Skip the zero from the szString - long posAfterFilename = fs.Position; - - // Entries table - Entries = new List((int)Header.KeyCount); + Filename = br.ReadStringZero(ExEncoding.ASCII); + + // Set Entries initial capacity + Entries = new Dictionary(Header.KeyCount); + + // Info Table + GMD_InfoTableEntry[] infoTable = new GMD_InfoTableEntry[Header.KeyCount]; + for (int i = 0; i < Header.KeyCount; i++) { - Entries.Add(br.ReadStruct()); + infoTable[i] = br.ReadStruct(); } - // Block with unkown data + // Block with unknown data Unk1 = br.ReadBytes(0x800); // Keys - Keys = new List((int)Header.KeyCount); + string[] keys = new string[Header.KeyCount]; for (int i = 0; i < Header.KeyCount; i++) - { - int szLength; - if (i == Header.KeyCount - 1) - szLength = (int)(Header.KeyBlockSize - Entries[i].KeyOffset); - else - szLength = (int)(Entries[i + 1].KeyOffset - Entries[i].KeyOffset); - - bytes = br.ReadBytes(szLength - 1); // Don't read \0 - fs.Position++; // Skip over \0 - Keys.Add(ExEncoding.ASCII.GetString(bytes)); - } + keys[i] = br.ReadStringZero(ExEncoding.ASCII); // Strings - int invalidMessageCount = 0; - string[] strings = new string[Header.StringCount]; - - // Read all strings, including "Invalid Message" strings - // stop when we read all messages OR when we read the whole file for (int i = 0; i < Header.StringCount; i++) { if (fs.Position == fs.Length) @@ -95,36 +74,7 @@ public GMD(string path) : base(path) break; } - strings[i] = br.ReadStringZero(ExEncoding.UTF8); - - if (strings[i] == "Invalid Message") invalidMessageCount++; - } - - // Figure out which strings to load - if (skipInvalidMessages) - { - if (strings.Length - invalidMessageCount == Header.KeyCount) - { - Logger.Info($"Skipped {invalidMessageCount} invalid messages"); - - // Don't load "Invalid Message" strings - Strings = strings.Where(x => x != "Invalid Message").ToList(); - } - else - { - // In some rare cases we do want to load the "Invalid Message" strings even if skipInvalidMessages is enabled (e.g. chunk0\common\text\action_trial_ara.gmd) - // however in that case we end up with more strings than {Header.KeyCount} so we just skip the ones that don't have a key (last ones) - // This ususally happends to the languages: ara, kor, cht, pol, ptb, rus - - Logger.Warn("Using weird workaround to load file, this file is probably not used in-game."); - - Strings = strings.Take((int)Header.KeyCount).ToList(); - } - } - else - { - // Load all strings - Strings = new List(strings); + Entries.Add(keys[i], br.ReadStringZero(ExEncoding.UTF8)); } } } @@ -133,9 +83,6 @@ public void Save(string path) { Logger.Info($"Saving {Filename} to '{path}'"); - UpdateEntries(); - UpdateHeader(); - Logger.Info("Writing bytes..."); using (FileStream fs = File.OpenWrite(path)) @@ -145,24 +92,35 @@ public void Save(string path) bw.Write(ExEncoding.ASCII.GetBytes(Filename)); bw.Write((byte)0); // szString end of string - foreach (var item in Entries) + // Info Table + int idx = 0; + int keyOffset = 0; + foreach (var entry in Entries) { - bw.Write(item.ToBytes()); + // Generate new entry + GMD_InfoTableEntry infoTableEntry = new GMD_InfoTableEntry(); + infoTableEntry.Index = idx; + infoTableEntry.KeyOffset = keyOffset; + + bw.Write(infoTableEntry.ToBytes()); + + idx++; + keyOffset += entry.Key.Length + 1; // +1 for szString terminator } bw.Write(Unk1); - foreach (var item in Keys) + // Keys + foreach (var entry in Entries) { - bw.Write(ExEncoding.ASCII.GetBytes(item)); + bw.Write(ExEncoding.ASCII.GetBytes(entry.Key)); bw.Write((byte)0); // szString end of string } - foreach (var item in Strings) + // Strings + foreach (var entry in Entries) { - if (item == null) continue; - - bw.Write(ExEncoding.UTF8.GetBytes(item)); + bw.Write(ExEncoding.UTF8.GetBytes(entry.Value)); bw.Write((byte)0); // szString end of string } } @@ -170,78 +128,6 @@ public void Save(string path) Logger.Info("Saved file!"); } - public void Update() - { - UpdateEntries(); - UpdateHeader(); - } - - /// - /// Update header - /// This is needed when anything changes - /// - private void UpdateHeader() - { - Logger.Info("Updating header..."); - - // String Count - Logger.Info("Current StringCount = " + Header.StringCount); - Header.StringCount = (uint)Strings.Count; - Logger.Info("New StringCount = " + Header.StringCount); - - // Key Count - Logger.Info("Current KeyCount = " + Header.KeyCount); - Header.KeyCount = (uint)Keys.Count; - Logger.Info("New KeyCount = " + Header.KeyCount); - - // StringBlockSize - Logger.Info("Current StringBlockSize = " + Header.StringBlockSize); - - uint newSize = 0; - - for (int i = 0; i < Header.StringCount; i++) - { - if (Strings[i] != null) - newSize += (uint)ExEncoding.UTF8.GetByteCount(Strings[i]) + 1; // +1 because szString - } - - Header.StringBlockSize = newSize; - Logger.Info("New StringBlockSize = " + Header.StringBlockSize); - - // KeyBlockSize - Logger.Info("Current KeyBlockSize = " + Header.KeyBlockSize); - - // ASCII.GetByteCount() is not needed because all chars are exactly one byte large - Header.KeyBlockSize = Entries[Entries.Count - 1].KeyOffset + (uint)Keys[Keys.Count - 1].Length + 1; // +1 for szString end - - Logger.Info("New KeyBlockSize = " + Header.KeyBlockSize); - } - - /// - /// Update entries - /// This is only needed when the Tags changed - /// - private void UpdateEntries() - { - Logger.Info("Updating entries..."); - - List newEntries = new List(Keys.Count); - newEntries.Add(Entries[0]); // First entry never changes (at least the offset + index, and we don't know what the other values mean) - - for (int i = 1; i < Keys.Count; i++) // Start at 1 - { - // TODO: This doesn't set the Unk fields! But everything still seems to work just fine? - Logger.Info("Creating new GMD_Entry for " + Keys[i - 1]); - GMD_Entry newEntry = new GMD_Entry(); // Create new entry, this happens when we added a new key - - newEntry.Index = (uint)i; - newEntry.KeyOffset = newEntries[i - 1].KeyOffset + (uint)Keys[i - 1].Length + 1; // +1 for szString end - newEntries.Add(newEntry); - } - - Entries = newEntries; - } - /// /// Add string with key /// @@ -249,19 +135,7 @@ private void UpdateEntries() /// public void AddString(string key, string value, int index = -1) { - if (Keys.Contains(key)) - throw new Exception("Can't add duplicate key!"); - - if (index == -1) - { - Keys.Add(key); - Strings.Add(value); - } - else - { - Keys.Insert(index, key); - Strings.Insert(index, value); - } + throw new NotImplementedException(); } /// @@ -270,13 +144,7 @@ public void AddString(string key, string value, int index = -1) /// public void RemoveString(string key) { - int idx = Keys.IndexOf(key); - - if (idx == -1) - throw new Exception($"No string found with key '{key}'"); - - Keys.RemoveAt(idx); - Strings.RemoveAt(idx); + throw new NotImplementedException(); } } } diff --git a/Cirilla.Core/Structs/Native/GMD.cs b/Cirilla.Core/Structs/Native/GMD.cs index 950db46..6aad5d0 100644 --- a/Cirilla.Core/Structs/Native/GMD.cs +++ b/Cirilla.Core/Structs/Native/GMD.cs @@ -1,5 +1,4 @@ using Cirilla.Core.Helpers; -using System; using System.Runtime.InteropServices; using System.Text; @@ -19,32 +18,32 @@ public struct GMD_Header public byte[] Version; // Version? [Endian(Endianness.LittleEndian)] - public UInt32 Language; + public int Language; [Endian(Endianness.LittleEndian)] - public UInt32 Unk1; // Zero + public int Unk1; // Zero // 0x10 [Endian(Endianness.LittleEndian)] - public UInt32 Unk2; // Zero + public int Unk2; // Zero [Endian(Endianness.LittleEndian)] - public UInt32 KeyCount; + public int KeyCount; [Endian(Endianness.LittleEndian)] - public UInt32 StringCount; // Usually the same as KeyCount + public int StringCount; // Usually the same as KeyCount [Endian(Endianness.LittleEndian)] - public UInt32 KeyBlockSize; + public int KeyBlockSize; // 0x20 [Endian(Endianness.LittleEndian)] - public UInt32 StringBlockSize; + public int StringBlockSize; [Endian(Endianness.LittleEndian)] - public UInt32 FilenameLength; + public int FilenameLength; // 0x28 @@ -54,32 +53,32 @@ public struct GMD_Header } [StructLayout(LayoutKind.Sequential, Pack = 4)] - public struct GMD_Entry + public struct GMD_InfoTableEntry { [Endian(Endianness.LittleEndian)] - public UInt32 Index; + public int Index; [Endian(Endianness.LittleEndian)] - public UInt32 Unk2; + public int Unk2; [Endian(Endianness.LittleEndian)] - public UInt32 Unk3; + public int Unk3; [Endian(Endianness.LittleEndian)] - public UInt32 Unk4; + public int Unk4; // 0x10 [Endian(Endianness.LittleEndian)] - public UInt32 KeyOffset; + public int KeyOffset; [Endian(Endianness.LittleEndian)] - public UInt32 Unk6; + public int Unk6; [Endian(Endianness.LittleEndian)] - public UInt32 Unk7; + public int Unk7; [Endian(Endianness.LittleEndian)] - public UInt32 Unk8; + public int Unk8; } } From 6c02e720afb0ee23a1663ea9aa2120d3f13dc139 Mon Sep 17 00:00:00 2001 From: Fusion86 Date: Thu, 13 Sep 2018 22:56:57 +0200 Subject: [PATCH 2/2] Capcom pls... - String loading and saving should be identical to the official/in-game behaviour, including all its weird stuff. - Add GUI color highlights on unused and orphaned (no key) strings. - ReadStringZero no longer ignores 0x00 zero bytes in front of a string, it now returns the string read so far as soon as it sees a 0x00. This means that it could also return null. --- Cirilla.Core.Test/Tests/GMDTests.cs | 51 +++-- Cirilla.Core/Cirilla.Core.csproj | 2 +- .../Extensions/BinaryReaderExtensions.cs | 19 +- Cirilla.Core/Models/GMD.cs | 182 +++++++++++++----- Cirilla.Core/Structs/Native/GMD.cs | 9 +- Cirilla/MainWindow.xaml | 24 +++ Cirilla/Properties/AssemblyInfo.cs | 4 +- Cirilla/ViewModels/GMDViewModel.cs | 14 +- Cirilla/ViewModels/MainWindowViewModel.cs | 2 +- 9 files changed, 204 insertions(+), 103 deletions(-) diff --git a/Cirilla.Core.Test/Tests/GMDTests.cs b/Cirilla.Core.Test/Tests/GMDTests.cs index 9201d66..104b75d 100644 --- a/Cirilla.Core.Test/Tests/GMDTests.cs +++ b/Cirilla.Core.Test/Tests/GMDTests.cs @@ -28,7 +28,6 @@ public void Load__item_eng() [TestMethod] public void Load__armor_eng() { - // StringCount > actual number of strings GMD gmd = new GMD(Utility.GetFullPath(@"chunk0/common/text/steam/armor_eng.gmd")); } @@ -50,6 +49,18 @@ public void Load__action_trial_ara() //Assert.AreEqual(gmd.Entries[3].Value, "©CAPCOM CO., LTD. ALL RIGHTS RESERVED."); } + [TestMethod] + public void Load__cm_facility_eng() + { + GMD gmd = new GMD(Utility.GetFullPath(@"chunk0/common/text/cm_facility_eng.gmd")); + } + + [TestMethod] + public void Load__cm_facility_kor() + { + GMD gmd = new GMD(Utility.GetFullPath(@"chunk0/common/text/cm_facility_kor.gmd")); + } + [TestMethod] public void Rebuild__em_names_eng() { @@ -67,7 +78,7 @@ public void Rebuild__em_names_eng() public void Rebuild__q00503_eng() { string origPath = Utility.GetFullPath(@"chunk0/common/text/quest/q00503_eng.gmd"); - string rebuildPath = "rebuild__q00503_eng"; + string rebuildPath = "rebuild__q00503_eng.gmd"; GMD gmd = new GMD(origPath); gmd.Save(rebuildPath); @@ -99,8 +110,8 @@ public void Rebuild__action_trial_eng() GMD gmd = new GMD(origPath); gmd.Save(rebuildPath); - //if (!Utility.CheckFilesAreSame(origPath, rebuildPath)) - // Assert.Fail("Hash doesn't match!"); + if (!Utility.CheckFilesAreSame(origPath, rebuildPath)) + Assert.Fail("Hash doesn't match!"); } [TestMethod] @@ -112,8 +123,8 @@ public void Rebuild__wep_series_eng() GMD gmd = new GMD(origPath); gmd.Save(rebuildPath); - //if (!Utility.CheckFilesAreSame(origPath, rebuildPath)) - // Assert.Fail("Hash doesn't match!"); + if (!Utility.CheckFilesAreSame(origPath, rebuildPath)) + Assert.Fail("Hash doesn't match!"); } [TestMethod] @@ -145,40 +156,26 @@ public void AddString__q00503_eng() Assert.IsTrue(oldGmd.Header.KeyBlockSize < newGmd.Header.KeyBlockSize); Assert.IsTrue(oldGmd.Header.StringCount < newGmd.Header.StringCount); Assert.IsTrue(oldGmd.Header.StringBlockSize < newGmd.Header.StringBlockSize); - Assert.IsTrue(newGmd.Entries.ContainsValue("New string text....")); + Assert.IsNotNull(newGmd.Entries.FirstOrDefault(x => x.Value == "New string text....")); } [TestMethod] - public void RemoveString__w_sword_eng() + public void ReaddString__w_sword_eng() { + // This won't display correctly in game, because the string order DOES matter + string origPath = Utility.GetFullPath(@"chunk0/common/text/steam/w_sword_eng.gmd"); string newPath = "removestring__w_sword_eng.gmd"; string readdPath = "readdstring__w_sword_eng.gmd"; GMD gmd = new GMD(origPath); - //int idx = gmd.Entries.FindIndex(x => x.Key == "WP_WSWD_044_NAME"); gmd.RemoveString("WP_WSWD_044_NAME"); gmd.Save(newPath); - //GMD newGmd = new GMD(newPath); - - //newGmd.AddString("WP_WSWD_044_NAME", "My new string", idx); - //newGmd.Save(readdPath); - } - - [TestMethod] - public void Shuffle__q00503_eng() - { - string newPath = "shuffle__q00503_eng.gmd"; - - GMD gmd = new GMD(Utility.GetFullPath(@"chunk0/common/text/quest/q00503_eng.gmd")); - - var shuffled = gmd.Entries.OrderBy(x => "A").ToDictionary(item => item.Key, item => item.Value); - gmd.Entries = shuffled; - - gmd.Save(newPath); - GMD newGmd = new GMD(newPath); + + newGmd.AddString("WP_WSWD_044_NAME", "My new string"); + newGmd.Save(readdPath); } } } diff --git a/Cirilla.Core/Cirilla.Core.csproj b/Cirilla.Core/Cirilla.Core.csproj index 6e5c777..f057667 100644 --- a/Cirilla.Core/Cirilla.Core.csproj +++ b/Cirilla.Core/Cirilla.Core.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 0.0.4 + 0.0.5 Fusion86 diff --git a/Cirilla.Core/Extensions/BinaryReaderExtensions.cs b/Cirilla.Core/Extensions/BinaryReaderExtensions.cs index 4ee1ae1..230f6fd 100644 --- a/Cirilla.Core/Extensions/BinaryReaderExtensions.cs +++ b/Cirilla.Core/Extensions/BinaryReaderExtensions.cs @@ -25,7 +25,8 @@ public static T ReadStruct(this BinaryReader br) } /// - /// Read zero terminated string (skips zeros in front of it) + /// Read zero terminated string (skips zeros in front of it). + /// Leaves BaseStream.Position += stringLength + 1 (for szString terminator) /// /// /// @@ -33,7 +34,6 @@ public static T ReadStruct(this BinaryReader br) public static string ReadStringZero(this BinaryReader br, Encoding encoding) { byte b; - int skippedZeros = 0; List szBytes = new List(); while (br.BaseStream.Position != br.BaseStream.Length) @@ -42,13 +42,7 @@ public static string ReadStringZero(this BinaryReader br, Encoding encoding) if (b == 0) { - // Stop if we found a \0 **AND** we already have read some text. - // This is because a string could have empty space in front of it. - // While this is 'undocumented behaviour' it works in-game. - if (szBytes.Count > 0) - break; - else - skippedZeros++; + break; } else { @@ -56,12 +50,7 @@ public static string ReadStringZero(this BinaryReader br, Encoding encoding) } } - string str = encoding.GetString(szBytes.ToArray()); - - if (skippedZeros != 0) - Logger.Warn($"Skipped {skippedZeros} zeros in front of string '{str}'"); - - return str; + return encoding.GetString(szBytes.ToArray()); } } } diff --git a/Cirilla.Core/Models/GMD.cs b/Cirilla.Core/Models/GMD.cs index ae8335e..8b476a5 100644 --- a/Cirilla.Core/Models/GMD.cs +++ b/Cirilla.Core/Models/GMD.cs @@ -14,11 +14,12 @@ public class GMD : FileTypeBase { private static readonly ILog Logger = LogProvider.GetCurrentClassLogger(); - public GMD_Header Header; - public string Filename; - public Dictionary Entries; - - private byte[] Unk1; + public GMD_Header Header => _header; + public string Filename { get; } + public List Entries { get; } + + private GMD_Header _header; + private byte[] _unk1; public GMD(string path) : base(path) { @@ -28,54 +29,74 @@ public GMD(string path) : base(path) using (BinaryReader br = new BinaryReader(fs)) { // Header - Header = br.ReadStruct(); + _header = br.ReadStruct(); - if (Header.MagicString != "GMD") throw new Exception("Not a GMD file!"); + if (_header.MagicString != "GMD") throw new Exception("Not a GMD file!"); // Log some info about the GMD language - if (Enum.IsDefined(typeof(EmLanguage), Header.Language)) + if (Enum.IsDefined(typeof(EmLanguage), _header.Language)) { - EmLanguage language = (EmLanguage)Header.Language; + EmLanguage language = (EmLanguage)_header.Language; Logger.Info("Language: " + language); } else { - Logger.Warn($"Unknown language: 0x{Header.Language:X04} ({Header.Language})"); + Logger.Warn($"Unknown language: 0x{_header.Language:X04} ({_header.Language})"); } // Filename Filename = br.ReadStringZero(ExEncoding.ASCII); - + // Set Entries initial capacity - Entries = new Dictionary(Header.KeyCount); + Entries = new List(_header.StringCount); // Info Table - GMD_InfoTableEntry[] infoTable = new GMD_InfoTableEntry[Header.KeyCount]; - - for (int i = 0; i < Header.KeyCount; i++) + for (int i = 0; i < _header.KeyCount; i++) { - infoTable[i] = br.ReadStruct(); + GMD_Entry entry = new GMD_Entry { InfoTableEntry = br.ReadStruct() }; + + int lastStringIndex = 0; + if (Entries.OfType().Count() > 0) + lastStringIndex = Entries.OfType().Last().InfoTableEntry.StringIndex; + + for (int j = lastStringIndex + 1; j < entry.InfoTableEntry.StringIndex; j++) + Entries.Add(new GMD_EntryWithoutKey()); + + Entries.Add(entry); } // Block with unknown data - Unk1 = br.ReadBytes(0x800); + _unk1 = br.ReadBytes(0x800); - // Keys - string[] keys = new string[Header.KeyCount]; - for (int i = 0; i < Header.KeyCount; i++) - keys[i] = br.ReadStringZero(ExEncoding.ASCII); + // Keys, his skips over the GMD_EntryWithoutKey entries + foreach (GMD_Entry entry in Entries.OfType()) + { + entry.Key = br.ReadStringZero(ExEncoding.ASCII); + } // Strings - for (int i = 0; i < Header.StringCount; i++) + string[] strings = new string[_header.StringCount]; + long startOfStringBlock = fs.Position; + + for (int i = 0; i < _header.StringCount; i++) { if (fs.Position == fs.Length) { - Logger.Warn("We expected more strings, but we already are at the end of the stream."); + Logger.Warn($"Expected to read {_header.StringCount - i} more strings (for a total of {_header.StringCount}). But we already are at the end of the stream!"); break; } - Entries.Add(keys[i], br.ReadStringZero(ExEncoding.UTF8)); + strings[i] = br.ReadStringZero(ExEncoding.UTF8); } + + Logger.Info("Expected StringBlockSize = " + _header.StringBlockSize); + Logger.Info("Actual StringBlockSize = " + (fs.Position - startOfStringBlock)); + + if (_header.StringBlockSize != (fs.Position - startOfStringBlock)) + Logger.Warn("Actual StringBlockSize is not the same as the expected StringBlockSize!"); + + for (int i = 0; i < _header.StringCount; i++) + Entries[i].Value = strings[i]; } } @@ -83,42 +104,34 @@ public void Save(string path) { Logger.Info($"Saving {Filename} to '{path}'"); - Logger.Info("Writing bytes..."); + Update(); using (FileStream fs = File.OpenWrite(path)) using (BinaryWriter bw = new BinaryWriter(fs)) { - bw.Write(Header.ToBytes()); + Logger.Info("Writing bytes..."); + + bw.Write(_header.ToBytes()); bw.Write(ExEncoding.ASCII.GetBytes(Filename)); bw.Write((byte)0); // szString end of string - // Info Table - int idx = 0; - int keyOffset = 0; - foreach (var entry in Entries) + // Info Table, excludes GMD_EntryWithoutKey + foreach (GMD_Entry entry in Entries.OfType()) { - // Generate new entry - GMD_InfoTableEntry infoTableEntry = new GMD_InfoTableEntry(); - infoTableEntry.Index = idx; - infoTableEntry.KeyOffset = keyOffset; - - bw.Write(infoTableEntry.ToBytes()); - - idx++; - keyOffset += entry.Key.Length + 1; // +1 for szString terminator + bw.Write(entry.InfoTableEntry.ToBytes()); } - bw.Write(Unk1); + bw.Write(_unk1); - // Keys - foreach (var entry in Entries) + // Keys, excludes GMD_EntryWithoutKey + foreach (GMD_Entry entry in Entries.OfType()) { bw.Write(ExEncoding.ASCII.GetBytes(entry.Key)); bw.Write((byte)0); // szString end of string } - // Strings - foreach (var entry in Entries) + // Strings, __includes__ GMD_EntryWithoutKey + foreach (IGMD_Entry entry in Entries) { bw.Write(ExEncoding.UTF8.GetBytes(entry.Value)); bw.Write((byte)0); // szString end of string @@ -128,14 +141,69 @@ public void Save(string path) Logger.Info("Saved file!"); } + /// + /// Update header + /// This is needed when anything changes + /// + private void Update() + { + Logger.Info("Updating entries..."); + + var realEntries = Entries.OfType().ToList(); + + // First info entry always has Index = 0 and KeyOffset = 0 + realEntries[0].InfoTableEntry.StringIndex = 0; + realEntries[0].InfoTableEntry.KeyOffset = 0; + + for (int i = 1; i < realEntries.Count; i++) // Start at 1 + { + realEntries[i].InfoTableEntry.StringIndex = Entries.IndexOf(realEntries[i]); + realEntries[i].InfoTableEntry.KeyOffset = realEntries[i - 1].InfoTableEntry.KeyOffset + realEntries[i - 1].Key.Length + 1; // +1 for szString end + } + + Logger.Info("Updating header..."); + + // String Count + Logger.Info("Current StringCount = " + _header.StringCount); + _header.StringCount = Entries.Count; + Logger.Info("New StringCount = " + _header.StringCount); // Key Count + Logger.Info("Current KeyCount = " + _header.KeyCount); + _header.KeyCount = realEntries.Count; + Logger.Info("New KeyCount = " + _header.KeyCount); + + // StringBlockSize + Logger.Info("Current StringBlockSize = " + _header.StringBlockSize); + + int newSize = 0; + foreach (IGMD_Entry entry in Entries) + { + newSize += ExEncoding.UTF8.GetByteCount(entry.Value) + 1; // +1 because szString + } + + _header.StringBlockSize = newSize; + Logger.Info("New StringBlockSize = " + _header.StringBlockSize); + + // KeyBlockSize + Logger.Info("Current KeyBlockSize = " + _header.KeyBlockSize); + + // ASCII.GetByteCount() is not needed because all chars are exactly one byte large + _header.KeyBlockSize = realEntries.Last().InfoTableEntry.KeyOffset + realEntries.Last().Key.Length + 1; // +1 for szString end + + Logger.Info("New KeyBlockSize = " + _header.KeyBlockSize); + } + /// /// Add string with key /// /// /// - public void AddString(string key, string value, int index = -1) + public void AddString(string key, string value) { - throw new NotImplementedException(); + Entries.Add(new GMD_Entry + { + Key = key, + Value = value + }); } /// @@ -144,7 +212,27 @@ public void AddString(string key, string value, int index = -1) /// public void RemoveString(string key) { - throw new NotImplementedException(); + var entry = Entries.OfType().FirstOrDefault(x => x.Key == key); + + if (entry != null) + Entries.Remove(entry); } } + + public interface IGMD_Entry + { + string Value { get; set; } + } + + public class GMD_Entry : IGMD_Entry + { + public GMD_InfoTableEntry InfoTableEntry; + public string Key { get; set; } + public string Value { get; set; } + } + + public class GMD_EntryWithoutKey : IGMD_Entry + { + public string Value { get; set; } + } } diff --git a/Cirilla.Core/Structs/Native/GMD.cs b/Cirilla.Core/Structs/Native/GMD.cs index 6aad5d0..adf19cf 100644 --- a/Cirilla.Core/Structs/Native/GMD.cs +++ b/Cirilla.Core/Structs/Native/GMD.cs @@ -56,7 +56,7 @@ public struct GMD_Header public struct GMD_InfoTableEntry { [Endian(Endianness.LittleEndian)] - public int Index; + public int StringIndex; [Endian(Endianness.LittleEndian)] public int Unk2; @@ -65,7 +65,10 @@ public struct GMD_InfoTableEntry public int Unk3; [Endian(Endianness.LittleEndian)] - public int Unk4; + public short Short1; + + [Endian(Endianness.LittleEndian)] + public short Short2; // 0x10 @@ -80,5 +83,7 @@ public struct GMD_InfoTableEntry [Endian(Endianness.LittleEndian)] public int Unk8; + + // 0x20 } } diff --git a/Cirilla/MainWindow.xaml b/Cirilla/MainWindow.xaml index e5e535d..bb41a7f 100644 --- a/Cirilla/MainWindow.xaml +++ b/Cirilla/MainWindow.xaml @@ -62,8 +62,32 @@ + + + + + + + + diff --git a/Cirilla/Properties/AssemblyInfo.cs b/Cirilla/Properties/AssemblyInfo.cs index 0f02322..ca7f349 100644 --- a/Cirilla/Properties/AssemblyInfo.cs +++ b/Cirilla/Properties/AssemblyInfo.cs @@ -51,5 +51,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.0.3.0")] -[assembly: AssemblyFileVersion("0.0.3.0")] +[assembly: AssemblyVersion("0.0.4.0")] +[assembly: AssemblyFileVersion("0.0.4.0")] diff --git a/Cirilla/ViewModels/GMDViewModel.cs b/Cirilla/ViewModels/GMDViewModel.cs index 4992385..045b1bc 100644 --- a/Cirilla/ViewModels/GMDViewModel.cs +++ b/Cirilla/ViewModels/GMDViewModel.cs @@ -30,11 +30,11 @@ public GMDViewModel(string path) : base(path) HeaderMetadata.Add(new KeyValueViewModel("Filename", _context.Filename, EditCondition.Never, EditCondition.Never)); // Entries - for (int i = 0; i < _context.Header.KeyCount; i++) + for (int i = 0; i < _context.Header.StringCount; i++) { Entries.Add(new KeyValueViewModel( - _context.Keys[i], - _context.Strings[i], + _context.Entries[i].GetType() == typeof(GMD_Entry) ? ((GMD_Entry)_context.Entries[i]).Key : "", + _context.Entries[i].Value, EditCondition.UnsafeOnly, EditCondition.Always )); @@ -45,13 +45,11 @@ public override void Save(string path) { int updatedStrings = 0; - _context.Update(); - - for (int i = 0; i < _context.Header.StringCount; i++) + for (int i = 0; i < _context.Entries.Count; i++) { - if (_context.Strings[i] != Entries[i].Value) + if (_context.Entries[i].Value != Entries[i].Value) { - _context.Strings[i] = Entries[i].Value; + _context.Entries[i].Value = Entries[i].Value; updatedStrings++; } } diff --git a/Cirilla/ViewModels/MainWindowViewModel.cs b/Cirilla/ViewModels/MainWindowViewModel.cs index 30eb1f8..c5df755 100644 --- a/Cirilla/ViewModels/MainWindowViewModel.cs +++ b/Cirilla/ViewModels/MainWindowViewModel.cs @@ -101,7 +101,7 @@ public void SaveInWorkingDirectory() public bool CanSaveInWorkingDirectory() { - if (SelectedItem is FileTypeTabItemViewModelBase item) + if (SelectedItem is FileTypeTabItemViewModelBase item && Properties.Settings.Default.Config.WorkingDirectoryPath != null) return Directory.Exists(Properties.Settings.Default.Config.WorkingDirectoryPath) && GetFullRelativePathForGameFile(item.Filepath) != null;