diff --git a/Lotd.Core/AnimationNames.txt b/Lotd.Core/AnimationNames.txt new file mode 100644 index 0000000..8d27a1f --- /dev/null +++ b/Lotd.Core/AnimationNames.txt @@ -0,0 +1,105 @@ +[actions] +0 DuelStart +1 "Duel!" +2 TurnChange +8 ShowDialog +10 TurnBegin +12 MainPhase +15 EndPhase +36 Attack +81 PlaceMonster +82 PlaceSpellTrap +87 DrawCard +[animations] +0 Null +1 DuelStart +2 DuelEnd +3 WaitFrame +4 WaitInput +5 PhaseChange +6 TurnChange +7 FieldChange +8 CursorSet +9 BgmUpdate +10 BattleInit +11 BattleSelect +12 BattleAttack +13 BattleRun +14 BattleEnd +15 LifeSet +16 LifeDamage +17 LifeReset +18 HandShuffle +19 HandShow +20 HandOpen +21 DeckShuffle +22 DeckReset +23 DeckFlipTop +24 GraveTop +25 CardLockon +26 CardMove +27 CardSwap +28 CardFlipTurn +29 CardCheat +30 CardSet +31 CardVanish +32 CardBreak +33 CardExplosion +34 CardExclude +35 CardHappen +36 CardDisable +37 CardEquip +38 CardIncTurn +39 CardUpdate +40 ManaSet +41 MonstDeathTurn +42 MonstShuffle +43 TributeSet +44 TributeReset +45 TributeRun +46 MaterialSet +47 MaterialReset +48 MaterialRun +49 TuningSet +50 TuningReset +51 TuningRun +52 ChainSet +53 ChainRun +54 RunSurrender +55 RunDialog +56 RunList +57 RunSummon +58 RunSpSummon +59 RunFusion +60 RunDetail +61 RunCoin +62 RunDice +63 RunYujyo +64 RunSpecialWin +65 RunVija +66 RunExtra +67 RunCommand +68 CutinDraw +69 CutinSummon +70 CutinFusion +71 CutinChain +72 CutinActivate +73 CutinSet +74 CutinReverse +75 CutinTurn +76 CutinFlip +77 CutinTurnEnd +78 CutinDamage +79 CutinBreak +80 CpuThinking +81 HandRundom +82 OverlaySet +83 OverlayReset +84 OverlayRun +85 CutinSuccess +86 ChainEnd +87 LinkSet +88 LinkReset +89 LinkRun +90 RunJanken +91 CutinCoinDice \ No newline at end of file diff --git a/Lotd.Core/BlockedAnimations.txt b/Lotd.Core/BlockedAnimations.txt new file mode 100644 index 0000000..46ea6a5 --- /dev/null +++ b/Lotd.Core/BlockedAnimations.txt @@ -0,0 +1,2 @@ +[actions] +[animations] diff --git a/Lotd.Core/CardCollection.cs b/Lotd.Core/CardCollection.cs new file mode 100644 index 0000000..c81a4ff --- /dev/null +++ b/Lotd.Core/CardCollection.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Lotd +{ + public class CardCollection + { + public List CardIds { get; set; } + + public CardCollection() + { + CardIds = new List(); + } + + public void Add(short cardId) + { + CardIds.Add(cardId); + } + + public void Remove(short cardId) + { + CardIds.Remove(cardId); + } + + public void RemoveAll(short cardId) + { + while (CardIds.Remove(cardId)) + { + } + } + + public void Clear() + { + CardIds.Clear(); + } + + public void Sort() + { + CardIds.Sort(); + } + } +} diff --git a/Lotd.Core/Constants.cs b/Lotd.Core/Constants.cs new file mode 100644 index 0000000..827e7b8 --- /dev/null +++ b/Lotd.Core/Constants.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Lotd +{ + public static class Constants + { + public const GameVersion LatestVersion = GameVersion.LinkEvolution2; + public const int AbsoluteMaxNumCards = 20000;// Keep this as a constant + + public static int GetNumCards(GameVersion version) + { + switch (version) + { + default: + case GameVersion.Lotd: + return 7581; + case GameVersion.LinkEvolution1: + case GameVersion.LinkEvolution2: + return 10166; + } + } + + public static int GetNumCards2(GameVersion version) + { + switch (version) + { + default: + case GameVersion.Lotd: + return GetNumCards(version); + case GameVersion.LinkEvolution1: + case GameVersion.LinkEvolution2: + return 20000; + } + } + public static ushort GetMaxCardId(GameVersion version) + { + switch (version) + { + default: + case GameVersion.Lotd: + return 12432; + case GameVersion.LinkEvolution1: + case GameVersion.LinkEvolution2: + return 14969; + } + } + + /// + /// Number of duel series (YuGiOh, GX, 5D, ZEXAL, ARCV) + /// + public static int GetNumDuelSeries(GameVersion version) + { + switch (version) + { + default: + case GameVersion.Lotd: + return 5; + case GameVersion.LinkEvolution1: + case GameVersion.LinkEvolution2: + return 6; + } + } + + /// + /// Number of available user deck slots which can be created in in the deck editor + /// + public const int NumUserDecks = 32; + + /// + /// Number of available battle packs (all sealed packs + all draft packs) + /// + public const int NumBattlePacks = 5; + + /// + /// Number of deck slots available which map into deckdata_X.bin + /// + public static int GetNumDeckDataSlots(GameVersion version) + { + switch (version) + { + default: + case GameVersion.Lotd: + return 477; + case GameVersion.LinkEvolution1: + case GameVersion.LinkEvolution2: + return 700; + } + } + + /// + /// The length of a deck name (this is technically 32 with a null terminator) + /// + public const int DeckNameLen = 33; + + /// + /// Number of usable characters in a deck name (1 less than DeckNameLen as 1 is reversed for null terminator) + /// + public const int DeckNameUsableLen = 32; + + /// + /// The length of a deck name in bytes + /// + public const int DeckNameByteLen = DeckNameLen * 2; + + /// + /// Number of slots available in data for main deck cards + /// + public const int NumMainDeckCards = 60; + + /// + /// Number of slots available in data for side deck cards + /// + public const int NumSideDeckCards = 15; + + /// + /// Number of slots available in data for extra deck cards + /// + public const int NumExtraDeckCards = 15; + + public const int NumMinMainDeckCards = 40; + + public const int NumMinMainDeckCardsSpeedDuel = 20; + public const int NumMainDeckCardsSpeedDuel = 30; + public const int NumExtraDeckCardsSpeedDuel = 5; + public const int NumSideDeckCardsSpeedDuel = 5; + + /// + /// The starting deck index for user deck indexes in memory (32 user decks) + /// + public const int DeckIndexUserStart = 0; + /// + /// The starting deck index for data/ydc deck indexes in memory (477 ydc decks) (note that index 0 is an empty entry) + /// + public const int DeckIndexYdcStart = 32; + + /// + /// Max number of players there can be (4 defined by tag duel) + /// + public const int MaxNumPlayers = 4; + + /// + /// The default life points for each player (8000) + /// + public const int DefaultLifePoints = 8000; + } +} diff --git a/Lotd.Core/DuelSeries.cs b/Lotd.Core/DuelSeries.cs new file mode 100644 index 0000000..cd052d3 --- /dev/null +++ b/Lotd.Core/DuelSeries.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Lotd +{ + public enum DuelSeries + { + None = -1, + YuGiOh = 0, + YuGiOhGX = 1, + YuGiOh5D = 2, + YuGiOhZEXAL = 3, + YuGiOhARCV = 4, + YuGiOhVRAINS = 5 + } +} diff --git a/Lotd.Core/Endian.cs b/Lotd.Core/Endian.cs new file mode 100644 index 0000000..c43d1b2 --- /dev/null +++ b/Lotd.Core/Endian.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Lotd +{ + public static class Endian + { + public static bool IsLittleEndian + { + get { return BitConverter.IsLittleEndian; } + } + + public static short ConvertInt16(short value) + { + return System.Net.IPAddress.HostToNetworkOrder(value); + } + + public static ushort ConvertUInt16(ushort value) + { + return (ushort)System.Net.IPAddress.HostToNetworkOrder((short)value); + } + + public static int ConvertInt32(int value) + { + return System.Net.IPAddress.HostToNetworkOrder(value); + } + + public static uint ConvertUInt32(uint value) + { + return (uint)System.Net.IPAddress.HostToNetworkOrder((int)value); + } + + public static long ConvertInt64(long value) + { + return System.Net.IPAddress.HostToNetworkOrder(value); + } + + public static ulong ConvertUInt64(ulong value) + { + return (ulong)System.Net.IPAddress.HostToNetworkOrder((long)value); + } + + public static float ConvertSingle(float value) + { + byte[] buffer = BitConverter.GetBytes(value); + Array.Reverse(buffer); + return BitConverter.ToSingle(buffer, 0); + } + + public static double ConvertDouble(double value) + { + byte[] buffer = BitConverter.GetBytes(value); + Array.Reverse(buffer); + return BitConverter.ToDouble(buffer, 0); + } + + public static short ToInt16(byte[] value, int startIndex) + { + return ToInt16(value, startIndex, IsLittleEndian); + } + + public static short ToInt16(byte[] value, int startIndex, bool convert) + { + short result = BitConverter.ToInt16(value, startIndex); + return convert ? ConvertInt16(result) : result; + } + + public static ushort ToUInt16(byte[] value, int startIndex) + { + return ToUInt16(value, startIndex, IsLittleEndian); + } + + public static ushort ToUInt16(byte[] value, int startIndex, bool convert) + { + ushort result = BitConverter.ToUInt16(value, startIndex); + return convert ? ConvertUInt16(result) : result; + } + + public static int ToInt32(byte[] value, int startIndex) + { + return ToInt32(value, startIndex, IsLittleEndian); + } + + public static int ToInt32(byte[] value, int startIndex, bool convert) + { + int result = BitConverter.ToInt32(value, startIndex); + return convert ? ConvertInt32(result) : result; + } + + public static uint ToUInt32(byte[] value, int startIndex) + { + return ToUInt32(value, startIndex, IsLittleEndian); + } + + public static uint ToUInt32(byte[] value, int startIndex, bool convert) + { + uint result = BitConverter.ToUInt32(value, startIndex); + return convert ? ConvertUInt32(result) : result; + } + + public static long ToInt64(byte[] value, int startIndex) + { + return ToInt64(value, startIndex, IsLittleEndian); + } + + public static long ToInt64(byte[] value, int startIndex, bool convert) + { + long result = BitConverter.ToInt64(value, startIndex); + return convert ? ConvertInt64(result) : result; + } + + public static ulong ToUInt64(byte[] value, int startIndex) + { + return ToUInt64(value, startIndex, IsLittleEndian); + } + + public static ulong ToUInt64(byte[] value, int startIndex, bool convert) + { + ulong result = BitConverter.ToUInt64(value, startIndex); + return convert ? ConvertUInt64(result) : result; + } + + public static float ToSingle(byte[] value, int startIndex) + { + return ToSingle(value, startIndex, IsLittleEndian); + } + + public static float ToSingle(byte[] value, int startIndex, bool convert) + { + float result = BitConverter.ToSingle(value, startIndex); + return convert ? ConvertSingle(result) : result; + } + + public static double ToDouble(byte[] value, int startIndex) + { + return ToDouble(value, startIndex, IsLittleEndian); + } + + public static double ToDouble(byte[] value, int startIndex, bool convert) + { + double result = BitConverter.ToDouble(value, startIndex); + return convert ? ConvertDouble(result) : result; + } + + public static byte[] GetBytes(short value) + { + return GetBytes(value, IsLittleEndian); + } + + public static byte[] GetBytes(short value, bool convert) + { + byte[] result = BitConverter.GetBytes(value); + if (convert) Array.Reverse(result); + return result; + } + + public static byte[] GetBytes(ushort value) + { + return GetBytes(value, IsLittleEndian); + } + + public static byte[] GetBytes(ushort value, bool convert) + { + byte[] result = BitConverter.GetBytes(value); + if (convert) Array.Reverse(result); + return result; + } + + public static byte[] GetBytes(int value) + { + return GetBytes(value, IsLittleEndian); + } + + public static byte[] GetBytes(int value, bool convert) + { + byte[] result = BitConverter.GetBytes(value); + if (convert) Array.Reverse(result); + return result; + } + + public static byte[] GetBytes(uint value) + { + return GetBytes(value, IsLittleEndian); + } + + public static byte[] GetBytes(uint value, bool convert) + { + byte[] result = BitConverter.GetBytes(value); + if (convert) Array.Reverse(result); + return result; + } + + public static byte[] GetBytes(long value) + { + return GetBytes(value, IsLittleEndian); + } + + public static byte[] GetBytes(long value, bool convert) + { + byte[] result = BitConverter.GetBytes(value); + if (convert) Array.Reverse(result); + return result; + } + + public static byte[] GetBytes(ulong value) + { + return GetBytes(value, IsLittleEndian); + } + + public static byte[] GetBytes(ulong value, bool convert) + { + byte[] result = BitConverter.GetBytes(value); + if (convert) Array.Reverse(result); + return result; + } + + public static byte[] GetBytes(float value) + { + return GetBytes(value, IsLittleEndian); + } + + public static byte[] GetBytes(float value, bool convert) + { + byte[] result = BitConverter.GetBytes(value); + if (convert) Array.Reverse(result); + return result; + } + + public static byte[] GetBytes(double value) + { + return GetBytes(value, IsLittleEndian); + } + + public static byte[] GetBytes(double value, bool convert) + { + byte[] result = BitConverter.GetBytes(value); + if (convert) Array.Reverse(result); + return result; + } + } +} diff --git a/Lotd.Core/Extensions.cs b/Lotd.Core/Extensions.cs new file mode 100644 index 0000000..2a689d8 --- /dev/null +++ b/Lotd.Core/Extensions.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd +{ + static class Extensions + { + public static byte[] ReadBytes(this BinaryReader reader, long count) + { + return reader.ReadBytes((int)count); + } + + public static string ReadNullTerminatedString(this BinaryReader reader, Encoding encoding) + { + StringBuilder stringBuilder = new StringBuilder(); + StreamReader streamReader = new StreamReader(reader.BaseStream, encoding); + + long startOffset = reader.BaseStream.Position; + + int intChar; + while ((intChar = streamReader.Read()) != -1) + { + char c = (char)intChar; + if (c == '\0') + { + break; + } + stringBuilder.Append(c); + } + + string result = stringBuilder.ToString(); + + // StreamReader breaks the offset by reading too much. Get the actual amount of bytes read. + reader.BaseStream.Position = startOffset + encoding.GetByteCount(result + '\0'); + + return result; + } + + public static void WriteNullTerminatedString(this BinaryWriter writer, string str, Encoding encoding) + { + writer.Write(encoding.GetBytes(str == null ? string.Empty : str + '\0')); + } + + public static byte[] GetBytes(this Encoding encoding, string str, int bufferLen, int maxStringLen) + { + if (str == null) + { + str = string.Empty; + } + if (maxStringLen >= 0 && str.Length > maxStringLen) + { + str = str.Substring(0, maxStringLen); + } + + byte[] buffer = new byte[bufferLen]; + byte[] tempBuffer = encoding.GetBytes(str == null ? string.Empty : str); + Buffer.BlockCopy(tempBuffer, 0, buffer, 0, Math.Min(bufferLen, tempBuffer.Length)); + return buffer; + } + + public static void WriteOffset(this BinaryWriter writer, long relativeTo, int offset) + { + writer.Write((int)(offset - relativeTo)); + } + + public static void WriteOffset(this BinaryWriter writer, long relativeTo, uint offset) + { + writer.Write((uint)(offset - relativeTo)); + } + + public static void WriteOffset(this BinaryWriter writer, long relativeTo, long offset) + { + writer.Write(offset - relativeTo); + } + } +} diff --git a/Lotd.Core/FileFormats/FileData.cs b/Lotd.Core/FileFormats/FileData.cs new file mode 100644 index 0000000..e948444 --- /dev/null +++ b/Lotd.Core/FileFormats/FileData.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + public abstract class FileData + { + public LotdFile File { get; set; } + public ZibFile ZibFile { get; set; } + + public LotdFileType FileType + { + get { return File != null ? File.FileType : ZibFile != null ? ZibFile.FileType : LotdFileType.Unknown; } + } + + public virtual bool IsLocalized + { + get { return false; } + } + + public byte[] LoadBuffer() + { + if (File.IsFileOnDisk) + { + return LoadBuffer(File.FilePathOnDisk); + } + else if (File.IsArchiveFile) + { + return LoadBuffer(File.Archive.Reader); + } + else + { + return null; + } + } + + public bool Load() + { + if (IsLocalized) + { + return Load(GetLanguage()); + } + + if (ZibFile != null) + { + if (ZibFile.IsFileOnDisk) + { + if (!System.IO.File.Exists(ZibFile.FilePathOnDisk)) + { + return false; + } + Load(ZibFile.FilePathOnDisk); + return true; + } + else if (ZibFile.Owner != null && ZibFile.Owner.File != null && ZibFile.Offset > 0 && ZibFile.Length > 0) + { + ZibFile.Owner.File.Archive.Reader.BaseStream.Position = ZibFile.Owner.File.ArchiveOffset + ZibFile.Offset; + Load(ZibFile.Owner.File.Archive.Reader, ZibFile.Length); + return true; + } + else + { + return false; + } + } + else if (File != null) + { + if (File.IsFileOnDisk) + { + if (!System.IO.File.Exists(File.FilePathOnDisk)) + { + return false; + } + Load(File.FilePathOnDisk); + return true; + } + else if (File.IsArchiveFile) + { + if (File.CanLoadArchive) + { + File.Archive.Reader.BaseStream.Position = File.ArchiveOffset; + Load(File.Archive.Reader, File.ArchiveLength); + } + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } + + public bool Load(Language language) + { + if (ZibFile != null) + { + if (ZibFile.IsFileOnDisk) + { + if (!System.IO.File.Exists(ZibFile.FilePathOnDisk)) + { + return false; + } + Load(ZibFile.FilePathOnDisk, language); + return true; + } + else if (ZibFile.Owner != null && ZibFile.Owner.File != null && ZibFile.Offset > 0 && ZibFile.Length > 0) + { + ZibFile.Owner.File.Archive.Reader.BaseStream.Position = ZibFile.Owner.File.ArchiveOffset + ZibFile.Offset; + Load(ZibFile.Owner.File.Archive.Reader, ZibFile.Length, language); + return true; + } + else + { + return false; + } + } + else if (File != null) + { + if (File.IsFileOnDisk) + { + if (!System.IO.File.Exists(File.FilePathOnDisk)) + { + return false; + } + Load(File.FilePathOnDisk, language); + return true; + } + else if (File.IsArchiveFile) + { + if (File.CanLoadArchive) + { + File.Archive.Reader.BaseStream.Position = File.ArchiveOffset; + Load(File.Archive.Reader, File.ArchiveLength); + } + return true; + } + else + { + return false; + } + } + else + { + return false; + } + } + + public byte[] LoadBuffer(BinaryReader reader) + { + File.Archive.Reader.BaseStream.Position = File.ArchiveOffset; + return reader.ReadBytes((int)File.ArchiveLength); + } + + public byte[] LoadBuffer(string path) + { + if (System.IO.File.Exists(path)) + { + return System.IO.File.ReadAllBytes(path); + } + return null; + } + + public virtual void Load(BinaryReader reader, long length) + { + if (IsLocalized) + { + Load(reader, length, GetLanguage()); + } + } + + public virtual void Load(BinaryReader reader, long length, Language language) + { + throw new NotImplementedException("Localized file doesn't implement Load function"); + } + + public virtual void Save(BinaryWriter writer) + { + if (IsLocalized) + { + Save(writer, GetLanguage()); + } + } + + public virtual void Save(BinaryWriter writer, Language language) + { + throw new NotImplementedException("Localized file doesn't implement Save function"); + } + + public void Load(string path) + { + using (BinaryReader reader = new BinaryReader(System.IO.File.OpenRead(path))) + { + Load(reader, reader.BaseStream.Length); + } + } + + public void Load(string path, Language language) + { + using (BinaryReader reader = new BinaryReader(System.IO.File.OpenRead(path))) + { + Load(reader, reader.BaseStream.Length, language); + } + } + + public void Save(string path) + { + using (BinaryWriter writer = new BinaryWriter(System.IO.File.Create(path))) + { + Save(writer); + } + } + + public void Save(string path, Language language) + { + using (BinaryWriter writer = new BinaryWriter(System.IO.File.Create(path))) + { + Save(writer, language); + } + } + + public virtual void Clear() + { + } + + private Language GetLanguage() + { + if (File != null) + { + return LotdFile.GetLanguageFromFileName(File.Name); + } + else if (ZibFile != null) + { + return LotdFile.GetLanguageFromFileName(ZibFile.FileName); + } + return Language.Unknown; + } + + public virtual void Dump(string outputDir) + { + Dump(new DumpSettings(outputDir)); + } + + public virtual void Dump(DumpSettings settings) + { + ShallowDump(settings); + } + + private void ShallowDump(DumpSettings settings) + { + if (File != null) + { + byte[] buffer = LoadBuffer(); + if (buffer != null) + { + string outputDir = settings.OutputDirectory; + outputDir = Path.Combine(outputDir == null ? string.Empty : outputDir, File.Directory.FullName); + if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir)) + { + Directory.CreateDirectory(outputDir); + } + System.IO.File.WriteAllBytes(Path.Combine(outputDir, File.Name), buffer); + } + } + } + + public virtual void Unload() + { + } + + protected int GetStringSize(string str, Encoding encoding) + { + return encoding.GetByteCount((str == null ? string.Empty : str) + '\0'); + } + + public LotdFile GetLocalizedFile(Language language) + { + return File == null ? null : File.GetLocalizedFile(language); + } + + public ZibFile GetLocalizedZibFile(Language language) + { + return ZibFile == null ? null : ZibFile.GetLocalizedFile(language); + } + } +} diff --git a/Lotd.Core/FileFormats/RawFileData.cs b/Lotd.Core/FileFormats/RawFileData.cs new file mode 100644 index 0000000..e2b87b8 --- /dev/null +++ b/Lotd.Core/FileFormats/RawFileData.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + public class RawFileData : FileData + { + public byte[] Buffer { get; set; } + + public override void Load(BinaryReader reader, long length) + { + Buffer = reader.ReadBytes(length); + } + + public override void Save(BinaryWriter writer) + { + if (Buffer != null) + { + writer.Write(Buffer); + } + } + } +} diff --git a/Lotd.Core/FileFormats/ZibData.cs b/Lotd.Core/FileFormats/ZibData.cs new file mode 100644 index 0000000..165d6cb --- /dev/null +++ b/Lotd.Core/FileFormats/ZibData.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + public class ZibData : FileData + { + private const int align = 16;// 16 byte alignment on file data + private Encoding stringEncoding = Encoding.ASCII; + public Dictionary Files { get; private set; } + + public ZibData() + { + Files = new Dictionary(); + } + + public override void Load(BinaryReader reader, long length) + { + bool longOffsets = IsLongOffsetFile(reader); + + bool firstFile = true; + while (true) + { + long fileOffset = ReadOffsetLength(reader, longOffsets); + long fileLength = ReadOffsetLength(reader, longOffsets); + + if (fileOffset == 0 && fileLength == 0) + { + break; + } + + if (firstFile && !longOffsets) + { + // Files which use 4 bytes for offsets/len seem to have an incorrect offset for the first file. + // These files also have 8 bytes of 00 padding between the last file info and the actual content. + // - It could be possible that the native client always reads 8 bytes for offset/len and determines + // if it should be 4 byte offset? Then that somehow impacts the first file offset as part of the calculation. + fileOffset--; + } + firstFile = false; + + string fileName = ReadString(reader, 64 - (longOffsets ? 16 : 8)); + Files.Add(fileName, new ZibFile(this, fileName, fileOffset, fileLength)); + } + } + + public override void Save(BinaryWriter writer) + { + bool longOffsets = IsLongOffsetFile(); + + long writerOffsetsStart = writer.BaseStream.Position; + Dictionary offsetOffsets = new Dictionary(); + + List orderedFiles = new List(); + foreach (ZibFile file in Files.Values) + { + if (file.IsValid) + { + orderedFiles.Add(file); + } + } + orderedFiles = orderedFiles.OrderBy(x => x.FileName).ToList(); + + byte[] tempBuffer = new byte[64]; + foreach (ZibFile file in orderedFiles) + { + offsetOffsets.Add(file, writer.BaseStream.Position); + writer.Write(tempBuffer); + } + + writer.Write((long)0); + writer.Write((long)0); + + bool firstFile = true; + foreach (ZibFile file in orderedFiles) + { + long writerOffset = writer.BaseStream.Position; + + byte[] fileData = file.Load(); + long dataOffset = writerOffset; + long dataLength = fileData.Length; + + if (firstFile && !longOffsets) + { + dataOffset++; + } + firstFile = false; + + writer.BaseStream.Position = offsetOffsets[file]; + WriteOffsetLength(writer, longOffsets, dataOffset); + WriteOffsetLength(writer, longOffsets, dataLength); + WriteString(writer, longOffsets, file.FileName); + + writer.BaseStream.Position = writerOffset; + writer.Write(fileData); + + if (fileData.Length % align != 0) + { + writer.Write(new byte[align - (fileData.Length % align)]); + } + } + } + + private string ReadString(BinaryReader reader, int length) + { + return stringEncoding.GetString(reader.ReadBytes(length)).TrimEnd('\0'); + } + + private long ReadOffsetLength(BinaryReader reader, bool longOffsets) + { + if (longOffsets) + { + return Endian.ConvertInt64(reader.ReadInt64()); + } + else + { + return Endian.ConvertUInt32(reader.ReadUInt32()); + } + } + + private void WriteString(BinaryWriter writer, bool longOffsets, string value) + { + byte[] buffer = stringEncoding.GetBytes(value); + int padding = (64 - (longOffsets ? 16 : 8)) - buffer.Length; + if (padding < 0) + { + throw new Exception("File name too long " + value); + } + writer.Write(buffer); + writer.Write(new byte[padding]); + } + + private void WriteOffsetLength(BinaryWriter writer, bool longOffsets, long value) + { + if (longOffsets) + { + writer.Write(Endian.ConvertInt64(value)); + } + else + { + writer.Write(Endian.ConvertUInt32((uint)value)); + } + } + + private bool IsLongOffsetFile(BinaryReader reader) + { + // TODO: Come up with a more generic method of calculating this based on the data + return IsLongOffsetFile(); + } + + private bool IsLongOffsetFile() + { + return File != null && File.Name.Contains("cardcrop"); + } + + public override void Dump(string outputDir) + { + Dump(outputDir, File.Archive.Reader); + } + + public override void Dump(DumpSettings settings) + { + if (settings.Deep) + { + Dump(settings.OutputDirectory, File.Archive.Reader); + } + else + { + base.Dump(settings); + } + } + + public void Dump(string outputDir, BinaryReader reader) + { + if (outputDir == null) + { + outputDir = string.Empty; + } + if (File != null) + { + outputDir = Path.Combine(outputDir, Path.GetFileNameWithoutExtension(File.FullName)); + } + if (!string.IsNullOrEmpty(outputDir) && !Directory.Exists(outputDir)) + { + Directory.CreateDirectory(outputDir); + } + + foreach (ZibFile file in Files.Values) + { + System.IO.File.WriteAllBytes(Path.Combine(outputDir, file.FileName), file.Load(reader)); + } + } + } + + public class ZibFile + { + public ZibData Owner { get; set; } + public string FileName { get; set; } + public long Offset { get; set; } + public long Length { get; set; } + + public string FilePathOnDisk { get; set; } + + public bool IsFileOnDisk + { + get { return !string.IsNullOrEmpty(FilePathOnDisk) && File.Exists(FilePathOnDisk); } + } + + public string Extension + { + get { return Path.GetExtension(FileName); } + } + + public LotdFileType FileType + { + get { return LotdFile.GetFileTypeFromExtension(FileName, Extension); } + } + + public bool IsValid + { + get + { + if (string.IsNullOrEmpty(FileName) || FileName.Length > 64 - 16) + { + return false; + } + if (!string.IsNullOrEmpty(FilePathOnDisk) && File.Exists(FilePathOnDisk)) + { + return true; + } + return Offset != 0 && Length != 0; + } + } + + public ZibFile(ZibData owner, string fileName, long offset, long length) + { + Owner = owner; + FileName = fileName; + Offset = offset; + Length = length; + } + + public long CalculateLength() + { + if (!string.IsNullOrEmpty(FilePathOnDisk) && File.Exists(FilePathOnDisk)) + { + return new FileInfo(FilePathOnDisk).Length; + } + return Length; + } + + public byte[] Load() + { + return Load(Owner.File.Archive.Reader); + } + + public byte[] Load(BinaryReader reader) + { + if (!string.IsNullOrEmpty(FilePathOnDisk) && File.Exists(FilePathOnDisk)) + { + return File.ReadAllBytes(FilePathOnDisk); + } + if (Offset == 0 && Length == 0) + { + return null; + } + + if (Owner.File != null && Owner.File.IsArchiveFile) + { + reader.BaseStream.Position = Owner.File.ArchiveOffset + Offset; + } + else + { + reader.BaseStream.Position = Offset; + } + return reader.ReadBytes(Length); + } + + public byte[] LoadBuffer() + { + if (IsFileOnDisk) + { + if (!File.Exists(FilePathOnDisk)) + { + return null; + } + return File.ReadAllBytes(FilePathOnDisk); + } + else if (Owner != null && Owner.File != null && Offset > 0 && Length > 0) + { + Owner.File.Archive.Reader.BaseStream.Position = Owner.File.ArchiveOffset + Offset; + return Owner.File.Archive.Reader.ReadBytes(Length); + } + + return null; + } + + public T LoadData() where T : FileData + { + return LoadData() as T; + } + + public T LoadData(bool cache) where T : FileData + { + return LoadData(cache) as T; + } + + public FileData LoadData() + { + return LoadData(true); + } + + public FileData LoadData(bool cache) + { + FileData fileData = LotdFile.CreateFileData(LotdFile.GetFileTypeFromExtension(FileName, Extension)); + + if (IsFileOnDisk) + { + if (!File.Exists(FilePathOnDisk)) + { + return null; + } + fileData.Load(FilePathOnDisk); + return fileData; + } + else if (Owner != null && Owner.File != null && Offset > 0 && Length > 0) + { + fileData.ZibFile = this; + Owner.File.Archive.Reader.BaseStream.Position = Owner.File.ArchiveOffset + Offset; + fileData.Load(Owner.File.Archive.Reader, Length); + return fileData; + } + + return null; + } + + public ZibFile GetLocalizedFile(Language language) + { + if (LotdFile.GetLanguageFromFileName(FileName) == language) + { + return this; + } + + string fileName = LotdFile.GetFileNameWithLanguage(FileName, language); + if (!string.IsNullOrEmpty(fileName)) + { + ZibFile file; + Owner.Files.TryGetValue(fileName, out file); + return file; + } + + return null; + } + } +} diff --git a/Lotd.Core/FileFormats/bin/CardLimits.cs b/Lotd.Core/FileFormats/bin/CardLimits.cs new file mode 100644 index 0000000..5d62818 --- /dev/null +++ b/Lotd.Core/FileFormats/bin/CardLimits.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + /// + /// Holds limited / banned card information (pd_limits.bin) + /// + public class CardLimits : FileData + { + public HashSet Forbidden { get; private set; } + public HashSet Limited { get; private set; } + public HashSet SemiLimited { get; private set; } + + public CardLimits() + { + Forbidden = new HashSet(); + Limited = new HashSet(); + SemiLimited = new HashSet(); + } + + public override void Load(BinaryReader reader, long length) + { + ReadCardIds(reader, Forbidden); + ReadCardIds(reader, Limited); + ReadCardIds(reader, SemiLimited); + } + + public override void Save(BinaryWriter writer) + { + WriteCardIds(writer, Forbidden); + WriteCardIds(writer, Limited); + WriteCardIds(writer, SemiLimited); + } + + private void ReadCardIds(BinaryReader reader, HashSet cardIds) + { + cardIds.Clear(); + + short count = reader.ReadInt16(); + for (int i = 0; i < count; i++) + { + cardIds.Add(reader.ReadInt16()); + } + } + + private void WriteCardIds(BinaryWriter writer, HashSet cardIds) + { + writer.Write((short)cardIds.Count); + foreach (ushort cardId in cardIds) + { + writer.Write(cardId); + } + } + } +} diff --git a/Lotd.Core/FileFormats/bin/CardManager.cs b/Lotd.Core/FileFormats/bin/CardManager.cs new file mode 100644 index 0000000..bf3b932 --- /dev/null +++ b/Lotd.Core/FileFormats/bin/CardManager.cs @@ -0,0 +1,1802 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + // TODO: Split some of this into data files + + public class CardManager + { + public Manager Manager { get; private set; } + public Dictionary Cards { get; private set; } + public List CardsByIndex { get; private set; } + + // Card name types / archetypes + public Dictionary> CardNameTypes { get; private set; } + + // Tags used for finding related cards which is used to display related cards on the left/right panels in deck edit + public List Tags { get; private set; } + + private Dictionary> cardsByName; + + public CardManager(Manager manager) + { + Manager = manager; + Cards = new Dictionary(); + CardsByIndex = new List(); + Tags = new List(); + cardsByName = new Dictionary>(); + CardNameTypes = new Dictionary>(); + } + + public CardInfo FindCardByName(Language language, string name) + { + CardInfo cardInfo; + cardsByName[language].TryGetValue(name, out cardInfo); + return cardInfo; + } + + public void Load() + { + Cards.Clear(); + CardsByIndex.Clear(); + cardsByName.Clear(); + Tags.Clear(); + + LotdArchive archive = Manager.Archive; + + Dictionary indx = archive.LoadLocalizedBuffer("CARD_Indx_", true); + Dictionary names = archive.LoadLocalizedBuffer("CARD_Name_", true); + Dictionary descriptions = archive.LoadLocalizedBuffer("CARD_Desc_", true); + Dictionary taginfos = archive.LoadLocalizedBuffer("taginfo_", true); + + List allCardImages = new List(); + List imageFiles = new List(); + switch (Manager.Version) + { + case GameVersion.Lotd: + imageFiles.Add("cardcropHD400.jpg.zib"); + imageFiles.Add("cardcropHD401.jpg.zib"); + break; + case GameVersion.LinkEvolution2: + imageFiles.Add("2020.full.illust_a.jpg.zib"); + imageFiles.Add("2020.full.illust_j.jpg.zib"); + break; + } + foreach (string imageFile in imageFiles) + { + allCardImages.AddRange(archive.Root.FindFile(imageFile).LoadData().Files.Values); + } + + Dictionary cardImagesById = new Dictionary(); + foreach (ZibFile file in allCardImages) + { + short cardId = short.Parse(file.FileName.Substring(0, file.FileName.IndexOf('.'))); + cardImagesById[cardId] = file; + } + + List cards = new List(); + foreach (Language language in Enum.GetValues(typeof(Language))) + { + if (language != Language.Unknown) + { + LoadCardNamesAndDescriptions(language, cards, indx, names, descriptions); + + Dictionary languageCardsByName = new Dictionary(); + cardsByName.Add(language, languageCardsByName); + foreach (CardInfo card in cards) + { + // This will wipe over cards with conflicting names + languageCardsByName[card.Name.GetText(language)] = card; + } + } + } + CardsByIndex.AddRange(cards); + + // Load card props (card id, atk, def, level, attribute, etc) + LoadCardProps(cards, Cards, cardImagesById); + + ProcessLimitedCardList(Cards); + LoadCardGenre(cards); + LoadRelatedCards(cards, Cards, Tags, taginfos); + LoadCardNameTypes(Cards, CardNameTypes); + + //PrintLimitedCardList(); + } + + /// + /// Loads the card name types / archetypes (e.g. "Harpie") + /// + private void LoadCardNameTypes(Dictionary cards, Dictionary> cardNameTypes) + { + using (BinaryReader reader = new BinaryReader(new MemoryStream(Manager.Archive.Root.FindFile("bin/CARD_Named.bin").LoadBuffer()))) + { + ushort numArchetypes = reader.ReadUInt16(); + ushort numCards = reader.ReadUInt16(); + + long cardsStartOffset = 4 + (numArchetypes * 4); + long cardsEndOffset = cardsStartOffset + (numCards * 2); + Debug.Assert(reader.BaseStream.Length == cardsEndOffset); + + for (int i = 0; i < numArchetypes; i++) + { + int offset = reader.ReadInt16();// The offset of the cards for this named group (starts at 0) + int count = reader.ReadInt16();// The number of cards for this named group + HashSet cardIds = new HashSet(); + cardNameTypes.Add((CardNameType)i, cardIds); + + long tempOffset = reader.BaseStream.Position; + reader.BaseStream.Position = cardsStartOffset + (offset * 2); + for (int j = 0; j < count; j++) + { + short cardId = reader.ReadInt16(); + Cards[cardId].NameTypes.Add((CardNameType)i); + cardIds.Add(cardId); + } + + reader.BaseStream.Position = tempOffset; + } + } + } + + private void LoadCardGenre(List cards) + { + using (BinaryReader reader = new BinaryReader(new MemoryStream(Manager.Archive.Root.FindFile("bin/CARD_Genre.bin").LoadBuffer()))) + { + for (int i = 0; i < cards.Count; i++) + { + CardInfo card = cards[i]; + card.Genre = (CardGenre)reader.ReadUInt64(); + } + } + } + + private void LoadCardProps(List cards, Dictionary cardsById, Dictionary cardImagesById) + { + using (BinaryReader reader = new BinaryReader(new MemoryStream(Manager.Archive.Root.FindFile("bin/CARD_Prop.bin").LoadBuffer()))) + { + for (int i = 0; i < cards.Count; i++) + { + CardInfo card = cards[i]; + LoadCardProp(card, cardsById, reader.ReadUInt32(), reader.ReadUInt32()); + if (card.CardId > 0) + { + card.ImageFile = cardImagesById[card.CardId]; + } + } + } + } + + private void LoadCardProp(CardInfo card, Dictionary cardsById, uint a1, uint a2) + { + BitVector32 bit1 = new BitVector32((int)a1); + BitVector32.Section bit1_mrk = BitVector32.CreateSection(16383);// offset 0, mask 16383 (0x3FFF) + BitVector32.Section bit1_attack = BitVector32.CreateSection(511, bit1_mrk);// offset 14, mask 511 (0x1FF) + BitVector32.Section bit1_defence = BitVector32.CreateSection(511, bit1_attack);// offset 23, mask 511 (0x1FF) + // All bits used + + BitVector32 bit2 = new BitVector32((int)a2); + BitVector32.Section bit2_exist = BitVector32.CreateSection(1);// offset 0, mask 1 (0x1) + BitVector32.Section bit2_kind = BitVector32.CreateSection(63, bit2_exist);// offset 1, mask 63 (0x3F) + BitVector32.Section bit2_attr = BitVector32.CreateSection(15, bit2_kind);// offset 7, mask 15 (0xF) + BitVector32.Section bit2_level = BitVector32.CreateSection(15, bit2_attr);// offset 11, mask 15 (0xF) + BitVector32.Section bit2_icon = BitVector32.CreateSection(7, bit2_level);// offset 15, mask 7 (0x7) + BitVector32.Section bit2_type = BitVector32.CreateSection(31, bit2_icon);// offset 18, mask 31 (0x1F) + BitVector32.Section bit2_scaleL = BitVector32.CreateSection(15, bit2_type);// offset 23, mask 15 (0xF) + BitVector32.Section bit2_scaleR = BitVector32.CreateSection(15, bit2_scaleL);// offset 27, mask 15 (0xF) + + BitVector32.Section bit2_unused = BitVector32.CreateSection(1, bit2_scaleR);// offset 31, mask 1 + Debug.Assert(bit2[bit2_unused] == 0); + + card.CardId = (short)bit1[bit1_mrk]; + card.Atk = (int)(bit1[bit1_attack] * 10); + card.Def = (int)(bit1[bit1_defence] * 10); + card.Level = (byte)bit2[bit2_level]; + card.Attribute = (CardAttribute)bit2[bit2_attr]; + card.CardType = (CardType)bit2[bit2_kind]; + card.SpellType = (SpellType)bit2[bit2_icon]; + card.MonsterType = (MonsterType)bit2[bit2_type]; + card.PendulumScale1 = (byte)bit2[bit2_scaleL]; + card.PendulumScale2 = (byte)bit2[bit2_scaleR]; + + cardsById.Add(card.CardId, card); + + // This is a hard coded value in native code. Might as well do the same check here. + Debug.Assert(card.CardId < Constants.GetMaxCardId(Manager.Version) + 1); + + if (!Enum.IsDefined(typeof(MonsterType), card.MonsterType) || + !Enum.IsDefined(typeof(SpellType), card.SpellType) || + !Enum.IsDefined(typeof(CardType), card.CardType) || + !Enum.IsDefined(typeof(CardAttribute), card.Attribute)) + { + //Debug.Assert(false);// TODO: Update for LE + } + } + + private void LoadCardNamesAndDescriptions(Language language, List cards, + Dictionary indxByLanguage, + Dictionary namesByLanguage, + Dictionary descriptionsByLanguage) + { + if (language == Language.Unknown) + { + return; + } + + byte[] indx = indxByLanguage[language]; + byte[] names = namesByLanguage[language]; + byte[] descriptions = descriptionsByLanguage[language]; + + using (BinaryReader indxReader = new BinaryReader(new MemoryStream(indx))) + using (BinaryReader namesReader = new BinaryReader(new MemoryStream(names))) + using (BinaryReader descriptionsReader = new BinaryReader(new MemoryStream(descriptions))) + { + Dictionary namesByOffset = ReadStrings(namesReader); + Dictionary descriptionsByOffset = ReadStrings(descriptionsReader); + + int index = 0; + while (true) + { + uint nameOffset = indxReader.ReadUInt32(); + uint descriptionOffset = indxReader.ReadUInt32(); + + if (indxReader.BaseStream.Position >= indxReader.BaseStream.Length) + { + // The last index points to an invalid offset + break; + } + + CardInfo card = null; + if (cards.Count > index) + { + card = cards[index]; + } + else + { + cards.Add(card = new CardInfo(index)); + } + + card.Name.SetText(language, namesByOffset[nameOffset]); + card.Description.SetText(language, descriptionsByOffset[descriptionOffset]); + + index++; + } + } + } + + private Dictionary ReadStrings(BinaryReader reader) + { + Dictionary result = new Dictionary(); + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + uint offset = (uint)reader.BaseStream.Position; + string name = reader.ReadNullTerminatedString(Encoding.Unicode); + result.Add(offset, name); + } + return result; + } + + private void LoadRelatedCards(List cards, Dictionary cardsByCardId, List tags, + Dictionary taginfos) + { + foreach (Language language in Enum.GetValues(typeof(Language))) + { + if (language == Language.Unknown) + { + continue; + } + + using (BinaryReader reader = new BinaryReader(new MemoryStream(taginfos[language]))) + { + int count = reader.ReadInt32(); + for (int i = 0; i < count; i++) + { + CardTagInfo tagInfo = null; + if (i >= tags.Count) + { + tagInfo = new CardTagInfo(); + tags.Add(tagInfo); + } + else + { + tagInfo = tags[i]; + } + + tagInfo.Index = i; + tagInfo.MainType = (CardTagInfo.Type)reader.ReadInt16(); + tagInfo.MainValue = reader.ReadInt16(); + for (int j = 0; j < tagInfo.Elements.Length; j++) + { + tagInfo.Elements[j].Type = (CardTagInfo.ElementType)reader.ReadInt16(); + tagInfo.Elements[j].Value = reader.ReadInt16(); + } + long stringOffset1 = reader.ReadInt64(); + long stringOffset2 = reader.ReadInt64(); + + long tempOffset = reader.BaseStream.Position; + + reader.BaseStream.Position = stringOffset1; + tagInfo.Text.SetText(language, reader.ReadNullTerminatedString(Encoding.Unicode)); + + reader.BaseStream.Position = stringOffset2; + tagInfo.DisplayText.SetText(language, reader.ReadNullTerminatedString(Encoding.Unicode)); + + reader.BaseStream.Position = tempOffset; + } + } + } + + using (BinaryReader reader = new BinaryReader(new MemoryStream(Manager.Archive.Root.FindFile("bin/tagdata.bin").LoadBuffer()))) + { + long dataStart = reader.BaseStream.Position + (cards.Count * 8); + + for (int i = 0; i < cards.Count; i++) + { + uint shortoffset = reader.ReadUInt32(); + uint tagCount = reader.ReadUInt32(); + + long tempOffset = reader.BaseStream.Position; + + long start = dataStart + (shortoffset * 4); + reader.BaseStream.Position = start; + if (tagCount > 0 && i >= 0) + { + CardInfo card = cards[i]; + card.RelatedCards.Clear(); + for (int j = 0; j < tagCount; j++) + { + card.RelatedCards.Add(new RelatedCardInfo(cardsByCardId[reader.ReadInt16()], Tags[reader.ReadInt16()])); + } + } + + reader.BaseStream.Position = tempOffset; + } + } + + CardTagInfo.Type[] knownMainTagTypes = (CardTagInfo.Type[])Enum.GetValues(typeof(CardTagInfo.Type)); + CardTagInfo.ElementType[] knownElementTagTypes = (CardTagInfo.ElementType[])Enum.GetValues(typeof(CardTagInfo.ElementType)); + foreach (CardTagInfo tag in tags) + { + Debug.Assert(knownMainTagTypes.Contains(tag.MainType)); + Debug.Assert(tag.MainValue <= 1); + + foreach (CardTagInfo.Element element in tag.Elements) + { + if (element.Type == CardTagInfo.ElementType.None) + { + continue; + } + + //Debug.Assert(knownElementTagTypes.Contains(element.Type));// TODO: Update for LE + } + + if (tag.MainType == CardTagInfo.Type.Exact) + { + // Need english here + Language language = Language.English; + string displayText = tag.DisplayText.GetText(language); + string text = tag.Text.GetText(Language.English); + int splitIndex = displayText == null ? -1 : displayText.IndexOf(':'); + if (splitIndex >= 0) + { + string typeStr = displayText.Substring(0, splitIndex).Trim(); + string value = displayText.Substring(splitIndex + 1).Trim(); + switch (typeStr.ToLower()) + { + case "related to": + tag.Exact = CardTagInfo.ExactType.RelatedTo; + tag.ExactCard = FindCardByName(language, value); + break; + case "card effect": + tag.Exact = CardTagInfo.ExactType.CardEffect; + break; + case "ritual monster": + tag.Exact = CardTagInfo.ExactType.RitualMonster; + tag.ExactCard = FindCardByName(language, value); + break; + case "fusion monster": + tag.Exact = CardTagInfo.ExactType.FusionMonster; + tag.ExactCard = FindCardByName(language, value); + break; + case "spell and trap": + tag.Exact = CardTagInfo.ExactType.SpellTrap; + break; + case "works well with": + tag.Exact = CardTagInfo.ExactType.WorksWellWith; + tag.ExactCard = FindCardByName(language, value); + break; + default: + Debug.Assert(false); + break; + } + } + else + { + switch (text.ToLower()) + { + case "banishbeast": + tag.Exact = CardTagInfo.ExactType.BanishBeast; + break; + case "banishdark": + tag.Exact = CardTagInfo.ExactType.BanishDark; + break; + case "banishfish": + tag.Exact = CardTagInfo.ExactType.BanishFish; + break; + case "banishrock": + tag.Exact = CardTagInfo.ExactType.BanishRock; + break; + case "countertrapfairy": + tag.Exact = CardTagInfo.ExactType.CounterTrapFairy; + break; + case "spellcounter": + tag.Exact = CardTagInfo.ExactType.SpellCounter; + break; + default: + Debug.Assert(false); + break; + } + } + + switch (tag.Exact) + { + case CardTagInfo.ExactType.CardEffect: + CardTagInfo.CardEffectType cardEffect; + if (!Enum.TryParse(text, true, out cardEffect)) + { + //Debug.Assert(false);// TODO: Update for LE + } + tag.CardEffect = cardEffect; + break; + + case CardTagInfo.ExactType.SpellTrap: + CardTagInfo.CardEffectType spellEffect; + if (!Enum.TryParse("Spell_" + text, true, out spellEffect)) + { + //Debug.Assert(false);// TODO: Update for LE + } + tag.CardEffect = spellEffect; + break; + } + } + } + + foreach (CardInfo card in cards) + { + foreach (RelatedCardInfo relatedCardInfo in card.RelatedCards) + { + if (relatedCardInfo.TagInfo.CardEffect != CardTagInfo.CardEffectType.None) + { + card.CardEffectTags.Add(relatedCardInfo.TagInfo.CardEffect); + } + } + } + } + + private void ProcessLimitedCardList(Dictionary cardsById) + { + foreach (CardInfo card in cardsById.Values) + { + card.Limit = CardLimitation.NotLimited; + } + + foreach (short cardId in Manager.CardLimits.Forbidden) + { + cardsById[cardId].Limit = CardLimitation.Forbidden; + } + + foreach (short cardId in Manager.CardLimits.Limited) + { + cardsById[cardId].Limit = CardLimitation.Limited; + } + + foreach (short cardId in Manager.CardLimits.SemiLimited) + { + cardsById[cardId].Limit = CardLimitation.SemiLimited; + } + } + + private void PrintLimitedCardList() + { + Debug.WriteLine("========================== Forbidden =========================="); + foreach (short cardId in Manager.CardLimits.Forbidden) + { + Debug.WriteLine(Cards[cardId].Name); + } + + Debug.WriteLine("========================== Limited =========================="); + foreach (short cardId in Manager.CardLimits.Limited) + { + Debug.WriteLine(Cards[cardId].Name); + } + + Debug.WriteLine("========================== Semi-limited =========================="); + foreach (short cardId in Manager.CardLimits.SemiLimited) + { + Debug.WriteLine(Cards[cardId].Name); + } + } + } + + public class CardInfo + { + public int CardIndex { get; set; } + public short CardId { get; set; } + public ZibFile ImageFile { get; set; } + public LocalizedText Name { get; set; } + public LocalizedText Description { get; set; } + public List RelatedCards { get; private set; } + public HashSet CardEffectTags { get; private set; } + + /// + /// Name type / archetype e.g. "Harpie" + /// + public HashSet NameTypes { get; set; } + + /// + /// The set ids this card belongs to (this is loaded from external sources) + /// A set is a pack, deck or other form of card collection in the official game + /// + public List SetIds { get; private set; } + + public int Atk { get; set; } + public int Def { get; set; } + public byte Level { get; set; } + + public bool IsUnknownAtk + { + get { return Atk == 5110; } + } + + public bool IsUnknownDef + { + get { return Def == 5110; } + } + + /// + /// Light, Dark, Water, Fire (also Spell / Trap) + /// + public CardAttribute Attribute { get; set; } + + /// + /// Fusion, Effect, Tuner, Flip, Ritual (also Spell / Trap) + /// + public CardType CardType { get; set; } + + /// + /// Field, Equip, QuickPlay, Continuous + /// + public SpellType SpellType { get; set; } + + /// + /// Insect, Fiend, Beast, Aqua, Plant (also Spell / Trap) + /// + public MonsterType MonsterType { get; set; } + + public byte PendulumScale1 { get; set; } + public byte PendulumScale2 { get; set; } + + public byte PendulumScale + { + get { return Math.Max(PendulumScale1, PendulumScale2); } + } + + /// + /// Card limitation (NotLimited, Forbidden, Limited, SemiLimited) + /// + public CardLimitation Limit { get; set; } + + /// + /// Card genre (negate effect, direct attack, cannot be destroyed, etc) + /// + public CardGenre Genre { get; set; } + + public CardTypeFlags CardTypeFlags + { + get { return GetCardTypeFlags(CardType); } + } + + public bool IsMonsterToken + { + get { return IsMonster && CardTypeFlags.HasFlag(CardTypeFlags.Token); } + } + + public bool IsEffect + { + get { return CardTypeFlags.HasFlag(CardTypeFlags.Effect); } + } + + public bool IsMonster + { + get { return Attribute != CardAttribute.Spell && Attribute != CardAttribute.Trap; } + } + + public bool IsNormalMonster + { + get { return FrameType == CardFrameType.Normal || FrameType == CardFrameType.PendulumNormal; } + } + + public bool IsPendulum + { + get { return CardTypeFlags.HasFlag(CardTypeFlags.Pendulum); } + } + + public bool IsXyz + { + get { return CardTypeFlags.HasFlag(CardTypeFlags.Xyz); } + } + + public bool IsSynchro + { + get { return CardTypeFlags.HasFlag(CardTypeFlags.Synchro); } + } + + public bool IsFusion + { + get { return CardTypeFlags.HasFlag(CardTypeFlags.Fusion); } + } + + public bool IsMainDeckCard + { + get { return !IsExtraDeckCard; } + } + + public bool IsExtraDeckCard + { + get + { + return CardTypeFlags.HasFlag(CardTypeFlags.Xyz) || CardTypeFlags.HasFlag(CardTypeFlags.Fusion) || + CardTypeFlags.HasFlag(CardTypeFlags.Synchro) || CardTypeFlags.HasFlag(CardTypeFlags.Link); + } + } + + public bool IsSpell + { + get { return Attribute == CardAttribute.Spell; } + } + + public bool IsTrap + { + get { return Attribute == CardAttribute.Trap; } + } + + public string FrameName + { + get { return GetFrameName(FrameType); } + } + + public CardFrameType FrameType + { + get + { + if (IsSpell) + { + return CardFrameType.Spell; + } + + if (IsTrap) + { + return CardFrameType.Trap; + } + + CardTypeFlags cardFlags = CardTypeFlags; + + if (cardFlags.HasFlag(CardTypeFlags.Synchro)) + { + if (cardFlags.HasFlag(CardTypeFlags.Pendulum)) + { + return CardFrameType.PendulumSynchro; + } + return CardFrameType.Synchro; + } + + if (cardFlags.HasFlag(CardTypeFlags.Xyz)) + { + if (cardFlags.HasFlag(CardTypeFlags.Pendulum)) + { + return CardFrameType.PendulumXyz; + } + return CardFrameType.Xyz; + } + + if (cardFlags.HasFlag(CardTypeFlags.Pendulum)) + { + if (cardFlags.HasFlag(CardTypeFlags.Effect)) + { + return CardFrameType.PendulumEffect; + } + return CardFrameType.PendulumNormal; + } + + if (cardFlags.HasFlag(CardTypeFlags.Token)) + { + return CardFrameType.Token; + } + + if (cardFlags.HasFlag(CardTypeFlags.Fusion)) + { + return CardFrameType.Fusion; + } + + if (cardFlags.HasFlag(CardTypeFlags.Ritual)) + { + return CardFrameType.Ritual; + } + + if (cardFlags.HasFlag(CardTypeFlags.Link)) + { + return CardFrameType.Link; + } + + if (cardFlags.HasFlag(CardTypeFlags.Effect) || + cardFlags.HasFlag(CardTypeFlags.SpecialSummon) || + cardFlags.HasFlag(CardTypeFlags.Union) || + cardFlags.HasFlag(CardTypeFlags.Toon) || + cardFlags.HasFlag(CardTypeFlags.Gemini)) + { + return CardFrameType.Effect; + } + + return CardFrameType.Normal; + } + } + + public CardInfo(int index) + { + CardIndex = index; + Name = new LocalizedText(); + Description = new LocalizedText(); + RelatedCards = new List(); + CardEffectTags = new HashSet(); + NameTypes = new HashSet(); + SetIds = new List(); + } + + public string GetDescription(Language language, bool pendulumDescription) + { + if (pendulumDescription && !IsPendulum) + { + return string.Empty; + } + + string text = Description.GetText(language); + if (IsPendulum) + { + string pendulumHeader = "[Pendulum Effect]"; + int index = text.IndexOf(pendulumHeader); + if (pendulumDescription) + { + return index == -1 ? string.Empty : text.Substring(index + pendulumHeader.Length); + } + else + { + return index == -1 ? text : text.Substring(0, index); + } + } + return text; + } + + public static CardTypeFlags GetCardTypeFlags(CardType cardType) + { + switch (cardType) + { + case CardType.Default: return CardTypeFlags.Default; + case CardType.Effect: return CardTypeFlags.Effect; + case CardType.Fusion: return CardTypeFlags.Fusion; + case CardType.FusionEffect: return CardTypeFlags.Fusion | CardTypeFlags.Effect; + case CardType.Ritual: return CardTypeFlags.Ritual; + case CardType.RitualEffect: return CardTypeFlags.Ritual | CardTypeFlags.Effect; + case CardType.ToonEffect: return CardTypeFlags.Toon | CardTypeFlags.Effect; + case CardType.SpiritEffect: return CardTypeFlags.Spirit | CardTypeFlags.Effect; + case CardType.UnionEffect: return CardTypeFlags.Union | CardTypeFlags.Effect; + case CardType.GeminiEffect: return CardTypeFlags.Gemini | CardTypeFlags.Effect; + case CardType.Token: return CardTypeFlags.Token; + case CardType.Spell: return CardTypeFlags.Spell; + case CardType.Trap: return CardTypeFlags.Trap; + case CardType.Tuner: return CardTypeFlags.Tuner; + case CardType.TunerEffect: return CardTypeFlags.Tuner | CardTypeFlags.Effect; + case CardType.Synchro: return CardTypeFlags.Synchro; + case CardType.SynchroEffect: return CardTypeFlags.Synchro | CardTypeFlags.Effect; + case CardType.SynchroTunerEffect: return CardTypeFlags.Synchro | CardTypeFlags.Tuner | CardTypeFlags.Effect; + case CardType.DarkTunerEffect: return CardTypeFlags.DarkTuner | CardTypeFlags.Effect; + case CardType.DarkSynchroEffect: return CardTypeFlags.DarkSynchro | CardTypeFlags.Effect; + case CardType.Xyz: return CardTypeFlags.Xyz; + case CardType.XyzEffect: return CardTypeFlags.Xyz | CardTypeFlags.Effect; + case CardType.FlipEffect: return CardTypeFlags.Flip | CardTypeFlags.Effect; + case CardType.Pendulum: return CardTypeFlags.Pendulum; + case CardType.PendulumEffect: return CardTypeFlags.Pendulum | CardTypeFlags.Effect; + case CardType.EffectSp: return CardTypeFlags.Effect | CardTypeFlags.SpecialSummon; + case CardType.ToonEffectSp: return CardTypeFlags.Toon | CardTypeFlags.Effect | CardTypeFlags.SpecialSummon; + case CardType.SpiritEffectSp: return CardTypeFlags.Spirit | CardTypeFlags.Effect | CardTypeFlags.SpecialSummon; + case CardType.TunerEffectSp: return CardTypeFlags.Tuner | CardTypeFlags.Effect | CardTypeFlags.SpecialSummon; + case CardType.DarkTunerEffectSp: return CardTypeFlags.DarkTuner | CardTypeFlags.Effect | CardTypeFlags.SpecialSummon; + case CardType.FlipTunerEffect: return CardTypeFlags.Flip | CardTypeFlags.Tuner | CardTypeFlags.Effect; + case CardType.PendulumTunerEffect: return CardTypeFlags.Pendulum | CardTypeFlags.Tuner | CardTypeFlags.Effect; + case CardType.XyzPendulumEffect: return CardTypeFlags.Xyz | CardTypeFlags.Pendulum | CardTypeFlags.Effect; + case CardType.PendulumFlipEffect: return CardTypeFlags.Pendulum | CardTypeFlags.Flip | CardTypeFlags.Effect; + //case CardType.SynchoPendulumEffect: return CardTypeFlags.Synchro | CardTypeFlags.Pendulum | CardTypeFlags.Effect; + //case CardType.UnionTunerEffect: return CardTypeFlags.Union | CardTypeFlags.Tuner | CardTypeFlags.Effect; + //case CardType.RitualSpiritEffect: return CardTypeFlags.Ritual | CardTypeFlags.Spirit | CardTypeFlags.Effect; + case CardType.Link: return CardTypeFlags.Link; + /*case CardType.AnyNormal: return CardTypeFlags.Any | CardTypeFlags.Normal; + case CardType.AnyFusion: return CardTypeFlags.Any | CardTypeFlags.Fusion; + case CardType.AnyFlip: return CardTypeFlags.Any | CardTypeFlags.Flip; + case CardType.AnyPendulum: return CardTypeFlags.Any | CardTypeFlags.Pendulum; + case CardType.AnyRitual: return CardTypeFlags.Any | CardTypeFlags.Ritual; + case CardType.AnySynchro: return CardTypeFlags.Any | CardTypeFlags.Synchro; + case CardType.AnyTuner: return CardTypeFlags.Any | CardTypeFlags.Tuner; + case CardType.AnyXyz: return CardTypeFlags.Any | CardTypeFlags.Xyz;*/ + default: + return 0;// TODO: Update for LE //throw new NotImplementedException("Unhandled CardType->CardTypeFlags conversion " + cardType); + } + } + + public static string GetFrameName(CardFrameType frameType) + { + switch (frameType) + { + default: + case CardFrameType.Normal: return "card_nomal"; + case CardFrameType.Effect: return "card_kouka"; + case CardFrameType.Token: return "card_token"; + case CardFrameType.Ritual: return "card_gisiki"; + case CardFrameType.Fusion: return "card_yugo"; + case CardFrameType.PendulumEffect: return "card_pendulum"; + case CardFrameType.PendulumNormal: return "card_pendulum_n"; + case CardFrameType.PendulumSynchro: return "card_sync_pendulum"; + case CardFrameType.PendulumXyz: return "card_xyz_pendulum"; + case CardFrameType.Synchro: return "card_sync"; + case CardFrameType.Xyz: return "card_xyz"; + case CardFrameType.Spell: return "card_mahou"; + case CardFrameType.Trap: return "card_wana"; + } + } + + public static string GetFullMonsterTypeName(MonsterType monsterType, CardTypeFlags cardType) + { + string result = null; + foreach (CardTypeFlags flag in Enum.GetValues(typeof(CardTypeFlags))) + { + if (cardType.HasFlag(flag)) + { + string flagName = GetCardTypeFlagName(flag); + if (!string.IsNullOrEmpty(flagName)) + { + // Reverse order + result = result == null ? flagName : flagName + "/" + result; + } + } + } + + return "[" + GetMonsterTypeName(monsterType) + (result == null ? string.Empty : "/" + result) + "]"; + } + + public static string GetMonsterTypeName(MonsterType monsterType) + { + switch (monsterType) + { + case MonsterType.Dragon: return "Dragon"; + case MonsterType.Zombie: return "Zombie"; + case MonsterType.Fiend: return "Fiend"; + case MonsterType.Pyro: return "Pyro"; + case MonsterType.SeaSerpent: return "Sea Serpent"; + case MonsterType.Rock: return "Rock"; + case MonsterType.Machine: return "Machine"; + case MonsterType.Fish: return "Fish"; + case MonsterType.Dinosaur: return "Dinosaur"; + case MonsterType.Insect: return "Insect"; + case MonsterType.Beast: return "Beast"; + case MonsterType.BeastWarrior: return "Beast-Warrior"; + case MonsterType.Plant: return "Plant"; + case MonsterType.Aqua: return "Aqua"; + case MonsterType.Warrior: return "Warrior"; + case MonsterType.WingedBeast: return "Winged Beast"; + case MonsterType.Fairy: return "Fairy"; + case MonsterType.Spellcaster: return "Spellcaster"; + case MonsterType.Thunder: return "Thunder"; + case MonsterType.Reptile: return "Reptile"; + case MonsterType.Psychic: return "Psychic"; + case MonsterType.Wyrm: return "Wyrm"; + case MonsterType.DivineBeast: return "Divine-Beast"; + case MonsterType.CreatorGod: return "Creator"; + case MonsterType.Spell: return "Spell"; + case MonsterType.Trap: return "Trap"; + case MonsterType.Unknown: + default: + return "?"; + } + } + + public static string GetCardTypeFlagName(CardTypeFlags flag) + { + switch (flag) + { + default: + case CardTypeFlags.Default: return null; + case CardTypeFlags.Effect: return "Effect"; + case CardTypeFlags.Fusion: return "Fusion"; + case CardTypeFlags.Ritual: return "Ritual"; + case CardTypeFlags.Toon: return "Toon"; + case CardTypeFlags.Spirit: return "Spirit"; + case CardTypeFlags.Union: return "Union"; + case CardTypeFlags.Gemini: return "Gemini"; + case CardTypeFlags.Token: return "Token"; + case CardTypeFlags.Spell: return "Spell"; + case CardTypeFlags.Trap: return "Trap"; + //case CardTypeFlags.Common: return ""; + case CardTypeFlags.Tuner: return "Tuner"; + case CardTypeFlags.DarkTuner: return "Dark Tuner"; + case CardTypeFlags.DarkSynchro: return "Dark Synchro"; + case CardTypeFlags.Synchro: return "Synchro"; + case CardTypeFlags.Xyz: return "Xyz"; + case CardTypeFlags.Flip: return "Flip"; + case CardTypeFlags.Pendulum: return "Pendulum"; + //case CardTypeFlags.SpecialSummon: return ""; + case CardTypeFlags.Link: return "Link"; + } + } + } + + public enum CardAttribute + { + Unknown = 0, + LightMonster = 1, + DarkMonster = 2, + WaterMonster = 3, + FireMonster = 4, + EarthMonster = 5, + WindMonster = 6, + DivineMonster = 7, + Spell = 8, + Trap = 9 + } + + public enum CardType + { + Default = 0, + Effect = 1, + Fusion = 2, + FusionEffect = 3,// Thousand-Eyes Restrict + Ritual = 4, + RitualEffect = 5,// Relinquished + ToonEffect = 6,// Toon Masked Scorcerer + SpiritEffect = 7,// Maharaghi + UnionEffect = 8,// Y-Dragon Head + GeminiEffect = 9,// Elemental HERO Neos Alius + Token = 10, + //11 = Effect? - duel links states this is "God" + //12 = Effect? - duel links states this is "Dummy" + Spell = 13, + Trap = 14, + Tuner = 15,// Flamvell Guard + TunerEffect = 16,// Cryomancer of the Ice Barrier + Synchro = 17, // Gaia Knight, the Force of Earth + SynchroEffect = 18,// Dark End Dragon + SynchroTunerEffect = 19,// Formula Synchron + DarkTunerEffect = 20,// unused + DarkSynchroEffect = 21,// unused + Xyz = 22,// Gem-Knight Pearl + XyzEffect = 23,// Number 39: Utopia + FlipEffect = 24, + Pendulum = 25,// Flash Knight + PendulumEffect = 26,// Stargazer Magician + EffectSp = 27,// Larvae Moth + ToonEffectSp = 28,// Manga Ryu-Ran (Sp = special summon "This monster cannot be Normal Summoned or Set") + SpiritEffectSp = 29,// Yamato-no-Kami + TunerEffectSp = 30,// Trap Eater + DarkTunerEffectSp = 31,// unused + FlipTunerEffect = 32,// Shaddoll Falco + PendulumTunerEffect = 33,// "Luster Pendulum, the Dracoslayer" + XyzPendulumEffect = 34,// Odd-Eyes Rebellion Dragon + PendulumFlipEffect = 35,// Performapal Momoncarpet + //SynchoPendulumEffect = 36,// unused + //UnionTunerEffect = 37,// unused + //RitualSpiritEffect = 38,// unused + //_______ = 39,// unused - underscores?? + + Link = 43 + + /*// These values are used for tagdata/taginfo + AnyNormal = 37,// NORMAL* + AnySynchro = 38,// SYNC* + AnyXyz = 39,// XYZ* + AnyTuner = 40,// TUNER* + AnyFusion = 41,// FUSION* + AnyRitual = 42,// RITUAL* + AnyPendulum = 43,// PEND* + AnyFlip = 44,// FLIP**/ + } + + /// + /// Flags version of CardType for easier checking of individual types + /// + [Flags] + public enum CardTypeFlags : uint + { + Default = 0, + Effect = 1 << 0, + Fusion = 1 << 1, + Ritual = 1 << 2, + Toon = 1 << 3, + Spirit = 1 << 4, + Union = 1 << 5, + Gemini = 1 << 6, + Token = 1 << 7, + Spell = 1 << 8, + Trap = 1 << 9, + Tuner = 1 << 10, + DarkTuner = 1 << 11, + DarkSynchro = 1 << 12, + Synchro = 1 << 13, + Xyz = 1 << 14, + Flip = 1 << 15, + Pendulum = 1 << 16, + SpecialSummon = 1 << 17,// "This monster cannot be Normal Summoned or Set" + Link = 1 << 18, + + Normal = 1 << 19,// Special flag used for finding related cards + Any = 1 << 20,// Special flag used for finding related cards + } + + public enum MonsterType + { + Unknown = 0, + Dragon = 1, + Zombie = 2, + Fiend = 3, + Pyro = 4, + SeaSerpent = 5, + Rock = 6, + Machine = 7, + Fish = 8, + Dinosaur = 9, + Insect = 10, + Beast = 11, + BeastWarrior = 12, + Plant = 13, + Aqua = 14, + Warrior = 15, + WingedBeast = 16, + Fairy = 17, + Spellcaster = 18, + Thunder = 19, + Reptile = 20, + Psychic = 21, + Wyrm = 22, + DivineBeast = 23, + CreatorGod = 24,// This does't appear on any card in the game - its meant for "Holacite the Creator of Light" + Spell = 25, + Trap = 26 + } + + /// + /// Also known as "Property" - the type of spell / trap card + /// + public enum SpellType + { + Normal = 0, + + /// + /// Counter trap cards are a unique trap card type that are of spell speed 3. + /// + Counter = 1, + + Field = 2, + Equip = 3, + Continuous = 4, + QuickPlay = 5, + Ritual = 6 + } + + public enum CardFrameType + { + Normal, + Effect, + Token, + Ritual, + Fusion, + PendulumEffect, + PendulumNormal, + PendulumSynchro, + PendulumXyz, + Synchro, + Xyz, + Spell, + Trap, + Link + } + + public enum CardLimitation + { + NotLimited, + Forbidden, + Limited, + SemiLimited + } + + [Flags] + public enum CardGenre : ulong + { + None = 0, + RecoverLP = 1UL << 0,//0x0000000000000001 ICON_ID_GENRE_LPUP + DamageLP = 1UL << 1,//0x0000000000000002 ICON_ID_GENRE_LPDOWN + HelpDraw = 1UL << 2,//0x0000000000000004 ICON_ID_GENRE_DRAW + SpecialSummon = 1UL << 3,//0x0000000000000008 ICON_ID_GENRE_SPSUMMON + NegateEffect = 1UL << 4,//0x0000000000000010 ICON_ID_GENRE_DISABLE + SearchDeck = 1UL << 5,//0x0000000000000020 ICON_ID_GENRE_DECKSEARCH + RecoverFromGraveyard = 1UL << 6,//0x0000000000000040 ICON_ID_GENRE_USEGRAVE + IncreaseDecreaseAtkDef = 1UL << 7,//0x0000000000000080 ICON_ID_GENRE_POWER + ChangeBattlePosition = 1UL << 8,//0x0000000000000100 ICON_ID_GENRE_POSITION + SetControls = 1UL << 9,//0x0000000000000200 ICON_ID_GENRE_CONTROL + DestroyMonster = 1UL << 10,//0x0000000000000400 ICON_ID_GENRE_BREAKMONST + DestroySpellCard = 1UL << 11,//0x0000000000000800 ICON_ID_GENRE_BREAKMAGIC + DestroyHand = 1UL << 12,//0x0000000000001000 ICON_ID_GENRE_HANDDES + DestroyDeck = 1UL << 13,//0x0000000000002000 ICON_ID_GENRE_DECKDES + RemoveCard = 1UL << 14,//0x0000000000004000 ICON_ID_GENRE_REMOVECARD + ReturnCard = 1UL << 15,//0x0000000000008000 ICON_ID_GENRE_CARDBACK + Piercing = 1UL << 16,//0x0000000000010000 ICON_ID_GENRE_SPEAR + DirectAttack = 1UL << 17,//0x0000000000020000 ICON_ID_GENRE_DIRECTATK + AttackMultipleTimes = 1UL << 18,//0x0000000000040000 ICON_ID_GENRE_MANYATK + CannotBeDestroyed = 1UL << 19,//0x0000000000080000 ICON_ID_GENRE_UNBREAK + LimitAttack = 1UL << 20,//0x0000000000100000 ICON_ID_GENRE_LIMITATK + CannotNormalSummon = 1UL << 21,//0x0000000000200000 ICON_ID_GENRE_CANTSUMMON + FlipEffectMonster = 1UL << 22,//0x0000000000400000 ICON_ID_GENRE_REVERSE + ToonMonster = 1UL << 23,//0x0000000000800000 ICON_ID_GENRE_TOON + SpiritMonster = 1UL << 24,//0x0000000001000000 ICON_ID_GENRE_SPIRIT + UnionMonster = 1UL << 25,//0x0000000002000000 ICON_ID_GENRE_UNION + GeminiMonster = 1UL << 26,//0x0000000004000000 ICON_ID_GENRE_DUAL + LvMonster = 1UL << 27,//0x0000000008000000 ICON_ID_GENRE_LEVELUP + Original = 1UL << 28,//0x0000000010000000 ICON_ID_GENRE_ORIGINAL + FusionMaterialMonster = 1UL << 29,//0x0000000020000000 ICON_ID_GENRE_FUSION + Ritual = 1UL << 30,//0x0000000040000000 ICON_ID_GENRE_RITUAL + Token = 1UL << 31,//0x0000000080000000 ICON_ID_GENRE_TOKEN + Counter = 1UL << 32,//0x0000000100000000 ICON_ID_GENRE_COUNTER + Gamble = 1UL << 33,//0x0000000200000000 ICON_ID_GENRE_GAMBLE + AttributeRelated = 1UL << 34,//0x0000000400000000 ICON_ID_GENRE_ATTR + TypeRelated = 1UL << 35,//0x0000000800000000 ICON_ID_GENRE_TYPE + Tuner = 1UL << 36,//0x0000001000000000 ICON_ID_GENRE_TUNER + SynchroMonster = 1UL << 37,//0x0000002000000000 ICON_ID_GENRE_SYNC + SendToGraveyard = 1UL << 38,//0x0000004000000000 ICON_ID_GENRE_DROPGRAVE + + // These values don't visibly appear in the game + NormalMonsterRelated = 1UL << 39,//0x0000008000000000 ICON_ID_GENRE_NORMAL + LightMonsterRelated = 1UL << 40,//0x0000010000000000 ICON_ID_GENRE_ATTR_LIGHT + DarkMonsterRelated = 1UL << 41,//0x0000020000000000 ICON_ID_GENRE_ATTR_DARK + EarthMonsterRelated = 1UL << 42,//0x0000040000000000 ICON_ID_GENRE_ATTR_EARTH + WaterMonsterRelated = 1UL << 43,//0x0000080000000000 ICON_ID_GENRE_ATTR_WATER + FireMonsterRelated = 1UL << 44,//0x0000100000000000 ICON_ID_GENRE_ATTR_FIRE + WindMonsterRelated = 1UL << 45,//0x0000200000000000 ICON_ID_GENRE_ATTR_WIND + + XyzMonster = 1UL << 46,//0x0000400000000000 ICON_ID_GENRE_XYZ + LevelModifier = 1UL << 47,//0x0000800000000000 ICON_ID_GENRE_LVUPDOWN + Pendulum = 1UL << 48,//0x0001000000000000 ICON_ID_GENRE_PENDULUM + + // These values aren't on any cards (but appear in game if you force them on a card) + DivineAttribute = 1UL << 49,//0x0002000000000000 ICON_ID_GENRE_ATTR_GOD + NewCard = 1UL << 50,//0x0004000000000000 ICON_ID_GENRE_NEW + GameOriginal = 1UL << 51,//0x0008000000000000 ICON_ID_GENRE_GAME_ORIGINAL + CardVaritation = 1UL << 52,//0x0010000000000000 ICON_ID_GENRE_PICTURE (assumed) + // + // The game uses broken icons for those values: + //DivineAttribute = ICON_ID_ORDER_ASCENDING + //NewCard = ICON_ID_ORDER_DESCENDING + //GameOriginal = ICON_ID_SEARCH + //CardVaritation = ICON_ID_DUEL_MENU_PHASE + + //Unused8 = 1UL << 53,//0x0020000000000000 + //Unused9 = 1UL << 54,//0x0040000000000000 + //Unused10 = 1UL << 55,//0x0080000000000000 + //Unused11 = 1UL << 56,//0x0100000000000000 + //Unused12 = 1UL << 57,//0x0200000000000000 + //Unused12 = 1UL << 58,//0x0400000000000000 + //Unused13 = 1UL << 59,//0x0800000000000000 + //Unused14 = 1UL << 60,//0x1000000000000000 + //Unused15 = 1UL << 61,//0x2000000000000000 + //Unused16 = 1UL << 62,//0x4000000000000000 + //Unused17 = 1UL << 63,//0x8000000000000000 + } + + public enum CardNameType + { + Null, + Toon, + Demon, + Keeper, + Guardian, + Scorpion, + Amazoness, + Ninja, + Level, + EHERO, + DHERO, + NeosMaterial, + NeosFusion, + Neos, + Ojama, + Battery, + DarkWorld, + BES, + Antique, + Sphinx, + Machiners, + Harpie, + Roid, + Vehicloid, + Neospacian, + Cocoon, + Alien, + Mythical, + HERO, + Allure, + Gadget, + Six, + Jewel, + Volcanic, + BlazeCanon, + Venom, + Cloudian, + Gladial, + Weapon, + Takemitsu, + EvHERO, + Drunk, + Arcana, + Fossil, + Gunner, + Forbidden, + Rainbow, + CyberFusion, + Icebarrier, + AOJ, + Saber, + Worm, + LightLord, + Frog, + Nitro, + Genex, + MistValley, + Flamebell, + NeosNHERO, + Deformer, + Chain, + Natul, + Clear, + RedEyes, + BlackFeather, + SlashBuster, + Roaring, + Jurac, + RealGenex, + EarthbindGod, + Koakimail, + Infernity, + X_Saber, + FortuneLady, + Dragnity, + FortuneWitch, + Synchron, + Saviour, + Reptiles, + Shien, + Junk, + Tomabo, + Sin, + Gem, + GemKnight, + Laval, + Vailon, + Scrap, + Eleki, + Fusion, + Infinity, + Wisel, + TG, + Karakuri, + Ritua, + Gusta, + Invelds, + Reactor, + Agent, + Polestar, + PolestarBeast, + PolestarGhost, + PolestarAngel, + PolestarItem, + PoleGod, + SoundWarrior, + Resonator, + MHERO, + VHERO, + Meklord_Emp, + Meklord_Sld, + Meklord, + Zenmai, + Penguin, + Evold, + Evolder, + TrapHole, + TimeGod, + Sacred, + Velds, + Numbers, + Gagaga, + Gogogo, + Photon, + Ninjutsu, + Inzector, + Invasion, + Bouncer, + Butterfly, + HolySeal, + Majin, + Heroic, + Ooparts, + Spellbook, + Madolce, + Geargear, + Xyz, + Poseidon, + Mermail, + Abyss, + Magical, + Nimble, + Duston, + Medallion, + NobleKnight, + FireKing, + Galaxy, + HolySword, + FireStar, + FireDance, + HazeBeast, + Haze, + ZexalWeapon, + Hope, + GimmickPuppet, + Dododo, + BK, + PhantomMek, + FireKingBeast, + ChaosNumbers, + ChaosXyz, + Geargearno, + SDRobo, + SDRobo2, + Umbral, + HolyLightning, + Bujin, + Kowakuma, + Hole, + CNo39, + H_Challenger, + Malicebolus, + Ghostrick, + Vampire, + Cat, + CyberDragon, + Cybernetic, + Shinra, + Necrovalley, + Zubaba, + Fishborg, + RUM, + Medallion2, + Artifact, + Evolkaiser, + GalaxyEyes, + Tachyon, + Over100, + Wizard, + OddEyes, + LegendDragon, + LegendKnight, + WingedKuriboh, + Stardust, + Sprout, + Artorius, + Lancelot, + Superheavy, + Genso, + Tellarknight, + Shadoll, + DragonStar, + EM, + Change, + Higan, + UA, + DD, + DDD, + Furnimal, + Deathtoy, + Qliphot, + Bunborg, + Goblin, + Cthulhu, + Contract, + Gottoms, + Yosen, + Necroth, + Spirit_All, + Spirit_Tamer, + Spirit_Beast, + RR, + Infernoid, + Jinzo, + Gaia, + Monarch, + Charmer, + Possessed, + Crystal, + Warrior, + PowerTool, + BMG, + EdgeImp, + Sephira, + GensoPrincess, + Spirit_Rider, + Stellarknight, + Void, + Em, + Dragonsword, + Igknight, + Aroma, + Empowered, + AetherWeapon, + FortunePrince, + Aquaactress, + Aquarium, + ChaosSoldier, + Majespecter, + Gradle, + SOz, + Kaiju, + SR, + PSYFrame, + RedDemon, + Burgestoma, + Dante, + BusterBlader, + BusterSword, + Dynamist, + Shiranui, + Dragondevil, + Exodia, + PhantomKnight, + Phantom, + Super, + Super_Quantum, + Super_Machine, + BlueEyes, + HopeX, + Moonlight, + Amorphage, + ElfSwordsman, + MagicianGirl, + BlackMagic, + Metalphose, + Tramid, + ABF, + Houkai, + Chaos, + CyberAngel, + Cypher, + Cardian, + SilentSword, + SilentMagic, + MagnetWarrior, + BlackMagic2, + Kuriboh, + Crystron, + Kagoju, + ApoQliphot, + Chichukai, + ChichukaiRyu, + Spyral, + SpyralGear, + MakaiGekidan, + MakaiDaihon, + FallenAngel, + WW, + Beast12, + PendDragon + } + + public class RelatedCardInfo + { + /// + /// The related card + /// + public CardInfo Card { get; set; } + + /// + /// The relationship tag info + /// + public CardTagInfo TagInfo { get; set; } + + public RelatedCardInfo(CardInfo card, CardTagInfo tagInfo) + { + Card = card; + TagInfo = tagInfo; + } + } + + /// + /// Describes how cards can be tagged / related to one another + /// + public class CardTagInfo + { + /// + /// The index of this tag info in the tags collection + /// + public int Index { get; set; } + + public ExactType Exact { get; set; } + public CardEffectType CardEffect { get; set; } + public CardInfo ExactCard { get; set; } + + /// + /// The main type of this tag - priority seems to be 0,1,2 + /// + public Type MainType { get; set; } + + /// + /// Unknown - some kind of sub priority? + /// + public short MainValue { get; set; } + + /// + /// The elements which make up this tag info + /// + public Element[] Elements { get; set; } + + /// + /// The string representation + /// + public LocalizedText Text { get; private set; } + + /// + /// The string respresentation which is displayed at the bottom of the relationship window + /// + public LocalizedText DisplayText { get; private set; } + + public CardTagInfo() + { + Elements = new Element[8]; + Text = new LocalizedText(); + DisplayText = new LocalizedText(); + } + + /// + /// The main type of the tag info + /// + public enum Type + { + /// + /// An exact relationship of some kind e.g. "Card effect: Negate Attack", "Related to: Exodia the Forbidden One" + /// + Exact = 0, + + /// + /// Some kind of impact on the card + /// + Ad = 1, + + /// + /// Finding other cards e.g. "Summon 1 Level 4 or lower Gemini monster from your hand." - "{FIND}KIND:DUAL LEVEL:<=4" + /// + Find = 2, + + /// + /// Same as Ad but for only XYZ + /// + AdXyz = 257, + + /// + /// Same as Find but only for XYZ + /// + FindXyz = 258 + } + + public enum ElementType + { + None = -1, + AtkLessThanOrEquals = 0, + DefLessThanOrEquals = 2, + LevelLessThanOrEquals = 4, + RankLessThanOrEquals = 8, + AtkLessThan = 256, + Attribute = 513,// This should map into the enum CardAttribute + AtkEquals = 512, + DefEquals = 514, + CardType = 515,// XYZ/Monster/Effect/etc - should map into CardType + LevelEquals = 516, + SpellType = 517,// This should map into enum SpellType + MonsterType = 518,// This should map into enum MonsterType + DeckType = 519, + RankEquals = 520,// For XYZ monsters + SpecialSummon = 521, + Tribute = 522, + Tribute2 = 523,// This monster counts as 2 tribute summons + AtkGreaterThanOrEquals = 768, + LevelGreaterThanOrEquals = 772, + RankGreaterThanOrEquals = 776, + } + + public struct Element + { + public ElementType Type; + public short Value; + } + + /// + /// Defines what an "Exact" tag info type does + /// + public enum ExactType + { + None, + + /// + /// Related to an archetype / exact card + /// + RelatedTo, + + /// + /// Related to a fusion monster + /// + FusionMonster, + + /// + /// Related to a ritual monster + /// + RitualMonster, + + /// + /// A spell / trap card effect + /// + SpellTrap, + + /// + /// Monster card effect + /// + CardEffect, + + WorksWellWith, + + /// + /// Monsters that use Spell Counters + /// + SpellCounter, + + + /// + /// Fairy-Type effects that trigger with Counter Traps + /// + CounterTrapFairy, + + /// + /// Banished Beast and Winged-Beast Type monsters + /// + BanishBeast, + /// + /// Banished Dark monsters + /// + BanishDark, + /// + /// Banished Fish, Sea Serpent, and Aqua-Type monsters + /// + BanishFish, + /// + /// Banished Rock-Type monsters + /// + BanishRock + } + + public enum CardEffectType + { + None, + AntiAttack, + AntiDefense, + AntiDiscard, + AntiDraw, + AntiEffectDamage, + AntiFaceDown, + AntiMonsterEffect, + AntiPendulum, + AntiSpell, + AntiTrap, + ATKReduction, + AttackDirectly, + AttributeDestruction, + AttributeEquipBoost, + BanishOpp, + BanishPlayer, + BoostNormal, + BurnDamageAtk, + BurnDamageCont, + BurnDamageDirect, + BurnDamageMons, + BurnDamageTrib, + CannotAttack, + CannotChangePosition, + CardDiscard, + CardDraw, + ChangeLevel, + ChooseAttackTarget, + CoinToss, + Combo, + ContinuousSpellTrib, + DarkCardDraw, + DEFGain, + DEFReduction, + DestroyType, + Dice, + DragonBoost, + EquipDragon, + EquipFairy, + EquipMachine, + EquipSpellcaster, + EquipWarrior, + FaceUp, + FieldPowerAttr, + FieldPowerType, + FlipFaceDown, + Fusion, + GiveControl, + GraveToHandMonster, + GraveToHandSpellTrap, + IceCount, + LargeATKGainEquip, + LifeGain, + LookAtDeck, + LookAtHand, + Mill, + NegateAttack, + RecycleToDeck, + RestrictMonster, + Ritual, + SameAttributeBoost, + Simochi, + SpellTrapProtect, + SSGraveyard, + SSZombie, + StopFlipNormalSummon, + StopSpecialSummon, + SwitchATKDEF, + SynchroFusion, + SynchroMaterial, + TakeControl, + ToGraveyard, + Token, + TokenOpponent, + TrapMonster, + Ultimaya, + ZoneDeny, + + // Spell/trap values + Spell_DoubleSummon, + Spell_MonsterDestruction, + Spell_MonsterProtect, + Spell_Piercing, + Spell_PreventBattleDamage, + Spell_QuickATKboost, + Spell_SSHandDeck, + Spell_StopFusion, + Spell_StopRitual, + Spell_StopSynchro, + Spell_StopTribute, + Spell_StopXyz + } + } + + public enum DeckType + { + Main = 0, + Extra = 1, + Side = 2 + } +} diff --git a/Lotd.Core/FileFormats/bin/RelatedCardData.cs b/Lotd.Core/FileFormats/bin/RelatedCardData.cs new file mode 100644 index 0000000..d5bc990 --- /dev/null +++ b/Lotd.Core/FileFormats/bin/RelatedCardData.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + /// + /// Holds a list of related cards for each card. This information is displayed in the panel on the right side of the screen + /// in the deck editor. This is tagdata.bin + /// + public class RelatedCardData : FileData + { + /// + /// List of related cards. + /// The first list represents a card index (not the card id). + /// The second list represents the related cards for that index. + /// + public List> Items { get; private set; } + + public RelatedCardData() + { + Items = new List>(); + } + + public override void Load(BinaryReader reader, long length) + { + Clear(); + + // There doesn't seem to be any identifier which says how many items to read. Assuming it reads until known max cards. + int numCards = Constants.GetNumCards2(File.Archive.Version); + + long dataStart = reader.BaseStream.Position + (numCards * 8); + + for (int i = 0; i < numCards; i++) + { + uint shortoffset = reader.ReadUInt32(); + uint tagCount = reader.ReadUInt32(); + + long tempOffset = reader.BaseStream.Position; + + long start = dataStart + (shortoffset * 4); + reader.BaseStream.Position = start; + + List items = new List(); + for (int j = 0; j < tagCount; j++) + { + items.Add(new Item(reader.ReadUInt16(), reader.ReadUInt16())); + } + Items.Add(items); + + reader.BaseStream.Position = tempOffset; + } + } + + public override void Save(BinaryWriter writer) + { + uint shortOffset = 0; + for (int i = 0; i < Items.Count; i++) + { + writer.Write(shortOffset); + writer.Write(Items[i].Count); + shortOffset += (uint)Items[i].Count + 1; + } + + for (int i = 0; i < Items.Count; i++) + { + foreach (Item item in Items[i]) + { + writer.Write(item.CardId); + writer.Write(item.TagIndex); + } + writer.Write(0); + } + } + + public override void Clear() + { + Items.Clear(); + } + + public class Item + { + public ushort CardId { get; set; } + + /// + /// An index into taginfo_X.bin + /// + public ushort TagIndex { get; set; } + + public Item(ushort cardId, ushort tagIndex) + { + CardId = cardId; + TagIndex = tagIndex; + } + } + } +} diff --git a/Lotd.Core/FileFormats/main/ArenaData.cs b/Lotd.Core/FileFormats/main/ArenaData.cs new file mode 100644 index 0000000..06d5bff --- /dev/null +++ b/Lotd.Core/FileFormats/main/ArenaData.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + /// + /// Note that this structure doesn't contain the background file information under /arenas/ so it is likely + /// impossible to add new arenas with new background images. + /// + public class ArenaData : FileData + { + static Encoding keyEncoding = Encoding.ASCII; + static Encoding valueEncoding = Encoding.Unicode; + static Encoding value2Encoding = Encoding.Unicode; + public Dictionary Items { get; private set; } + + public override bool IsLocalized + { + get { return true; } + } + + public ArenaData() + { + Items = new Dictionary(); + } + + public override void Load(BinaryReader reader, long length, Language language) + { + long fileStartPos = reader.BaseStream.Position; + + uint count = (uint)reader.ReadUInt64(); + for (uint i = 0; i < count; i++) + { + int id = reader.ReadInt32(); + long keyOffset = reader.ReadInt64(); + long valueOffset = reader.ReadInt64(); + long value2Offset = reader.ReadInt64(); + + long tempOffset = reader.BaseStream.Position; + + reader.BaseStream.Position = fileStartPos + keyOffset; + string key = reader.ReadNullTerminatedString(keyEncoding); + + reader.BaseStream.Position = fileStartPos + valueOffset; + string value = reader.ReadNullTerminatedString(valueEncoding); + + reader.BaseStream.Position = fileStartPos + value2Offset; + string value2 = reader.ReadNullTerminatedString(valueEncoding); + + reader.BaseStream.Position = tempOffset; + + Item item; + if (!Items.TryGetValue(id, out item)) + { + item = new Item(id); + Items.Add(item.Id, item); + } + item.Key.SetText(language, key); + item.Value.SetText(language, value); + item.Value2.SetText(language, value2); + } + } + + public override void Save(BinaryWriter writer, Language language) + { + int firstChunkItemSize = 28;// Size of each item in the first chunk + long fileStartPos = writer.BaseStream.Position; + + writer.Write((ulong)Items.Count); + + long offsetsOffset = writer.BaseStream.Position; + writer.Write(new byte[Items.Count * firstChunkItemSize]); + + int index = 0; + foreach(Item item in Items.Values) + { + int keyLen = GetStringSize(item.Key.GetText(language), keyEncoding); + int valueLen = GetStringSize(item.Value.GetText(language), valueEncoding); + long tempOffset = writer.BaseStream.Position; + + writer.BaseStream.Position = offsetsOffset + (index * firstChunkItemSize); + writer.Write(item.Id); + writer.WriteOffset(fileStartPos, tempOffset); + writer.WriteOffset(fileStartPos, tempOffset + keyLen); + writer.WriteOffset(fileStartPos, tempOffset + keyLen + valueLen); + writer.BaseStream.Position = tempOffset; + + writer.WriteNullTerminatedString(item.Key.GetText(language), keyEncoding); + writer.WriteNullTerminatedString(item.Value.GetText(language), valueEncoding); + writer.WriteNullTerminatedString(item.Value2.GetText(language), value2Encoding); + + index++; + } + } + + public class Item + { + public int Id { get; set; } + public LocalizedText Key { get; set; } + public LocalizedText Value { get; set; } + public LocalizedText Value2 { get; set; } + + public Item(int id) + { + Id = id; + Key = new LocalizedText(); + Value = new LocalizedText(); + Value2 = new LocalizedText(); + } + + public override string ToString() + { + return "id: " + Id + " key: '" + Key + "' value: '" + Value + "' value2: '" + Value2 + "'"; + } + } + } +} diff --git a/Lotd.Core/FileFormats/main/CharData.cs b/Lotd.Core/FileFormats/main/CharData.cs new file mode 100644 index 0000000..4ad5165 --- /dev/null +++ b/Lotd.Core/FileFormats/main/CharData.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + public class CharData : FileData + { + static Encoding keyEncoding = Encoding.ASCII; + static Encoding valueEncoding = Encoding.Unicode; + static Encoding descriptionEncoding = Encoding.Unicode; + public Dictionary Items { get; private set; } + + public override bool IsLocalized + { + get { return true; } + } + + public CharData() + { + Items = new Dictionary(); + } + + public override void Load(BinaryReader reader, long length, Language language) + { + long fileStartPos = reader.BaseStream.Position; + + uint count = (uint)reader.ReadUInt64(); + for (uint i = 0; i < count; i++) + { + int id = reader.ReadInt32(); + DuelSeries series = (DuelSeries)reader.ReadInt32(); + int challengeDeckId = reader.ReadInt32(); + int unk3 = reader.ReadInt32(); + int dlcId = reader.ReadInt32(); + int unk5 = reader.ReadInt32(); + long type = reader.ReadInt64(); + long keyOffset = reader.ReadInt64(); + long valueOffset = reader.ReadInt64(); + long descriptionOffset = reader.ReadInt64(); + + long tempOffset = reader.BaseStream.Position; + + reader.BaseStream.Position = fileStartPos + keyOffset; + string codeName = reader.ReadNullTerminatedString(keyEncoding); + + reader.BaseStream.Position = fileStartPos + valueOffset; + string name = reader.ReadNullTerminatedString(valueEncoding); + + reader.BaseStream.Position = fileStartPos + descriptionOffset; + string bio = reader.ReadNullTerminatedString(valueEncoding); + + reader.BaseStream.Position = tempOffset; + + Item item; + if (!Items.TryGetValue(id, out item)) + { + item = new Item(id, series, challengeDeckId, unk3, dlcId, unk5, type); + Items.Add(item.Id, item); + } + item.CodeName.SetText(language, codeName); + item.Name.SetText(language, name); + item.Bio.SetText(language, bio); + } + } + + public override void Save(BinaryWriter writer, Language language) + { + int firstChunkItemSize = 56;// Size of each item in the first chunk + long fileStartPos = writer.BaseStream.Position; + + writer.Write((ulong)Items.Count); + + long offsetsOffset = writer.BaseStream.Position; + writer.Write(new byte[Items.Count * firstChunkItemSize]); + + int index = 0; + foreach(Item item in Items.Values) + { + int keyLen = GetStringSize(item.CodeName.GetText(language), keyEncoding); + int valueLen = GetStringSize(item.Name.GetText(language), valueEncoding); + long tempOffset = writer.BaseStream.Position; + + writer.BaseStream.Position = offsetsOffset + (index * firstChunkItemSize); + writer.Write(item.Id); + writer.Write((int)item.Series); + writer.Write(item.ChallengeDeckId); + writer.Write(item.Unk3); + writer.Write(item.DlcId); + writer.Write(item.Unk5); + writer.Write(item.Type); + writer.WriteOffset(fileStartPos, tempOffset); + writer.WriteOffset(fileStartPos, tempOffset + keyLen); + writer.WriteOffset(fileStartPos, tempOffset + keyLen + valueLen); + writer.BaseStream.Position = tempOffset; + + writer.WriteNullTerminatedString(item.CodeName.GetText(language), keyEncoding); + writer.WriteNullTerminatedString(item.Name.GetText(language), valueEncoding); + writer.WriteNullTerminatedString(item.Bio.GetText(language), descriptionEncoding); + + index++; + } + } + + public override void Clear() + { + Items.Clear(); + } + + public class Item + { + public int Id { get; set; } + public DuelSeries Series { get; set; } + public int ChallengeDeckId { get; set; } + public int Unk3 { get; set; } + public int DlcId { get; set; } + public int Unk5 { get; set; } + public long Type { get; set; } + + /// + /// The code name for this character. This is the name that can be found in /busts/ + /// + public LocalizedText CodeName { get; set; } + + /// + /// The display name of the character + /// + public LocalizedText Name { get; set; } + + /// + /// An unused character bio. Most characters don't have this and it doesn't appear in game + /// + public LocalizedText Bio { get; set; } + + public Item(int id, DuelSeries series, int challengeDeckId, int unk3, int dlcId, int unk5, long type) + { + Id = id; + Series = series; + ChallengeDeckId = challengeDeckId; + Unk3 = unk3; + DlcId = dlcId; + Unk5 = unk5; + Type = type; + CodeName = new LocalizedText(); + Name = new LocalizedText(); + Bio = new LocalizedText(); + } + + public override string ToString() + { + return "id: " + Id + " series: " + Series + " challengeDeckId: " + ChallengeDeckId + " unk3: " + Unk3 + " dlcId: " + DlcId + " unk5: " + Unk5 + + " type: " + Type + " codeName: '" + CodeName + "' name: '" + Name + "' bio: '" + Bio + "'"; + } + } + } +} diff --git a/Lotd.Core/FileFormats/main/DeckData.cs b/Lotd.Core/FileFormats/main/DeckData.cs new file mode 100644 index 0000000..8480f9b --- /dev/null +++ b/Lotd.Core/FileFormats/main/DeckData.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + public class DeckData : FileData + { + static Encoding deckFileNameEncoding = Encoding.ASCII; + static Encoding deckNameEncoding = Encoding.Unicode; + static Encoding deckDescriptionEncoding = Encoding.Unicode; + static Encoding unkStr1Encoding = Encoding.Unicode; + public Dictionary Items { get; private set; } + + public override bool IsLocalized + { + get { return true; } + } + + public DeckData() + { + Items = new Dictionary(); + } + + public override void Load(BinaryReader reader, long length, Language language) + { + long fileStartPos = reader.BaseStream.Position; + + uint count = (uint)reader.ReadUInt64(); + for (uint i = 0; i < count; i++) + { + int id1 = reader.ReadInt32(); + int id2 = reader.ReadInt32(); + DuelSeries series = (DuelSeries)reader.ReadInt32(); + int signatureCardId = reader.ReadInt32(); + int deckOwner = reader.ReadInt32(); + int unk1 = reader.ReadInt32(); + long deckFileNameOffset = reader.ReadInt64(); + long deckNameOffset = reader.ReadInt64(); + long deckDescriptionOffset = reader.ReadInt64(); + long unkStr1Offset = reader.ReadInt64(); + + long tempOffset = reader.BaseStream.Position; + + reader.BaseStream.Position = fileStartPos + deckFileNameOffset; + string deckFileName = reader.ReadNullTerminatedString(deckFileNameEncoding); + + reader.BaseStream.Position = fileStartPos + deckNameOffset; + string deckName = reader.ReadNullTerminatedString(deckNameEncoding); + + reader.BaseStream.Position = fileStartPos + deckDescriptionOffset; + string deckDescription = reader.ReadNullTerminatedString(deckDescriptionEncoding); + + reader.BaseStream.Position = fileStartPos + unkStr1Offset; + string unkStr1 = reader.ReadNullTerminatedString(unkStr1Encoding); + + reader.BaseStream.Position = tempOffset; + + Item item; + if (!Items.TryGetValue(id1, out item)) + { + item = new Item(id1, id2, series, signatureCardId, deckOwner, unk1); + Items.Add(item.Id1, item); + } + item.DeckFileName.SetText(language, deckFileName); + item.DeckName.SetText(language, deckName); + item.DeckDescription.SetText(language, deckDescription); + item.UnkStr1.SetText(language, unkStr1); + } + } + + public override void Save(BinaryWriter writer, Language language) + { + int firstChunkItemSize = 56;// Size of each item in the first chunk + long fileStartPos = writer.BaseStream.Position; + + writer.Write((ulong)Items.Count); + + long offsetsOffset = writer.BaseStream.Position; + writer.Write(new byte[Items.Count * firstChunkItemSize]); + + int index = 0; + foreach (Item item in Items.Values) + { + int deckFileNameLen = GetStringSize(item.DeckFileName.GetText(language), deckFileNameEncoding); + int deckNameLen = GetStringSize(item.DeckName.GetText(language), deckNameEncoding); + int deckDescriptionLen = GetStringSize(item.DeckDescription.GetText(language), deckDescriptionEncoding); + long tempOffset = writer.BaseStream.Position; + + writer.BaseStream.Position = offsetsOffset + (index * firstChunkItemSize); + writer.Write(item.Id1); + writer.Write(item.Id2); + writer.Write((int)item.Series); + writer.Write(item.SignatureCardId); + writer.Write(item.DeckOwnerId); + writer.Write(item.Unk1); + writer.WriteOffset(fileStartPos, tempOffset); + writer.WriteOffset(fileStartPos, tempOffset + deckFileNameLen); + writer.WriteOffset(fileStartPos, tempOffset + deckFileNameLen + deckNameLen); + writer.WriteOffset(fileStartPos, tempOffset + deckFileNameLen + deckNameLen + deckDescriptionLen); + writer.BaseStream.Position = tempOffset; + + writer.WriteNullTerminatedString(item.DeckFileName.GetText(language), deckFileNameEncoding); + writer.WriteNullTerminatedString(item.DeckName.GetText(language), deckNameEncoding); + writer.WriteNullTerminatedString(item.DeckDescription.GetText(language), deckDescriptionEncoding); + writer.WriteNullTerminatedString(item.UnkStr1.GetText(language), unkStr1Encoding); + + index++; + } + } + + public override void Clear() + { + Items.Clear(); + } + + public class Item + { + public int Id1 { get; set; } + public int Id2 { get; set; } + public DuelSeries Series { get; set; } + public int SignatureCardId { get; set; } + + /// + /// The owner of this deck as an id. This is the same id as in CharData.Item.Id. (Joey, Mai, Kaiba, etc.) + /// + public int DeckOwnerId { get; set; } + + public int Unk1 { get; set; } + public LocalizedText DeckFileName { get; set; } + public LocalizedText DeckName { get; set; } + + /// + /// This is usally left black. I assume it is a description of sorts. The yu-gi deck which uses exodia says + /// "Yami's first deck depends Exodia." one of joeys says "I like dragons." - do these appear anywhere in game? + /// + public LocalizedText DeckDescription { get; set; } + + public LocalizedText UnkStr1 { get; set; } + + public Item(int id1, int id2, DuelSeries series, int signatureCardId, int deckOwner, int unk1) + { + Id1 = id1; + Id2 = id2; + Series = series; + SignatureCardId = signatureCardId; + DeckOwnerId = deckOwner; + Unk1 = unk1; + DeckFileName = new LocalizedText(); + DeckName = new LocalizedText(); + DeckDescription = new LocalizedText(); + UnkStr1 = new LocalizedText(); + } + + public override string ToString() + { + return "id1: " + Id1 + " id2: " + Id1 + " signatureCard: " + SignatureCardId + " deckOwner: " + DeckOwnerId + " unk1: " + Unk1 + + " deckFileName: '" + DeckFileName + "' deckName: '" + DeckName + "' unk4: '" + DeckDescription + "' unkStr1: '" + UnkStr1 + "'"; + } + + public CharData.Item GetDeckOwner(CharData charData) + { + CharData.Item owner; + charData.Items.TryGetValue(DeckOwnerId, out owner); + return owner; + } + } + } +} diff --git a/Lotd.Core/FileFormats/main/DuelData.cs b/Lotd.Core/FileFormats/main/DuelData.cs new file mode 100644 index 0000000..ac33431 --- /dev/null +++ b/Lotd.Core/FileFormats/main/DuelData.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + /// + /// Holds information about duels in the campain duels list + /// + public class DuelData : FileData + { + static Encoding encoding1 = Encoding.ASCII; + static Encoding encoding2 = Encoding.Unicode; + public Dictionary Items { get; private set; } + + public override bool IsLocalized + { + get { return true; } + } + + public DuelData() + { + Items = new Dictionary(); + } + + public override void Load(BinaryReader reader, long length, Language language) + { + long fileStartPos = reader.BaseStream.Position; + + uint count = (uint)reader.ReadUInt64(); + for (uint i = 0; i < count; i++) + { + int id = reader.ReadInt32(); + DuelSeries series = (DuelSeries)reader.ReadInt32(); + int displayIndex = reader.ReadInt32(); + int playerCharId = reader.ReadInt32(); + int opponentCharId = reader.ReadInt32(); + int playerDeckId = reader.ReadInt32(); + int opponentDeckId = reader.ReadInt32(); + int arenaId = reader.ReadInt32(); + int unk8 = reader.ReadInt32(); + int dlcId = reader.ReadInt32(); + long codeNameOffset = reader.ReadInt64(); + long playerAlternateSkinOffset = reader.ReadInt64(); + long opponentAlternateSkinOffset = reader.ReadInt64(); + long nameOffset = reader.ReadInt64(); + long descriptionOffset = reader.ReadInt64(); + long tipOffset = reader.ReadInt64(); + + long tempOffset = reader.BaseStream.Position; + + reader.BaseStream.Position = fileStartPos + codeNameOffset; + string codeName = reader.ReadNullTerminatedString(encoding1); + + reader.BaseStream.Position = fileStartPos + playerAlternateSkinOffset; + string playerAlternateSkin = reader.ReadNullTerminatedString(encoding1); + + reader.BaseStream.Position = fileStartPos + opponentAlternateSkinOffset; + string opponentAlternateSkin = reader.ReadNullTerminatedString(encoding1); + + reader.BaseStream.Position = fileStartPos + nameOffset; + string name = reader.ReadNullTerminatedString(encoding2); + + reader.BaseStream.Position = fileStartPos + descriptionOffset; + string description = reader.ReadNullTerminatedString(encoding2); + + reader.BaseStream.Position = fileStartPos + tipOffset; + string tipStr = reader.ReadNullTerminatedString(encoding2); + + reader.BaseStream.Position = tempOffset; + + Item item; + if (!Items.TryGetValue(id, out item)) + { + item = new Item(id, series, displayIndex, playerCharId, opponentCharId, playerDeckId, opponentDeckId, arenaId, unk8, dlcId); + Items.Add(item.Id, item); + } + item.CodeName.SetText(language, codeName); + item.PlayerAlternateSkin.SetText(language, playerAlternateSkin); + item.OpponentAlternateSkin.SetText(language, opponentAlternateSkin); + item.Name.SetText(language, name); + item.Description.SetText(language, description); + item.Tip.SetText(language, tipStr); + } + } + + public override void Save(BinaryWriter writer, Language language) + { + int firstChunkItemSize = 88;// Size of each item in the first chunk + long fileStartPos = writer.BaseStream.Position; + + writer.Write((ulong)Items.Count); + + long offsetsOffset = writer.BaseStream.Position; + writer.Write(new byte[Items.Count * firstChunkItemSize]); + + int index = 0; + foreach (Item item in Items.Values) + { + int codeNameLen = GetStringSize(item.CodeName.GetText(language), encoding1); + int playerAlternateSkinLen = GetStringSize(item.PlayerAlternateSkin.GetText(language), encoding1); + int opponentAlternateSkinLen = GetStringSize(item.OpponentAlternateSkin.GetText(language), encoding1); + int nameLen = GetStringSize(item.Name.GetText(language), encoding2); + int descriptionLen = GetStringSize(item.Description.GetText(language), encoding2); + long tempOffset = writer.BaseStream.Position; + + writer.BaseStream.Position = offsetsOffset + (index * firstChunkItemSize); + writer.Write(item.Id); + writer.Write((int)item.Series); + writer.Write(item.DisplayIndex); + writer.Write(item.PlayerCharId); + writer.Write(item.OpponentCharId); + writer.Write(item.PlayerDeckId); + writer.Write(item.OpponentDeckId); + writer.Write(item.ArenaId); + writer.Write(item.Unk8); + writer.Write(item.DlcId); + writer.WriteOffset(fileStartPos, tempOffset); + writer.WriteOffset(fileStartPos, tempOffset + codeNameLen); + writer.WriteOffset(fileStartPos, tempOffset + codeNameLen + playerAlternateSkinLen); + writer.WriteOffset(fileStartPos, tempOffset + codeNameLen + playerAlternateSkinLen + opponentAlternateSkinLen); + writer.WriteOffset(fileStartPos, tempOffset + codeNameLen + playerAlternateSkinLen + opponentAlternateSkinLen + nameLen); + writer.WriteOffset(fileStartPos, tempOffset + codeNameLen + playerAlternateSkinLen + opponentAlternateSkinLen + nameLen + descriptionLen); + writer.BaseStream.Position = tempOffset; + + writer.WriteNullTerminatedString(item.CodeName.GetText(language), encoding1); + writer.WriteNullTerminatedString(item.PlayerAlternateSkin.GetText(language), encoding1); + writer.WriteNullTerminatedString(item.OpponentAlternateSkin.GetText(language), encoding1); + writer.WriteNullTerminatedString(item.Name.GetText(language), encoding2); + writer.WriteNullTerminatedString(item.Description.GetText(language), encoding2); + writer.WriteNullTerminatedString(item.Tip.GetText(language), encoding2); + + index++; + } + } + + public class Item + { + public int Id { get; set; } + + /// + /// The series this duel belongs to (Yu-Gi-Oh!, GX, 5D's, ZEXAL, ARC-V) + /// + public DuelSeries Series { get; set; } + + /// + /// The index at which this duel will be displayed in the list (unclear what happens if there are duplicates) + /// Note that this starts at 1 for each series and increments. + /// Note that in the original data there are some out of order but there aren't any duplicates (GX). + /// + public int DisplayIndex { get; set; } + + /// + /// Your character id. This is an id which maps into CharData. Use that structure get to the character info. + /// + public int PlayerCharId { get; set; } + + /// + /// Your opponents character id. This is an id which maps into CharData. Use that structure get to the character info. + /// + public int OpponentCharId { get; set; } + + /// + /// Your deck id. This is an id which maps into DeckData. Use that structure to get the deck info. + /// + public int PlayerDeckId { get; set; } + + /// + /// Your opponents deck id. This is an id which maps into DeckData. Use that structure to get the deck info. + /// + public int OpponentDeckId { get; set; } + + /// + /// The arena this duel takes place. This is an id which maps into ArenaData. Use that structure to get the arena info. + /// + public int ArenaId { get; set; } + + public int Unk8 { get; set; } + public int DlcId { get; set; } + + /// + /// The code name for this duel. This is likely used internally. It is usually the duel name with spaces. + /// e.g. "TheDuelistKingdom", "TheHeartOfTheCards", "TheUltimateGreatMoth" + /// + public LocalizedText CodeName { get; set; } + + /// + /// Alternate skin / style for your character. + /// These can be seen if you open the 'busts' folder and see prefixes before "_neutral" + /// e.g. "alternate", "barian", "dark", "glasses", "blue", "notattoo" + /// + public LocalizedText PlayerAlternateSkin { get; set; } + + /// + /// Alternate skin / style for your opponents character. + /// + public LocalizedText OpponentAlternateSkin { get; set; } + + /// + /// The name of the duel. This is the name that is displayed in the campaign duels list. + /// e.g. "The Duelist Kingdom", "The heart of the Cards", "The Ultimate Great Moth" + /// + public LocalizedText Name { get; set; } + + /// + /// The description / reason for the duel (this isn't displayed anywhere in-game?). + /// Note that some of these are blank, code names ("ARCVDuel_03_1") or the same as the duel name + /// "Help Yugi Muto explain the rules of Duel Monsters to Joey Wheeler." + /// "Seto Kaiba defeated Solomon Muto and has destroyed his rare Blue-Eyes White Dragon card!" + /// "The Duelist Tournament has begun! In the first round it is Yugi Muto versus the underhanded and sneaky Weevil Underwood." + /// + public LocalizedText Description { get; set; } + + /// + /// This tip is displayed when you lose the duel (in the blue box on the right - above the rewards) + /// + public LocalizedText Tip { get; set; } + + public Item(int id, DuelSeries series, int displayIndex, int playerCharId, int opponentCharId, + int playerDeckId, int opponentDeckId, int duelArena, int unk8, int dlcId) + { + Id = id; + Series = series; + DisplayIndex = displayIndex; + PlayerCharId = playerCharId; + OpponentCharId = opponentCharId; + PlayerDeckId = playerDeckId; + OpponentDeckId = opponentDeckId; + ArenaId = duelArena; + Unk8 = unk8; + DlcId = dlcId; + CodeName = new LocalizedText(); + PlayerAlternateSkin = new LocalizedText(); + OpponentAlternateSkin = new LocalizedText(); + Name = new LocalizedText(); + Description = new LocalizedText(); + Tip = new LocalizedText(); + } + + public override string ToString() + { + return "id: " + Id + + " series: " + Series + " displayIndex: " + DisplayIndex + + " playerCharId: " + PlayerCharId + " opponentCharId: " + OpponentCharId + + " playerDeckId: " + PlayerDeckId + " opponentDeckId: " + OpponentDeckId + + " arenaId: " + ArenaId + " unk8: " + Unk8 + " dlcId: " + DlcId + + + " codeName: '" + CodeName + "' playerAlternateSkin: '" + PlayerAlternateSkin + + "' opponentAlternateSkin: '" + OpponentAlternateSkin + "' name: '" + Name + + "' unkStr5: '" + Description + "' tip: '" + Tip + "'"; + } + } + } +} diff --git a/Lotd.Core/FileFormats/main/PackDefData.cs b/Lotd.Core/FileFormats/main/PackDefData.cs new file mode 100644 index 0000000..5ba8ca0 --- /dev/null +++ b/Lotd.Core/FileFormats/main/PackDefData.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + /// + /// Holds basic information for packs which includes both shop packs and "battle pack" packs. + /// Note that this doesn't include card data. See ShopPackData/BattlePackData for the card information. + /// + public class PackDefData : FileData + { + static Encoding encoding1 = Encoding.ASCII; + static Encoding encoding2 = Encoding.Unicode; + public Dictionary Items { get; private set; } + + public override bool IsLocalized + { + get { return true; } + } + + public PackDefData() + { + Items = new Dictionary(); + } + + public override void Load(BinaryReader reader, long length, Language language) + { + long fileStartPos = reader.BaseStream.Position; + + uint count = (uint)reader.ReadUInt64(); + for (uint i = 0; i < count; i++) + { + int id = reader.ReadInt32(); + DuelSeries series = (DuelSeries)reader.ReadInt32(); + int price = reader.ReadInt32(); + PackType type = (PackType)reader.ReadInt32(); + long codeNameOffset = reader.ReadInt64(); + long nameOffset = reader.ReadInt64(); + long unkStrOffset = reader.ReadInt64(); + + long tempOffset = reader.BaseStream.Position; + + reader.BaseStream.Position = fileStartPos + codeNameOffset; + string codeName = reader.ReadNullTerminatedString(encoding1); + + reader.BaseStream.Position = fileStartPos + nameOffset; + string name = reader.ReadNullTerminatedString(encoding2); + + reader.BaseStream.Position = fileStartPos + unkStrOffset; + string unkStr = reader.ReadNullTerminatedString(encoding2); + + reader.BaseStream.Position = tempOffset; + + Item item; + if (!Items.TryGetValue(id, out item)) + { + item = new Item(id, series, price, type); + Items.Add(item.Id, item); + } + item.CodeName.SetText(language, codeName); + item.Name.SetText(language, name); + item.UnkStr.SetText(language, unkStr); + } + } + + public override void Save(BinaryWriter writer, Language language) + { + int firstChunkItemSize = 40;// Size of each item in the first chunk + long fileStartPos = writer.BaseStream.Position; + + writer.Write((ulong)Items.Count); + + long offsetsOffset = writer.BaseStream.Position; + writer.Write(new byte[Items.Count * firstChunkItemSize]); + + int index = 0; + foreach (Item item in Items.Values) + { + int codeNameLen = GetStringSize(item.CodeName.GetText(language), encoding1); + int nameLen = GetStringSize(item.Name.GetText(language), encoding2); + long tempOffset = writer.BaseStream.Position; + + writer.BaseStream.Position = offsetsOffset + (index * firstChunkItemSize); + writer.Write(item.Id); + writer.Write((int)item.Series); + writer.Write(item.Price); + writer.Write((int)item.Type); + writer.WriteOffset(fileStartPos, tempOffset); + writer.WriteOffset(fileStartPos, tempOffset + codeNameLen); + writer.WriteOffset(fileStartPos, tempOffset + codeNameLen + nameLen); + writer.BaseStream.Position = tempOffset; + + writer.WriteNullTerminatedString(item.CodeName.GetText(language), encoding1); + writer.WriteNullTerminatedString(item.Name.GetText(language), encoding2); + writer.WriteNullTerminatedString(item.UnkStr.GetText(language), encoding2); + + index++; + } + } + + public class Item + { + public int Id { get; set; } + + /// + /// The series this pack belongs to (Yu-Gi-Oh!, GX, 5D's, ZEXAL, ARC-V) + /// + public DuelSeries Series { get; set; } + + /// + /// The price of this pack. + /// + public int Price { get; set; } + + /// + /// Pack type - 82 (regular) / 66 (battle packs) + /// + public PackType Type { get; set; } + + /// + /// The short form code name of the pack (this maps to /packs/reward_wrap_XXXX.png - unclear if you can add new ones). + /// + public LocalizedText CodeName { get; set; } + + /// + /// The name for the pack (this is the character for regular packs, battle pack name for battle packs) + /// + public LocalizedText Name { get; set; } + + /// + /// Unknown - always an empty string + /// + public LocalizedText UnkStr { get; set; } + + public Item(int id, DuelSeries pack, int price, PackType type) + { + Id = id; + Series = pack; + Price = price; + Type = type; + CodeName = new LocalizedText(); + Name = new LocalizedText(); + UnkStr = new LocalizedText(); + } + + public override string ToString() + { + return "id: " + Id + " series: " + Series + " price: " + Price + " type: " + Type + + " codeName: '" + CodeName + "' name: '" + Name + "' unkStr: '" + UnkStr + "'"; + } + } + } + + public enum PackType + { + Shop = 82, + Battle = 66 + } +} diff --git a/Lotd.Core/FileFormats/main/ScriptData.cs b/Lotd.Core/FileFormats/main/ScriptData.cs new file mode 100644 index 0000000..0fe29d9 --- /dev/null +++ b/Lotd.Core/FileFormats/main/ScriptData.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Lotd.FileFormats +{ + /// + /// This represents the scripted conversations shown before / after duels in campaign gameplay. + /// + public class ScriptData : FileData + { + static Encoding encoding = Encoding.UTF8; + public List