using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace FF8 { /// /// Loads strings from FF8 files /// public class Strings { #region Fields private readonly string[] filenames = new string[] { "mngrp.bin", "mngrphd.bin", "areames.dc1", "namedic.bin", "kernel.bin" }; private string ArchiveString; private ArchiveWorker aw; //temp storage for locations isn't kept long term. private Dictionary BinMSG; private Dictionary> ComplexStr; private Dictionary files; private FileID last; private BinaryReader localbr; private MemoryStream localms; /// /// Colly's list of string pointers. Adapted. Some strings might be missing. /// /// public Dictionary> Kernel_LocSTR { get; private set; } private bool opened = false; private uint[] StringsLoc; private uint[] StringsPadLoc; public Dictionary Files { get => files; } public string[] Filenames => filenames; #endregion Fields #region Constructors public Strings() => init(); #endregion Constructors #region Enums /// /// filenames of files with strings and id's for structs that hold the data. /// public enum FileID : uint { MNGRP = 0, /// /// only used as holder for the mngrp's map filename /// MNGRP_MAP = 1, AREAMES = 2, NAMEDIC = 3, KERNEL = 4, } /// /// Todo make an enum to id every section. /// public enum SectionID : uint { tkmnmes1 = 0, tkmnmes2 = 1, tkmnmes3 = 2, } #endregion Enums #region Methods public void Close() { if (opened) { localbr.Close(); localbr.Dispose(); opened = false; } } public void GetAW(FileID fileID, bool force = false) { switch (fileID) { case FileID.MNGRP: case FileID.MNGRP_MAP: case FileID.AREAMES: default: ArchiveString = Memory.Archives.A_MENU; break; case FileID.NAMEDIC: case FileID.KERNEL: ArchiveString = Memory.Archives.A_MAIN; break; } if (aw == null || aw.GetPath() != ArchiveString || force) aw = new ArchiveWorker(ArchiveString); } public void Open(FileID fileID) { if (opened) throw new Exception("Must close before opening again"); GetAW(fileID); try { localms = new MemoryStream(aw.GetBinaryFile( aw.GetListOfFiles().First(x => x.IndexOf(filenames[(int)fileID], StringComparison.OrdinalIgnoreCase) >= 0))); } catch { GetAW(fileID, true); localms = new MemoryStream(aw.GetBinaryFile( aw.GetListOfFiles().First(x => x.IndexOf(filenames[(int)fileID], StringComparison.OrdinalIgnoreCase) >= 0))); } localbr = new BinaryReader(localms); opened = true; last = fileID; } //public byte[] Read(FileID fileID, SectionID sectionID, int stringID) => Read(fileID, (int)sectionID, stringID); /// /// Remember to Close() if done using /// /// /// /// /// public FF8String Read(FileID fileID, int sectionID, int stringID) { switch (fileID) { case FileID.MNGRP_MAP: throw new Exception("map file has no string"); case FileID.MNGRP: case FileID.AREAMES: default: return Read(fileID, files[fileID].sPositions[(uint)sectionID][stringID]); } } private void init() { files = new Dictionary(2); GetAW(FileID.MNGRP, true); mngrp_init(); GetAW(FileID.AREAMES); simple_init(FileID.AREAMES); GetAW(FileID.NAMEDIC, true); simple_init(FileID.NAMEDIC); GetAW(FileID.KERNEL); Kernel_init(FileID.KERNEL); } /// /// Fetch strings from kernel.bin /// /// Should be FileID.KERNEL /// private void Kernel_init(FileID fileID) { files[fileID] = new Stringfile(new Dictionary>(56), new List(56)); using (MemoryStream ms = new MemoryStream(aw.GetBinaryFile( aw.GetListOfFiles().First(x => x.IndexOf(filenames[(int)fileID], StringComparison.OrdinalIgnoreCase) >= 0)))) using (BinaryReader br = new BinaryReader(ms)) { kernel_GetFileLocations(br); Kernel_LocSTR = new Dictionary> { //working {0, new Tuple(31,2,4) }, {1, new Tuple(32,2,56) }, {2, new Tuple(33,2,128) }, {3, new Tuple(34,1,18) },//38,58,178, or 78 {4, new Tuple(35,1,10) }, {5, new Tuple(36,2,20) }, {6, new Tuple(37,1,34) },//+1interval 70 //character names here. {7, new Tuple(38,2,20) }, {8, new Tuple(39,1,0) }, {9, new Tuple(40,1,18) }, {11, new Tuple(41,2,4) }, {12, new Tuple(42,2,4) }, {13, new Tuple(43,2,4) }, {14, new Tuple(44,2,4) }, {15, new Tuple(45,2,4) }, {16, new Tuple(46,2,4) }, {17, new Tuple(47,2,4) }, {18, new Tuple(48,2,20) }, {19, new Tuple(49,2,12) }, {21, new Tuple(50,2,20) }, {22, new Tuple(51,2,28) }, {24, new Tuple(52,2,4) }, {25, new Tuple(53,1,18) }, {28, new Tuple(54,1,10) }, {30, new Tuple(55,1,0) }, }; for (uint key = 0; key < files[fileID].subPositions.Count; key++) { Loc fpos = files[fileID].subPositions[(int)key]; bool pad = (Array.IndexOf(StringsPadLoc, key) >= 0); //if (pad || Array.IndexOf(StringsLoc, key) >= 0) // mngrp_get_string_offsets(br, fileID, key, pad); //else if (Kernel_LocSTR.ContainsKey(key)) { mngrp_get_string_BinMSG(br, fileID, key, files[fileID].subPositions[(int)(Kernel_LocSTR[key].Item1)].seek, Kernel_LocSTR[key].Item2, Kernel_LocSTR[key].Item3); } //else if (ComplexStr.ContainsKey(key)) //{ // Mngrp_get_string_ComplexStr(br, fileID, key, ComplexStr[key]); //} } } } private void mngrp_get_string_BinMSG(BinaryReader br, FileID fileID, uint key, uint msgPos, uint grab = 0, uint skip = 0) { Loc fpos = files[fileID].subPositions[(int)key]; br.BaseStream.Seek(fpos.seek, SeekOrigin.Begin); if (files[fileID].sPositions.ContainsKey(key)) { } else { ushort b = 0; ushort last = b; files[fileID].sPositions.Add(key, new List()); uint g = 1; while (br.BaseStream.Position < fpos.max) { b = br.ReadUInt16(); if (last > b) break; else { if (b != 0xFFFF) { files[fileID].sPositions[key].Add(b + msgPos); last = b; } else files[fileID].sPositions[key].Add(0); if (grab > 0 && ++g > grab) { br.BaseStream.Seek(skip, SeekOrigin.Current); g = 1; } } } } } private void Mngrp_get_string_ComplexStr(BinaryReader br, FileID fileID, uint key, List list) { uint[] fPaddings; fPaddings = mngrp_read_padding(br, files[fileID].subPositions[(int)key], 1); files[fileID].sPositions.Add(key, new List()); for (uint p = 0; p < fPaddings.Length; p += 2) { key = list[(int)fPaddings[(int)p + 1]]; Loc fpos = files[fileID].subPositions[(int)key]; uint fpad = fPaddings[p] + fpos.seek; br.BaseStream.Seek(fpad, SeekOrigin.Begin); if (!files[fileID].sPositions.ContainsKey(key)) files[fileID].sPositions.Add(key, new List()); br.BaseStream.Seek(fpad + 6, SeekOrigin.Begin); //byte[] UNK = br.ReadBytes(6); ushort len = br.ReadUInt16(); uint stop = (uint)(br.BaseStream.Position + len - 9); //6 for UNK, 2 for len 1, for end null files[fileID].sPositions[key].Add((uint)br.BaseStream.Position); //entry contains possible more than one string so I am scanning for null while (br.BaseStream.Position + 1 < stop) { byte b = br.ReadByte(); if (b == 0) files[fileID].sPositions[key].Add((uint)br.BaseStream.Position); } } } /// /// TODO: make this work with more than one file. /// /// /// /// /// private void mngrp_get_string_offsets(BinaryReader br, FileID fileID, uint key, bool pad = false) { Loc fpos = files[fileID].subPositions[(int)key]; uint[] fPaddings = pad ? mngrp_read_padding(br, fpos) : (new uint[] { 1 }); files[fileID].sPositions.Add(key, new List()); for (uint p = 0; p < fPaddings.Length; p++) { if (fPaddings[p] <= 0) continue; uint fpad = pad ? fPaddings[p] + fpos.seek : fpos.seek; br.BaseStream.Seek(fpad, SeekOrigin.Begin); if (br.BaseStream.Position + 4 < br.BaseStream.Length) { int count = br.ReadUInt16(); for (int i = 0; i < count && br.BaseStream.Position + 2 < br.BaseStream.Length; i++) { uint c = br.ReadUInt16(); if (c < br.BaseStream.Length && c != 0) { c += fpad; files[fileID].sPositions[key].Add(c); } } } } } private void mngrp_GetFileLocations() { FileID fileID = FileID.MNGRP; using (MemoryStream ms = new MemoryStream(aw.GetBinaryFile( aw.GetListOfFiles().First(x => x.IndexOf(filenames[(int)FileID.MNGRP_MAP], StringComparison.OrdinalIgnoreCase) >= 0)))) using (BinaryReader br = new BinaryReader(ms)) { while (ms.Position < ms.Length) { Loc loc = new Loc() { seek = br.ReadUInt32(), length = br.ReadUInt32() }; if (loc.seek != 0xFFFFFFFF && loc.length != 0x00000000) { loc.seek--; files[fileID].subPositions.Add(loc); } } } } private void kernel_GetFileLocations(BinaryReader br) { FileID fileID = FileID.KERNEL; uint count = br.ReadUInt32(); while (count-- > 0) { Loc l = new Loc { seek = br.ReadUInt32() }; if (count <= 0) l.length = (uint)br.BaseStream.Length - l.seek; else { l.length = br.ReadUInt32() - l.seek; br.BaseStream.Seek(-4, SeekOrigin.Current); } files[fileID].subPositions.Add(l); } } private void mngrp_init() { FileID fileID = FileID.MNGRP; files[fileID] = new Stringfile(new Dictionary>(118), new List(118)); mngrp_GetFileLocations(); using (MemoryStream ms = new MemoryStream(aw.GetBinaryFile( aw.GetListOfFiles().First(x => x.IndexOf(filenames[(int)FileID.MNGRP], StringComparison.OrdinalIgnoreCase) >= 0)))) using (BinaryReader br = new BinaryReader(ms)) { //string contain padding values at start of file //then location data before strings StringsPadLoc = new uint[] { (uint)SectionID.tkmnmes1, (uint)SectionID.tkmnmes2, (uint)SectionID.tkmnmes3 }; //only location data before strings StringsLoc = new uint[] { 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 81, 82, 83, 84, 85, 86, 87, 88, 116}; //complexstr has locations in first file, //and they have 8 bytes of stuff at the start of each entry, 6 bytes UNK and ushort length? //also can have multiple null ending strings per entry. ComplexStr = new Dictionary> { { 74, new List { 75, 76, 77, 78, 79, 80 } } }; //these files come in pairs. the bin has string offsets and 6 bytes of other data //msg is where the strings are. BinMSG = new Dictionary {{106,111},{107,112},{108,113},{109,114},{110,115}}; for (uint key = 0; key < files[fileID].subPositions.Count; key++) { Loc fpos = files[fileID].subPositions[(int)key]; bool pad = (Array.IndexOf(StringsPadLoc, key) >= 0); if (pad || Array.IndexOf(StringsLoc, key) >= 0) mngrp_get_string_offsets(br, fileID, key, pad); else if (BinMSG.ContainsKey(key)) { mngrp_get_string_BinMSG(br, fileID, key, files[fileID].subPositions[(int)BinMSG[key]].seek, 1, 6); } else if (ComplexStr.ContainsKey(key)) { Mngrp_get_string_ComplexStr(br, fileID, key, ComplexStr[key]); } } } } private uint[] mngrp_read_padding(BinaryReader br, Loc fpos, int type = 0) { uint[] fPaddings = null; br.BaseStream.Seek(fpos.seek, SeekOrigin.Begin); uint size = type == 0 ? br.ReadUInt16() : br.ReadUInt32(); fPaddings = new uint[type == 0 ? size : size * type * 2]; for (int i = 0; i < fPaddings.Length; i += 1 + type) { fPaddings[i] = br.ReadUInt16(); if (type == 0 && fPaddings[i] + fpos.seek >= fpos.max) fPaddings[i] = 0; //if (fPaddings[i] != 0) // fPaddings[i] += fpos.seek; for (int j = 1; j < type + 1; j++) { fPaddings[i + j] = br.ReadUInt16(); } } return fPaddings; } public FF8String Read(FileID fileID, uint pos) { //switching archive make sure we are closed before opening another. if (aw != null || aw.GetPath() != ArchiveString || last != fileID) Close(); if (!opened) Open(fileID); return Read(localbr, fileID, pos); } public FF8String Read(BinaryReader br, FileID fid, uint pos) { if (pos == 0) return new FF8String(""); if (pos < br.BaseStream.Length) using (MemoryStream os = new MemoryStream(50)) { br.BaseStream.Seek(pos, SeekOrigin.Begin); int c = 0; byte b = 0; do { if (br.BaseStream.Position > br.BaseStream.Length) break; //sometimes strings start with 00 or 01. But there is another 00 at the end. //I think it's for SeeD test like 1 is right and 0 is wrong. for now i skip them. b = br.ReadByte(); if (b != 0 && b != 1) { os.WriteByte(b); } c++; } while (b != 0 || c == 0); if (os.Length > 0) return os.ToArray(); } return null; } private void simple_init(FileID fileID) { string[] list = aw.GetListOfFiles(); string index = list.First(x => x.IndexOf(filenames[(int)fileID], StringComparison.OrdinalIgnoreCase) >= 0); using (MemoryStream ms = new MemoryStream(aw.GetBinaryFile(index))) using (BinaryReader br = new BinaryReader(ms)) { if (!files.ContainsKey(fileID)) files[fileID] = new Stringfile(new Dictionary>(1), new List(1) { new Loc { seek = 0, length = uint.MaxValue } }); mngrp_get_string_offsets(br, fileID, 0); } } public FF8String GetName(Characters c, Saves.Data d = null) => GetName((Faces.ID)c, d); public FF8String GetName(GFs gf, Saves.Data d = null) => GetName((Faces.ID)((int)gf+16), d); public FF8String GetName(Faces.ID id, Saves.Data d = null ) { if (d == null) d = Memory.State; switch (id) { case Faces.ID.Squall_Leonhart: return d.Squallsname?? Read(FileID.MNGRP,2,92); case Faces.ID.Rinoa_Heartilly: return d.Rinoasname ?? Read(FileID.MNGRP, 2, 93); case Faces.ID.Angelo: return d.Angelosname ?? Read(FileID.MNGRP, 2, 94); case Faces.ID.Boko: return d.Bokosname ?? Read(FileID.MNGRP, 2, 135); case Faces.ID.Zell_Dincht: case Faces.ID.Irvine_Kinneas: case Faces.ID.Quistis_Trepe: case Faces.ID.Selphie_Tilmitt: case Faces.ID.Seifer_Almasy: case Faces.ID.Edea_Kramer: case Faces.ID.Laguna_Loire: case Faces.ID.Kiros_Seagill: case Faces.ID.Ward_Zabac: return Read(FileID.KERNEL, 6, (int)id); case Faces.ID.Quezacotl: case Faces.ID.Shiva: case Faces.ID.Ifrit: case Faces.ID.Siren: case Faces.ID.Brothers: case Faces.ID.Diablos: case Faces.ID.Carbuncle: case Faces.ID.Leviathan: case Faces.ID.Pandemona: case Faces.ID.Cerberus: case Faces.ID.Alexander: case Faces.ID.Doomtrain: case Faces.ID.Bahamut: case Faces.ID.Cactuar: case Faces.ID.Tonberry: case Faces.ID.Eden: return d.GFs[(GFs)((int)id - 16)].Name ?? Read(FileID.MNGRP, 2, 95-16 + (int)id); case Faces.ID.Griever: return d.Grieversname?? Read(FileID.MNGRP, 2, 135); case Faces.ID.MiniMog: return Read(FileID.KERNEL, 0, 72); // also in KERNEL, 12, 36 default: return new FF8String(); } } #endregion Methods //private byte[] Read(FileID fid, uint pos) //{ // try // { // Open(fid); // return Read(localbr, fid, pos); // } // finally // { // Close(); // } //} //private void readfile() //{ // //text is prescrabbled and is ready to draw to screen using font renderer // //based on what I see here some parts of menu ignore \n and some will not //example when // you highlight a item to refine it will only show only on one line up above. // 1 will // refine into 20 Thundaras //and when you select it the whole string will show. // Coral // Fragment: // 1 will refine // into 20 Thundaras // //m000.msg = 104 strings //example = Coral Fragment:\n1 will refine \ninto 20 Thundaras\0 // //I think this one is any item that turns into magic //___ Mag-RF items? except for // upgrade abilities // //m001.msg = 145 strings //same format differnt items //example = Tent:\n4 will refine // into \n1 Mega-Potion\0 //I think this one is any item that turns into another item //___ // Med-RF items? except for upgrade abilities //guessing Ammo-RF is in here too. // //m002.msg = 10 strings //same format differnt items //example = Fire:\n5 will refine // \ninto 1 Fira\0 //this one is magic tha turns into higher level magic //first 4 are Mid // Mag-RF //last 6 are High Mag-RF // //m003.msg = 12 strings //same format differnt items //example = Elixer:\n10 will refine // \ninto 1 Megalixir\0 //this one is Med items tha turns into higher level Med items //all // 12 are Med LV Up // //m004.msg = 110 strings //same format differnt items //example = Geezard Card:\n1 will // refine \ninto 5 Screws\0 //this one is converts cards into items //all 110 are Card Mod // //mwepon.msg = 34 strings //all strings are " " or " " kinda a odd file. // //pet_exp.msg = 18 strings //format: ability name\0description\0 //{0x0340} = Angelo's // name //example: {0x0340} Rush\0 Damage one enemy\0 //list of Angelo's attack names and descriptions // //namedic.bin 32 strings // //Seems to be location names. // //start of file // // UIint16 Count // // UIint16[Count]Location // //at each location // // Byte[Count][Bytes to null] //} } }