diff --git a/CNAME b/CNAME new file mode 100644 index 000000000..483b92ee2 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +openkh.dev diff --git a/CODE_GUIDELINE.md b/CODE_GUIDELINE.md index ddab7f852..454a72131 100644 --- a/CODE_GUIDELINE.md +++ b/CODE_GUIDELINE.md @@ -2,51 +2,51 @@ ## Do not re-invent the wheel -The main goal of the project is to have a central point for everything related to Kingdom Hearts. If you want to use a functionality, do not start to code it before to check if it already exists. If you are not happy with the code, you do not understand how you can re-use it or if you want to improve it, check the [contributing guide](CONTRIBUTING.md). +The main goal of the project is to have a central point for everything related to Kingdom Hearts. If you want to add functionality, do not start coding something unless you've checked and made sure it doesn't already exist. If you are not happy with the code, do not understand how you can use it, or if you want to improve upon it, check the [contributing guide](CONTRIBUTING.md) or ask for assistance from other maintainers. ## The power of refactoring -It is okay to not strictly respect the content below this document (however it is a best practice to follow it in order to achieve optimal and comformant code). Regardless the technology, the programming language used or the coding style, as long as something works it is fine; it can be always optimized and refactored later. +It is okay to not strictly respect the content below this document (however it is a best practice to follow it in order to achieve optimal and comformant code). Regardless of the technology, the programming language used, or the coding style, as long as something works, it is fine; it can be always optimized and refactored later if needed. ## Property of the code -Any portion of code committed to the repository belong to OpenKH and its community and it is subject to the specified [license](LICENSE). +Any portion of code committed to the repository belongs to OpenKH and its community and it is subject to the [specified license](LICENSE). # Naming convention -Pascal case is used everywhere in the code. When it is using a short version of a name (eg. from Kingdom Hearts to KH), only the first letter is capitalized (eg. Kh). +Pascal case is used everywhere in the code. When it is using a short version of a name (E.g. from Kingdom Hearts to KH), only the first letter is capitalized (E.g. Kh). ## Libraries -Every library should start with `OpenKh.*`, where `*` is the short name of the game (eg. `Kh1`, `KhBbs`, `KhReCom` etc.). If the content is more related to a specific engine, use the name of the engine instead (eg. `Osaka` for KhBbs and Kh3d, `Unreal` for Kh02 and Kh3). If the content is generic, it has to be placed in `Common`, `Imaging`, `Messages` or other classes that defines a common group. +Every library should start with `OpenKh.*`, where `*` is the short name of the game (E.g. `Kh1`, `KhBbs`, `KhReCom` etc.). If the content is more related to a specific engine, use the name of the engine instead (E.g. `Tokyo` for Kh1 and Kh2, `Osaka` for KhBbs and Kh3d, `Unreal` for Kh02 and Kh3). If the content is generic, it should be placed in `Common`, `Imaging`, `Messages` or other classes that defines a common group shared between games. ## Graphics tools -Every GUI tool should start with `OpenKh.Tools.*`, where `*` is a short name of the application. It is preferrable to use a verb at the end of the name to give an idea of the capability of the tool (eg. `Viewer`, `Converter`, `Editor`). All the common logic for the GUI tools should be placed in `OpenKh.Tools.Common` +Every GUI tool should start with `OpenKh.Tools.*`, where `*` is a short name of the application. It is preferrable to use an appropriate short description at the end of the name to give an idea of the capability of the tool (E.g. `Viewer`, `Converter`, `Editor`). All the common logic for the GUI tools should be placed in `OpenKh.Tools.Common` ## Command tools -Every command line tool should start with `OpenKh.Command.*`, where `*` is a short name of the application. There are no particular restrictions about how to name a command line tool. +Every command line tool should start with `OpenKh.Command.*`, where `*` is a short name of the application. There are no particular restrictions about how to name a command line tool otherwise. # Technology ## Programming language -Right now, all the code base is written in C# 7.0. +Currently, the entire codebase is written in C# 7.0. The framework used for the common libraries is .Net Standard 2.0, which is compatible with .Net Core 2.0, .Net Framework 4.6.1, Mono 5.4 and others. -The framework used for the tools is .Net Framework 4.7.1, which currently works only on Windows. +The framework used for the tools is .Net Framework 4.7.1, which currently only works on Windows. There are plans to rectify this over time as the project progresses. ## Third party libraries -The main framework for GUI is `WPF`. For the rendering engine in the tools, `SharpDx` is currently used. As a game engine it is preferrable to use `MonoGame`. +The main framework for GUI is `WPF`. For the rendering engine in the tools, `SharpDx` is currently used. `Monogame` will be preferred for the game engine. # Coding style ## Casting -Avoid hard type casting like `(Foo)obj` since it can easily throw a `InvalidCastException` on runtime. Prefer to use `obj is Foo` to check if `obj` is or inhert the type `Foo`. Use `obj as Foo` to cast on type `Foo`, which returns `null` if `obj` is not or does not inhert `Foo`, instead of throwing a runtime exception. +Avoid hard type casting like `(Foo)obj` since it can easily throw an `InvalidCastException` on runtime. Preferably, use `obj is Foo` to check if `obj` is or inhert the type `Foo`. Use `obj as Foo` to cast on type `Foo`, which returns `null` if `obj` is not or does not inhert `Foo`, instead of throwing a runtime exception. To avoid reduntant code and use both `is` and `as` operators, it is possible to use the following statement: ```csharp @@ -58,23 +58,23 @@ else ## Interfaces -They are a powerful instrument used for **inversion of control** and **contract** definition. +Interfaces are a powerful instrument used for **inversion of control** and **contract** definition. -A good example is [`OpenKh.Imaging.IImage`](https://github.com/Xeeynamo/OpenKh/blob/master/OpenKh.Imaging/IImage.cs), which defines the minimum amount of information to represent an image. All the methods that accepts a `IImage` as parameter can accept any image class that implements `IImage` interface. +A good example is [`OpenKh.Imaging.IImage`](https://github.com/Xeeynamo/OpenKh/blob/master/OpenKh.Imaging/IImage.cs), which defines the minimum amount of information to represent an image. All the methods that accept `IImage` as a parameter can accept any image class that implements the `IImage` interface. ## Helper methods -C# comes with this excellent feature called [extension methods](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods) that allows to add any member to existing class or interfaces. +C# comes with this excellent feature called [extension methods](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods) that allow the addition of any member to existing classes or interfaces. Taking [`OpenKh.Imaging.IImageRead`](https://github.com/Xeeynamo/OpenKh/blob/master/OpenKh.Imaging/IImageRead.cs), which is a super set of `IImage`, the class [`OpenKh.Imaging.ImageHelpers`](https://github.com/Xeeynamo/OpenKh/blob/master/OpenKh.Imaging/ImageHelpers.cs) contains a bunch of methods that can be used with any ``IImageRead`. -If we take `public static void SaveImage(this IImageRead imageRead, string fileName)` as an example, it is possible to invoke `IImageRead.SaveImage(fileName)` without having that method implemented in any of the interface implementations. Now, all the classes that implements `IImageRead` can use `SaveImage` (eg. `new Imgd(input).SaveImage(output)`). +If we take `public static void SaveImage(this IImageRead imageRead, string fileName)` as an example, it is possible to invoke `IImageRead.SaveImage(fileName)` without having that method implemented in any of the interface implementations. Now, all the classes that implement `IImageRead` can use `SaveImage` (E.g. `new Imgd(input).SaveImage(output)`). ## Lambda functions -In C# it is possible to use functions as first class citizens. This becomes really useful to inject some logic into a method. +In C# it is possible to use functions as first class citizens. This is really useful for injecting logic into a method. -Eg. +E.g. ```csharp var funcFilterBarFiles = fileName => Path.GetExtension(fileName) == ".bar"; @@ -87,9 +87,9 @@ IEnumerable GetFiles(Func filter) => ## LINQ -This is one of the most powerful libraries of C# that comes for free. With [LINQ](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/basic-linq-query-operations) it is possible to perform operations to list of data in a SQL/NoSQL way, filtering, transforming, aggregating and manipulating lists. It can completely replace for loops and can dramatically reduces lines of code. +This is one of the most powerful libraries of C# that can be used for free. With [LINQ](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/basic-linq-query-operations) it is possible to perform operations to list data in a SQL/NoSQL way, filtering, transforming, aggregating, and manipulating lists. It can completely replace loops and can dramatically reduce lines of code. -Eg. This is a portion of code using a common for loop: +This is a portion of code using a common for a loop: ```csharp List GetList(Entry[] entries) @@ -114,12 +114,16 @@ IEnumerable GetList(IEnumerable entries) => entries ## Paradigm -It is preferrable to use a functional approach, which removes the over usage of branch conditions, protective code and null checking, while adding code readability. +It is preferrable to use a functional approach, which removes the overuse of branch conditions, protective code, and null checking, while supplementing code readability. -The functional paradigm comes with the concept of immutable objects rather than classes full of conditional logic. Imagine that as the `string` or `DateTime` type in C#, where once the object is created it cannot be modified. +The functional paradigm comes with the concept of immutable objects rather than classes full of conditional logic. Imagine the `string` or `DateTime` type in C#, where once the object is created it cannot be modified. ## Testing -Every line of code is supposed to be covered by unit testing. The approach used is TDD ([Test Driven Development](https://www.amazon.com/dp/0321146530)), where the tests are written before the implementation, as requirement or check list. +Every line of code is supposed to be covered by unit testing. The approach used is TDD ([Test Driven Development](https://www.amazon.com/dp/0321146530)), where the tests are written before the implementation, as requirements or a checklist. -The advantage of it is to be 100% sure that everything is working as expected, giving more confidence when refactoring. Since the tests are automated, they just requires few seconds to run. \ No newline at end of file +The advantage of this method is it allows us to be 100% sure that everything works as expected. This gives more confidence in the event of refactoring. Since tests are automated, they require only seconds to run. + +## Conclusion + +While it may be the best practice to follow these guidelines to make code more readable, easy to use, extensive, and portable, they're not necessarily strictly enforced! We recognize that everyone has their own coding style and preferences, so taking that into account is important for a project of this scope. As such, discussion and questions regarding someone's syntax in comparison to another's is perfectly fine as long as it remains civil and provides a good experience for everyone involved. [Built by a friendly community, for a friendly community!](CODE_OF_CONDUCT.md) \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 3cc81d754..8b7bb684a 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -4,10 +4,21 @@ In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. +our community a harassment-free experience for everyone, regardless of: +* Age +* Body size +* Disability +* Ethnicity +* Sex characteristics +* Gender identity and expression +* Level of experience +* Education +* Socio-economic status +* Nationality +* Personal appearance +* Race +* Religion +* Sexual identity and orientation ## Our Standards @@ -17,7 +28,7 @@ include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism -* Focusing on what is best for the community +* Focusing on what is best for the community and each other as a whole * Showing empathy towards other community members Examples of unacceptable behavior by participants include: @@ -27,7 +38,7 @@ Examples of unacceptable behavior by participants include: * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic - address, without explicit permission + addresses, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting @@ -39,9 +50,9 @@ response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. +that are not aligned to this Code of Conduct, or to temporarily or permanently +ban any contributor for other behaviors that they deem inappropriate, threatening, +offensive, or harmful. ## Scope @@ -54,12 +65,14 @@ a project may be further defined and clarified by project maintainers. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team to [Discord](https://discord.gg/MgTaQgf). All +Instances of harassment, abusive, or otherwise unacceptable behavior may be +reported by contacting the project team at [Discord](https://discord.gg/GVtG3Zu). All complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. +is deemed necessary and appropriate given the circumstances. The project team is +obligated to maintain confidentiality in regards to the reporter of an incident, +meaning details of reports and the one who reported another user will not be +named or otherwise openly shared where others can see. Further details of specific +enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f1eda0448..78e693336 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,22 +6,22 @@ The project involves code and documentation. There is no need for you to have co ## Ask questions -Asking is the most important part when it comes to helping the project. If there is any aspect that you would like to help improve, feel free to discuss it openly! +Your curiosity is the most important part when it comes to helping the project. If there is any aspect that you would like to help improve, feel free to ask about it and otherwise discuss it openly! -There are multiple ways to open a discussion. You can [open a new issue](https://github.com/Xeeynamo/OpenKh/issues/new) or [join to our Discord server](https://discord.gg/MgTaQgf). There is even a [Trello board](https://trello.com/b/xUMpsGBE/openkh) showing an organized list of objectives that need to be worked on, have been completed, or are currently being worked on. +There are multiple ways to open a discussion. You can [open a new issue](https://github.com/Xeeynamo/OpenKh/issues/new) or [join our Discord server](https://discord.gg/GVtG3Zu). There is even a [Trello board](https://trello.com/b/xUMpsGBE/openkh) (currently unmaintained) showing an organized list of objectives that need to be worked on, have been completed, or are currently being worked on. ## Bug report -To report bugs and errors, [open a new issue](https://github.com/Xeeynamo/OpenKh/issues/new) and specify the steps to reproduce the bug, including the stack trace, messages, and the files that the tools or libraries were trying to open. +To report bugs and errors, [open a new issue](https://github.com/Xeeynamo/OpenKh/issues/new) and specify the steps taken to reproduce the bug, including the stack trace, messages, and the files that the tools or libraries were trying to open. ## Feature request -To request a feature, [open a new issue](https://github.com/Xeeynamo/OpenKh/issues/new) detailing your request. This can be anything from optimizations to new features. +To request a feature, [open a new issue](https://github.com/Xeeynamo/OpenKh/issues/new) detailing your request. This can be anything from code or format optimizations to new features. ## Code guideline -[This](CODE_GUIDELINE.md) is a document that gives an idea on the code requirements to have your content merged into the master branch. For the most part, as long as the code works as intended and can be cleaned up later if needed, it's more than welcome! +[This](CODE_GUIDELINE.md) is a document that gives an idea of the code requirements to have your content merged into the master branch. For the most part, as long as the code works as intended and can be cleaned up later if needed, it's more than welcome! While C# is the primary language, other languages are welcome as long as they do not compromise the usability or readability of the toolchain or codebase! ## Documents contribution -Documentation is really important, since it is what everyone (including non-coders) can read and understand. It includes every technical research done for all the games in the series, like file formats, differences between each version of the game, unused content, how to use the libraries, tools, or modding tutorials. RAM hacking content is more than welcome too. +Documentation is really important, as it is what everyone (including non-coders) can read and understand. It includes all technical research done for the games in the series, such as file formats, differences between various version of the game in question, unused content, how to use the libraries, tools, or modding tutorials, etc. RAM hacking content is more than welcome too. diff --git a/OpenKh.Bbs/Arc.cs b/OpenKh.Bbs/Arc.cs new file mode 100644 index 000000000..445d50e26 --- /dev/null +++ b/OpenKh.Bbs/Arc.cs @@ -0,0 +1,116 @@ +using OpenKh.Common; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xe.BinaryMapper; + +namespace OpenKh.Bbs +{ + public static class Arc + { + private const int MagicCode = 0x435241; + private const int Version = 1; + private const int MetaEntrySize = 0x20; + private const int Alignment = 0x10; + + private class Header + { + [Data] public int MagicCode { get; set; } + [Data] public short Version { get; set; } + [Data] public short EntryCount { get; set; } + [Data] public int Unused08 { get; set; } + [Data] public int Unused0c { get; set; } + } + + private class MetaEntry + { + [Data] public uint DirectoryPointer { get; set; } + [Data] public int Offset { get; set; } + [Data] public int Length { get; set; } + [Data] public int Unused { get; set; } + [Data(Count = 16)] public string Name { get; set; } + + public bool IsPointer => DirectoryPointer != 0; + } + + public class Entry + { + public bool IsLink => DirectoryPointer != 0; + public uint DirectoryPointer { get; set; } + public string Name { get; set; } + public byte[] Data { get; set; } + + public string Path + { + get + { + if (DirectoryPointer == 0) + return Name; + + var directory = Bbsa.GetDirectoryName(DirectoryPointer); + if (string.IsNullOrEmpty(directory)) + return $"{DirectoryPointer:X08}/{Name}"; + + return $"{directory}/{Name}"; + + } + } + } + + public static IEnumerable Read(Stream stream) + { + var header = BinaryMapping.ReadObject
(stream.SetPosition(0)); + + return Enumerable.Range(0, header.EntryCount) + .Select(x => BinaryMapping.ReadObject(stream)) + .ToArray() + .Select(x => new Entry + { + DirectoryPointer = x.DirectoryPointer, + Name = x.Name, + Data = x.IsPointer ? null : stream.SetPosition(x.Offset).ReadBytes(x.Length) + }) + .ToArray(); + } + + public static void Write(this IEnumerable entries, Stream stream) + { + var myEntries = entries.ToArray(); + + stream.Position = 0; + BinaryMapping.WriteObject(stream, new Header + { + MagicCode = MagicCode, + Version = Version, + EntryCount = (short)myEntries.Length, + Unused08 = 0, + Unused0c = 0 + }); + + var dataStartOffset = (int)stream.Position + myEntries.Length * MetaEntrySize; + foreach (var entry in myEntries) + { + BinaryMapping.WriteObject(stream, new MetaEntry + { + DirectoryPointer = entry.DirectoryPointer, + Offset = entry.IsLink ? 0 : dataStartOffset, + Length = entry.IsLink ? 0 : entry.Data.Length, + Unused = 0, + Name = entry.Name + }); + + dataStartOffset += Helpers.Align(entry.Data?.Length ?? 0, Alignment); + } + + foreach (var entry in myEntries.Where(x => !x.IsLink)) + { + stream.Write(entry.Data, 0, entry.Data.Length); + stream.AlignPosition(Alignment); + } + } + + public static bool IsValid(Stream stream) => + stream.Length >= 4 && + new BinaryReader(stream.SetPosition(0)).ReadInt32() == MagicCode; + } +} diff --git a/OpenKh.Bbs/Bbsa.Entry.cs b/OpenKh.Bbs/Bbsa.Entry.cs index 91fe54499..45069b075 100644 --- a/OpenKh.Bbs/Bbsa.Entry.cs +++ b/OpenKh.Bbs/Bbsa.Entry.cs @@ -156,7 +156,7 @@ private static string CalculateExtension(Stream stream, int offset) private static string CalculateFolderName(uint hash) { - var category = hash >> 24; + var category = (byte)(hash >> 24); var world = (hash >> 16) & 0x1F; var language = (hash >> 21) & 7; var id = hash & 0xFFFF; @@ -166,33 +166,10 @@ private static string CalculateFolderName(uint hash) var strLanguage = language < Constants.Language.Length ? Constants.Language[language] : null; - switch (category) - { - case 0x00: return "arc_"; - case 0x80: return "sound/bgm"; - case 0xC0: return "lua"; - case 0x90: return $"sound/se/common"; - case 0x91: return $"sound/se/event/{strWorld}"; - case 0x92: return $"sound/se/footstep/{strWorld}"; - case 0x93: return "sound/se/enemy"; - case 0x94: return "sound/se/weapon"; - case 0x95: return "sound/se/act"; - case 0xA1: return $"sound/voice/{strLanguage}/event/{strWorld}"; - case 0xAA: return $"sound/voice/{strLanguage}/battle"; - case 0xD0: return $"message/{strLanguage}/system"; - case 0xD1: return $"message/{strLanguage}/map"; - case 0xD2: return $"message/{strLanguage}/menu"; - case 0xD3: return $"message/{strLanguage}/event"; - case 0xD4: return $"message/{strLanguage}/mission"; - case 0xD5: return $"message/{strLanguage}/npc_talk/{strWorld}"; - case 0xD6: return $"message/{strLanguage}/network"; - case 0xD7: return $"message/{strLanguage}/battledice"; - case 0xD8: return $"message/{strLanguage}/minigame"; - case 0xD9: return $"message/{strLanguage}/shop"; - case 0xDA: return $"message/{strLanguage}/playerselect"; - case 0xDB: return $"message/{strLanguage}/report"; - default: return null; - } + if (!PathCategories.TryGetValue(category, out var pathCategory)) + return null; + + return string.Format(pathCategory, strLanguage, strWorld); } } } diff --git a/OpenKh.Bbs/Bbsa.Partition.cs b/OpenKh.Bbs/Bbsa.Partition.cs index 81d60d2de..f617e1456 100644 --- a/OpenKh.Bbs/Bbsa.Partition.cs +++ b/OpenKh.Bbs/Bbsa.Partition.cs @@ -14,7 +14,7 @@ protected interface ILba protected class Partition where TLba : ILba { - [Data] public int Name { get; set; } + [Data] public uint Name { get; set; } [Data] public short Count { get; set; } [Data] public short Offset { get; set; } public TLba[] Lba { get; set; } diff --git a/OpenKh.Bbs/Bbsa.cs b/OpenKh.Bbs/Bbsa.cs index 4d7c82f6e..1044a8805 100644 --- a/OpenKh.Bbs/Bbsa.cs +++ b/OpenKh.Bbs/Bbsa.cs @@ -22,7 +22,7 @@ public partial class Bbsa "PMF", "ESE", "PTX", "" }; - protected static Dictionary Paths = new Dictionary + protected static Dictionary Paths = new Dictionary { [0x0050414D] = "arc/map", [0x4E455645] = "arc/event", @@ -45,7 +45,52 @@ public partial class Bbsa [0x55424544] = "arc/debug", }; - protected static Dictionary PathsReverse = + protected static Dictionary PathCategories = new Dictionary + { + [0x00] = "arc_", + [0x80] = "sound/bgm", + [0xC0] = "lua", + [0x90] = "sound/se/common", + [0x91] = "sound/se/event/{1}", + [0x92] = "sound/se/footstep/{1}", + [0x93] = "sound/se/enemy", + [0x94] = "sound/se/weapon", + [0x95] = "sound/se/act", + [0xA1] = "sound/voice/{0}/event/{1}", + [0xAA] = "sound/voice/{0}/battle", + [0xD0] = "message/{0}/system", + [0xD1] = "message/{0}/map", + [0xD2] = "message/{0}/menu", + [0xD3] = "message/{0}/event", + [0xD4] = "message/{0}/mission", + [0xD5] = "message/{0}/npc_talk/{1}", + [0xD6] = "message/{0}/network", + [0xD7] = "message/{0}/battledice", + [0xD8] = "message/{0}/minigame", + [0xD9] = "message/{0}/shop", + [0xDA] = "message/{0}/playerselect", + [0xDB] = "message/{0}/report", + }; + + protected static Dictionary PathCategoriesReverse = PathCategories + .SelectMany(x => + Constants.Language.Select((l, i) => new + { + Key = (x.Key << 24) | (i << 21), + Value = x.Value.Replace("{0}", l) + }) + ) + .SelectMany(x => + Constants.Worlds.Select((w, i) => new + { + Key = x.Key | (i << 16), + Value = x.Value.Replace("{1}", w) + }) + ) + .GroupBy(x => x.Value) + .ToDictionary(x => x.Key, x => (uint)x.First().Key); + + protected static Dictionary PathsReverse = Paths.ToDictionary(x => x.Value, x => x.Key); protected static string[] AllPaths = @@ -190,5 +235,19 @@ public int GetOffset(string fileName) } public static Bbsa Read(Stream stream) => new Bbsa(stream); + + public static string GetDirectoryName(uint hash) => + Paths.TryGetValue(hash, out var path) ? path : CalculateFolderName(hash); + + public static uint GetDirectoryHash(string directory) + { + if (PathsReverse.TryGetValue(directory.ToLower(), out var hash)) + return (uint)hash; + + if (PathCategoriesReverse.TryGetValue(directory.ToLower(), out hash)) + return (uint)hash; + + return uint.MaxValue; + } } } diff --git a/OpenKh.Bbs/Constants.cs b/OpenKh.Bbs/Constants.cs index 0ed5bed2f..3a9068a5a 100644 --- a/OpenKh.Bbs/Constants.cs +++ b/OpenKh.Bbs/Constants.cs @@ -12,6 +12,34 @@ public class Constants "20", "21", "22", "jf", }; + public static readonly string[] WorldNames = + { + "Common", + "Land of Departure", + "Dwarf Woodlands", + "Castle of Dreams", + "Enchanted Dominion", + "Mysterious Tower", + "Radiant Garden", + "Dark Workd", + "Olympus Coliseum", + "Deep Space", + "Destiny Island", + "Never Land", + "Disney Town", + "Keyblade Graveyard", + "14 - Unused", + "Mirage Arena", + "Command Board", + "World Map", + "WP - ???", + "19 - Unused", + "20 - Unused", + "21 - Unused", + "22 - Unused", + "JF - ??", + }; + public static readonly string[] Language = { "jp", "en", "fr", "it", "de", "es" diff --git a/OpenKh.Bbs/Event.cs b/OpenKh.Bbs/Event.cs new file mode 100644 index 000000000..5148e10c0 --- /dev/null +++ b/OpenKh.Bbs/Event.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xe.BinaryMapper; + +namespace OpenKh.Bbs +{ + public class Event + { + private const int MagicCode = 1; + + private class Header + { + [Data] public int MagicCode { get; set; } + [Data] public int Count { get => Items.TryGetCount(); set => Items = Items.CreateOrResize(value); } + [Data] public List Items { get; set; } + + static Header() + { + BinaryMapping.SetMemberLengthMapping
(nameof(Items), (o, m) => o.Count); + } + } + + [Data] public ushort Id { get; set; } + [Data] public ushort EventIndex { get; set; } + [Data] public byte World { get; set; } + [Data] public byte Room { get; set; } + [Data] public ushort Unknown06 { get; set; } + + public static bool IsValid(Stream stream) + { + var prevPosition = stream.Position; + var magicCode = new BinaryReader(stream).ReadInt32(); + stream.Position = prevPosition; + + return magicCode == MagicCode; + } + + public static List Read(Stream stream) => + BinaryMapping.ReadObject
(stream).Items; + + public static void Write(Stream stream, IEnumerable events) => + BinaryMapping.WriteObject(stream, new Header + { + MagicCode = MagicCode, + Items = events.ToList() + }); + } +} diff --git a/OpenKh.Bbs/OpenKh.Bbs.csproj b/OpenKh.Bbs/OpenKh.Bbs.csproj index b4d899581..8cd6f4cda 100644 --- a/OpenKh.Bbs/OpenKh.Bbs.csproj +++ b/OpenKh.Bbs/OpenKh.Bbs.csproj @@ -10,7 +10,7 @@ - + diff --git a/OpenKh.Command.Arc/OpenKh.Command.Arc.csproj b/OpenKh.Command.Arc/OpenKh.Command.Arc.csproj new file mode 100644 index 000000000..44ba152e5 --- /dev/null +++ b/OpenKh.Command.Arc/OpenKh.Command.Arc.csproj @@ -0,0 +1,18 @@ + + + + Exe + netcoreapp2.1 + 2.1.* + + + + + + + + + + + + diff --git a/OpenKh.Command.Arc/Program.cs b/OpenKh.Command.Arc/Program.cs new file mode 100644 index 000000000..42d129289 --- /dev/null +++ b/OpenKh.Command.Arc/Program.cs @@ -0,0 +1,133 @@ +using OpenKh.Common; +using McMaster.Extensions.CommandLineUtils; +using System; +using System.IO; +using System.Linq; + +namespace OpenKh.Command.Arc +{ + class Program + { + static void Main(string[] args) + { + try + { + CommandLineApplication.Execute(args); + } + catch (FileNotFoundException e) + { + Console.WriteLine($"The file {e.FileName} cannot be found. The program will now exit."); + } + catch (Exception e) + { + Console.WriteLine($"FATAL ERROR: {e.Message}\n{e.StackTrace}"); + } + } + + [Argument(0, "ARC file", "The ARC file to pack or unpack")] + public string FileName { get; } + + [Argument(1, "ARC directory", "The ARC directory used as destination for unpakcing or source for packing")] + public string DirectoryName { get; } + + [Option(ShortName = "p", LongName = "pack", Description = "Pack ARC")] + public bool Pack { get; } + + private void OnExecute() + { + try + { + if (Pack) + Repack(FileName, DirectoryName); + else + Unpack(FileName, DirectoryName); + } + catch (Exception ex) + { + Console.WriteLine($"ERROR: {ex.Message}"); + } + } + + private static void Unpack(string inputFile, string outputDirectory) + { + if (!File.Exists(inputFile)) + throw new FileNotFoundException("The specified file name cannot be found", inputFile); + + var entries = File.OpenRead(inputFile).Using(stream => + { + if (!Bbs.Arc.IsValid(stream)) + throw new InvalidDataException("The specified ARC file is not valid"); + + return Bbs.Arc.Read(stream); + }); + + if (string.IsNullOrEmpty(outputDirectory)) + outputDirectory = Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile)); + Directory.CreateDirectory(outputDirectory); + + foreach (var entry in entries.Where(x => !x.IsLink)) + { + Console.WriteLine(entry.Name); + + File.Create(Path.Combine(outputDirectory, entry.Name)).Using(stream => + { + stream.Write(entry.Data, 0, entry.Data.Length); + }); + } + + File.CreateText(Path.Combine(outputDirectory, "@ARC.txt")).Using(stream => + { + foreach (var entry in entries) + { + stream.WriteLine(entry.IsLink ? $"@{entry.Path}" : entry.Name); + } + }); + } + + private static void Repack(string outputFile, string inputDirectory) + { + if (!Directory.Exists(inputDirectory)) + throw new DirectoryNotFoundException("The specified input directory cannot be found"); + + var arcEntriesFileName = Path.Combine(inputDirectory, "@ARC.txt"); + if (!File.Exists(arcEntriesFileName)) + throw new FileNotFoundException("The @ARC.txt descriptor cannot be found in the specified directory", arcEntriesFileName); + + var entries = File.ReadAllLines(arcEntriesFileName) + .Select(x => GetEntry(inputDirectory, x)); + + File.Create(outputFile).Using(stream => Bbs.Arc.Write(entries, stream)); + + } + + private static Bbs.Arc.Entry GetEntry(string baseDirectory, string entryName) + { + if (entryName.FirstOrDefault() == '@') + { + var linkFileName = entryName.Substring(1); + var directoryName = Path.GetDirectoryName(linkFileName).Replace('\\', '/'); + var fileName = Path.GetFileName(linkFileName); + + var directoryPointer = Bbs.Bbsa.GetDirectoryHash(directoryName); + if (directoryPointer == uint.MaxValue) + throw new DirectoryNotFoundException($"The directory {directoryName} cannot be recognized by BBS engine."); + + return new Bbs.Arc.Entry + { + DirectoryPointer = directoryPointer, + Name = fileName + }; + } + else + { + var fileName = Path.Combine(baseDirectory, entryName); + return new Bbs.Arc.Entry + { + DirectoryPointer = 0, + Data = File.ReadAllBytes(fileName), + Name = entryName + }; + } + } + } +} diff --git a/OpenKh.Common/Exceptions/CharacterNotSupportedException.cs b/OpenKh.Common/Exceptions/CharacterNotSupportedException.cs new file mode 100644 index 000000000..266951b51 --- /dev/null +++ b/OpenKh.Common/Exceptions/CharacterNotSupportedException.cs @@ -0,0 +1,11 @@ +using System; + +namespace OpenKh.Common.Exceptions +{ + public class CharacterNotSupportedException : ArgumentException + { + public CharacterNotSupportedException(char ch) : + base($"The character {ch} it is not supported by the specified encoding.") + { } + } +} diff --git a/OpenKh.Common/Exceptions/InvalidFileException.cs b/OpenKh.Common/Exceptions/InvalidFileException.cs new file mode 100644 index 000000000..cd8ddff7a --- /dev/null +++ b/OpenKh.Common/Exceptions/InvalidFileException.cs @@ -0,0 +1,18 @@ +using System; + +namespace OpenKh.Common.Exceptions +{ + public class InvalidFileException : Exception + { + public InvalidFileException() : + base($"The specified file is not recognized as {typeof(T).Name}.") + { } + } + + public class InvalidFileException : Exception + { + public InvalidFileException() : + base("The specified file is not recognized.") + { } + } +} diff --git a/OpenKh.Common/Helpers.cs b/OpenKh.Common/Helpers.cs new file mode 100644 index 000000000..ac841807d --- /dev/null +++ b/OpenKh.Common/Helpers.cs @@ -0,0 +1,11 @@ +namespace OpenKh.Common +{ + public class Helpers + { + public static int Align(int offset, int alignment) + { + var misalignment = offset % alignment; + return misalignment > 0 ? offset + alignment - misalignment : offset; + } + } +} diff --git a/OpenKh.Common/OpenKh.Common.csproj b/OpenKh.Common/OpenKh.Common.csproj index be531a60b..a0e10233e 100644 --- a/OpenKh.Common/OpenKh.Common.csproj +++ b/OpenKh.Common/OpenKh.Common.csproj @@ -5,7 +5,7 @@ - + diff --git a/OpenKh.Common/Ps2/Dma.cs b/OpenKh.Common/Ps2/Dma.cs new file mode 100644 index 000000000..0d1d829eb --- /dev/null +++ b/OpenKh.Common/Ps2/Dma.cs @@ -0,0 +1,78 @@ +using Xe.BinaryMapper; + +namespace OpenKh.Common.Ps2 +{ + /// + /// EE User Manual, 6.3.2 + /// + public enum VifOpcode : byte + { + NOP = 0b00000000, + STCYCL = 0b00000001, + OFFSET = 0b00000010, + BASE = 0b00000011, + ITOP = 0b00000100, + STMOD = 0b00000101, + MSKPATH3 = 0b00000110, + MARK = 0b00000111, + FLUSHE = 0b00010000, + FLUSH = 0b00010001, + FLUSHA = 0b00010011, + MSCAL = 0b00010100, + MSCALF = 0b00010101, + MSCNT = 0b00010111, + STMASK = 0b00100000, + STROW = 0b00110000, + STCOL = 0b00110001, + MPG = 0b01001010, + DIRECT = 0b01010000, + DIRECTH = 0b01010001 + } + + /// + /// EE User Manual, 6.3.2 + /// + public class VifCode + { + [Data] public byte Cmd { get; set; } + [Data] public byte Num { get; set; } + [Data] public ushort Immediate { get; set; } + + public VifOpcode Opcode + { + get => (VifOpcode)(Cmd & 7); + set => Cmd = (byte)((byte)value | (Interrupt ? 0x80 : 0)); + } + + public bool Interrupt + { + get => (Cmd >> 7) != 0; + set => Cmd = (byte)((byte)Opcode | (value ? 0x80 : 0)); + } + } + + /// + /// EE User Manual, 5.6 + /// + public class DmaTag + { + /// + /// Quadword count; packet size + /// + [Data] public ushort Qwc { get; set; } + [Data] public ushort Param { get; set; } + [Data] public int Address { get; set; } + + public int TagId + { + get => (Param >> 12) & 7; + set => Param = (ushort)(((value & 7) << 12) | (Irq ? 0x8000 : 0)); + } + + public bool Irq + { + get => (Param >> 15) != 0; + set => Param = (ushort)((TagId << 12) | (value ? 0x8000 : 0)); + } + } +} diff --git a/OpenKh.Common/StreamExtensions.cs b/OpenKh.Common/StreamExtensions.cs index ad3d23194..a146d54f6 100644 --- a/OpenKh.Common/StreamExtensions.cs +++ b/OpenKh.Common/StreamExtensions.cs @@ -17,6 +17,9 @@ public static T SetPosition(this T stream, int position) where T : Stream return stream; } + public static T AlignPosition(this T stream, int alignValue) where T : Stream => + stream.SetPosition(Helpers.Align((int)stream.Position, alignValue)); + public static List ReadList(this Stream stream, int offset, int count) where T : class { @@ -32,12 +35,24 @@ public static List ReadList(this Stream stream, int count) .ToList(); } + public static short ReadInt16(this Stream stream) => + new BinaryReader(stream).ReadInt16(); + + public static ushort ReadUInt16(this Stream stream) => + new BinaryReader(stream).ReadUInt16(); + public static int ReadInt32(this Stream stream) => new BinaryReader(stream).ReadInt32(); public static uint ReadUInt32(this Stream stream) => new BinaryReader(stream).ReadUInt32(); + public static long ReadInt64(this Stream stream) => + new BinaryReader(stream).ReadInt64(); + + public static ulong ReadUInt64(this Stream stream) => + new BinaryReader(stream).ReadUInt64(); + public static List ReadInt32List(this Stream stream, int offset, int count) { stream.Position = offset; @@ -89,6 +104,9 @@ public static int WriteList(this Stream stream, IEnumerable items) return (int)stream.Position - oldPosition; } + public static void Write(this Stream stream, byte[] data) => + stream.Write(data, 0, data.Length); + public static int Write(this Stream stream, IEnumerable items) { var oldPosition = (int)stream.Position; @@ -99,6 +117,30 @@ public static int Write(this Stream stream, IEnumerable items) return (int)stream.Position - oldPosition; } + public static void Write(this Stream stream, byte value) => + new BinaryWriter(stream).Write(value); + + public static void Write(this Stream stream, char value) => + new BinaryWriter(stream).Write(value); + + public static void Write(this Stream stream, short value) => + new BinaryWriter(stream).Write(value); + + public static void Write(this Stream stream, ushort value) => + new BinaryWriter(stream).Write(value); + + public static void Write(this Stream stream, int value) => + new BinaryWriter(stream).Write(value); + + public static void Write(this Stream stream, uint value) => + new BinaryWriter(stream).Write(value); + + public static void Write(this Stream stream, long value) => + new BinaryWriter(stream).Write(value); + + public static void Write(this Stream stream, ulong value) => + new BinaryWriter(stream).Write(value); + public static void Copy(this Stream source, Stream destination, int length, int bufferSize = 65536) { int read; diff --git a/OpenKh.Imaging/GdiFormats.cs b/OpenKh.Imaging/GdiFormats.cs new file mode 100644 index 000000000..ee534d671 --- /dev/null +++ b/OpenKh.Imaging/GdiFormats.cs @@ -0,0 +1,80 @@ +using System.Drawing.Imaging; +using System.IO; + +namespace OpenKh.Imaging +{ + public static class Png + { + public static bool IsValid(Stream stream) + { + stream.Position = 0; + return stream.ReadByte() == 0x89 && + stream.ReadByte() == 0x50 && + stream.ReadByte() == 0x4e && + stream.ReadByte() == 0x47 && + stream.ReadByte() == 0x0d && + stream.ReadByte() == 0x0a && + stream.ReadByte() == 0x1a && + stream.ReadByte() == 0x0a; + } + + public static IImageRead Read(Stream stream) => new GdiImage(stream); + + public static void Write(Stream stream, IImageRead image) + { + using (var bitmap = image.CreateBitmap()) + { + stream.Position = 0; + bitmap.Save(stream, ImageFormat.Png); + } + } + } + + public static class Bmp + { + public static bool IsValid(Stream stream) + { + stream.Position = 0; + return stream.ReadByte() == 0x42 && + stream.ReadByte() == 0x4d; + } + + public static IImageRead Read(Stream stream) => new GdiImage(stream); + + public static void Write(Stream stream, IImageRead image) + { + using (var bitmap = image.CreateBitmap()) + { + stream.Position = 0; + bitmap.Save(stream, ImageFormat.Bmp); + } + } + } + + public static class Tiff + { + public static bool IsValid(Stream stream) + { + stream.Position = 0; + if (stream.ReadByte() == 0x49 && stream.ReadByte() == 0x49) + return true; + + stream.Position = 0; + if (stream.ReadByte() == 0x4d && stream.ReadByte() == 0x4d) + return true; + + return false; + } + + public static IImageRead Read(Stream stream) => new GdiImage(stream); + + public static void Write(Stream stream, IImageRead image) + { + using (var bitmap = image.CreateBitmap()) + { + stream.Position = 0; + bitmap.Save(stream, ImageFormat.Tiff); + } + } + } +} diff --git a/OpenKh.Imaging/GdiImage.cs b/OpenKh.Imaging/GdiImage.cs new file mode 100644 index 000000000..45d20a47a --- /dev/null +++ b/OpenKh.Imaging/GdiImage.cs @@ -0,0 +1,52 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; + +namespace OpenKh.Imaging +{ + internal class GdiImage : IImageRead + { + private readonly Bitmap _bitmap; + + public GdiImage(Stream stream) + { + stream.Position = 0; + _bitmap = new Bitmap(stream); + } + + public Size Size => _bitmap.Size; + + public PixelFormat PixelFormat => _bitmap.PixelFormat.GetPixelFormat(); + + public byte[] GetClut() + { + var palette = _bitmap.Palette?.Entries ?? new Color[0]; + var clut = new byte[palette.Length * 4]; + + for (var i = 0; i < palette.Length; i++) + { + var color = palette[i]; + clut[i * 4 + 0] = color.R; + clut[i * 4 + 1] = color.G; + clut[i * 4 + 2] = color.B; + clut[i * 4 + 3] = color.A; + } + + return clut; + } + + public byte[] GetData() + { + var rect = new Rectangle(0, 0, _bitmap.Width, _bitmap.Height); + var bitmapData = _bitmap.LockBits(rect, ImageLockMode.ReadOnly, _bitmap.PixelFormat); + + var dstData = new byte[bitmapData.Stride * bitmapData.Height]; + Marshal.Copy(bitmapData.Scan0, dstData, 0, dstData.Length); + + _bitmap.UnlockBits(bitmapData); + + return dstData; + } + } +} diff --git a/OpenKh.Imaging/ImageDataHelpers.cs b/OpenKh.Imaging/ImageDataHelpers.cs new file mode 100644 index 000000000..fc87504e4 --- /dev/null +++ b/OpenKh.Imaging/ImageDataHelpers.cs @@ -0,0 +1,115 @@ +using System; +using System.Drawing; + +namespace OpenKh.Imaging +{ + public static class ImageDataHelpers + { + public static byte[] FromIndexed8ToBgra(byte[] data, byte[] clut) + { + var bitmap = new byte[data.Length * 4]; + for (int i = 0; i < data.Length; i++) + { + var clutIndex = data[i]; + bitmap[i * 4 + 0] = clut[clutIndex * 4 + 2]; + bitmap[i * 4 + 1] = clut[clutIndex * 4 + 1]; + bitmap[i * 4 + 2] = clut[clutIndex * 4 + 0]; + bitmap[i * 4 + 3] = clut[clutIndex * 4 + 3]; + } + return bitmap; + } + + public static byte[] FromIndexed4ToBgra(byte[] data, byte[] clut) + { + var bitmap = new byte[data.Length * 8]; + for (int i = 0; i < data.Length; i++) + { + var subData = data[i]; + var clutIndex1 = subData >> 4; + var clutIndex2 = subData & 0x0F; + bitmap[i * 8 + 0] = clut[clutIndex1 * 4 + 2]; + bitmap[i * 8 + 1] = clut[clutIndex1 * 4 + 1]; + bitmap[i * 8 + 2] = clut[clutIndex1 * 4 + 0]; + bitmap[i * 8 + 3] = clut[clutIndex1 * 4 + 3]; + bitmap[i * 8 + 4] = clut[clutIndex2 * 4 + 2]; + bitmap[i * 8 + 5] = clut[clutIndex2 * 4 + 1]; + bitmap[i * 8 + 6] = clut[clutIndex2 * 4 + 0]; + bitmap[i * 8 + 7] = clut[clutIndex2 * 4 + 3]; + } + return bitmap; + } + + public static void InvertRedBlueChannels(byte[] data, Size size, PixelFormat pixelFormat) + { + var length = size.Width * size.Height; + switch (pixelFormat) + { + case PixelFormat.Rgb888: + for (var i = 0; i < length; i++) + { + byte tmp = data[i * 3 + 0]; + data[i * 3 + 0] = data[i * 3 + 2]; + data[i * 3 + 2] = tmp; + } + break; + case PixelFormat.Rgba8888: + for (int i = 0; i < length; i++) + { + byte tmp = data[i * 4 + 0]; + data[i * 4 + 0] = data[i * 4 + 2]; + data[i * 4 + 2] = tmp; + } + break; + case PixelFormat.Indexed8: + break; + case PixelFormat.Indexed4: + for (var i = 0; i < length / 2; i++) + { + data[i] = (byte)(((data[i] & 0x0F) << 4) | (data[i] >> 4)); + } + break; + default: + throw new ArgumentOutOfRangeException($"The format {pixelFormat} is invalid or not supported."); + } + } + + public static byte[] GetInvertedRedBlueChannels(byte[] data, Size size, PixelFormat pixelFormat) + { + var length = size.Width * size.Height; + var dst = new byte[data.Length]; + + switch (pixelFormat) + { + case PixelFormat.Rgb888: + for (var i = 0; i < length; i++) + { + dst[i * 3 + 0] = data[i * 3 + 2]; + dst[i * 3 + 1] = data[i * 3 + 1]; + dst[i * 3 + 2] = data[i * 3 + 0]; + } + break; + case PixelFormat.Rgba8888: + for (var i = 0; i < length; i++) + { + dst[i * 4 + 0] = data[i * 4 + 2]; + dst[i * 4 + 1] = data[i * 4 + 1]; + dst[i * 4 + 2] = data[i * 4 + 0]; + dst[i * 4 + 3] = data[i * 4 + 3]; + } + break; + case PixelFormat.Indexed8: + return data; + case PixelFormat.Indexed4: + for (var i = 0; i < length / 2; i++) + { + dst[i] = (byte)(((data[i] & 0x0F) << 4) | (data[i] >> 4)); + } + break; + default: + throw new ArgumentOutOfRangeException($"The format {pixelFormat} is invalid or not supported."); + } + + return dst; + } + } +} diff --git a/OpenKh.Imaging/ImageExtensions.cs b/OpenKh.Imaging/ImageExtensions.cs new file mode 100644 index 000000000..ad453354f --- /dev/null +++ b/OpenKh.Imaging/ImageExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; + +namespace OpenKh.Imaging +{ + public static class ImageExtensions + { + public static byte[] ToBgra32(this IImageRead imageRead) + { + switch (imageRead.PixelFormat) + { + case PixelFormat.Indexed4: + return ImageDataHelpers.FromIndexed4ToBgra(imageRead.GetData(), imageRead.GetClut()); + case PixelFormat.Indexed8: + return ImageDataHelpers.FromIndexed8ToBgra(imageRead.GetData(), imageRead.GetClut()); + case PixelFormat.Rgba8888: + return imageRead.GetData(); + default: + throw new NotImplementedException($"The PixelFormat {imageRead.PixelFormat} cannot be converted to a Bgra32."); + } + } + + public static void SaveImage(this IImageRead imageRead, string fileName) + { + using (var gdiBitmap = imageRead.CreateBitmap()) + { + gdiBitmap.Save(fileName); + } + } + + internal static Bitmap CreateBitmap(this IImageRead imageRead) + { + var drawingPixelFormat = imageRead.PixelFormat.GetDrawingPixelFormat(); + Bitmap bitmap = new Bitmap(imageRead.Size.Width, imageRead.Size.Height, drawingPixelFormat); + + var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); + var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, drawingPixelFormat); + + var srcData = imageRead.GetData(); + Marshal.Copy(srcData, 0, bitmapData.Scan0, srcData.Length); + + bitmap.UnlockBits(bitmapData); + + var isIndexed = imageRead.PixelFormat.IsIndexed(); + if (isIndexed) + { + var palette = bitmap.Palette; + var clut = imageRead.GetClut(); + var colorsCount = Math.Min(clut.Length / 4, palette.Entries.Length); + + for (var i = 0; i < colorsCount; i++) + { + palette.Entries[i] = Color.FromArgb( + clut[i * 4 + 3], + clut[i * 4 + 0], + clut[i * 4 + 1], + clut[i * 4 + 2]); + } + + bitmap.Palette = palette; + } + + return bitmap; + } + } +} diff --git a/OpenKh.Imaging/ImageHelpers.cs b/OpenKh.Imaging/ImageHelpers.cs deleted file mode 100644 index f2f92d5f2..000000000 --- a/OpenKh.Imaging/ImageHelpers.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Drawing; -using System.Drawing.Imaging; -using System.Runtime.InteropServices; - -namespace OpenKh.Imaging -{ - public static class ImageHelpers - { - public static byte[] ToBgra32(this IImageRead imageRead) - { - switch (imageRead.PixelFormat) - { - case PixelFormat.Indexed4: - return FromIndexed4ToBgra(imageRead.GetData(), imageRead.GetClut()); - case PixelFormat.Indexed8: - return FromIndexed8ToBgra(imageRead.GetData(), imageRead.GetClut()); - case PixelFormat.Rgba8888: - return imageRead.GetData(); - default: - throw new NotImplementedException($"The PixelFormat {imageRead.PixelFormat} cannot be converted to a Bgra32."); - } - } - - public static byte[] FromIndexed8ToBgra(byte[] data, byte[] clut) - { - var bitmap = new byte[data.Length * 4]; - for (int i = 0; i < data.Length; i++) - { - var clutIndex = data[i]; - bitmap[i * 4 + 0] = clut[clutIndex * 4 + 2]; - bitmap[i * 4 + 1] = clut[clutIndex * 4 + 1]; - bitmap[i * 4 + 2] = clut[clutIndex * 4 + 0]; - bitmap[i * 4 + 3] = clut[clutIndex * 4 + 3]; - } - return bitmap; - } - - public static byte[] FromIndexed4ToBgra(byte[] data, byte[] clut) - { - var bitmap = new byte[data.Length * 8]; - for (int i = 0; i < data.Length; i++) - { - var subData = data[i]; - var clutIndex1 = subData >> 4; - var clutIndex2 = subData & 0x0F; - bitmap[i * 8 + 0] = clut[clutIndex1 * 4 + 2]; - bitmap[i * 8 + 1] = clut[clutIndex1 * 4 + 1]; - bitmap[i * 8 + 2] = clut[clutIndex1 * 4 + 0]; - bitmap[i * 8 + 3] = clut[clutIndex1 * 4 + 3]; - bitmap[i * 8 + 4] = clut[clutIndex2 * 4 + 2]; - bitmap[i * 8 + 5] = clut[clutIndex2 * 4 + 1]; - bitmap[i * 8 + 6] = clut[clutIndex2 * 4 + 0]; - bitmap[i * 8 + 7] = clut[clutIndex2 * 4 + 3]; - } - return bitmap; - } - - public static void SaveImage(this IImageRead imageRead, string fileName) - { - using (var gdiBitmap = imageRead.CreateBitmap()) - { - gdiBitmap.Save(fileName); - } - } - - private static Bitmap CreateBitmap(this IImageRead imageRead) - { - var drawingPixelFormat = imageRead.PixelFormat.GetDrawingPixelFormat(); - Bitmap bitmap = new Bitmap(imageRead.Size.Width, imageRead.Size.Height, drawingPixelFormat); - - var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); - var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, drawingPixelFormat); - - var srcData = imageRead.GetData(); - Marshal.Copy(srcData, 0, bitmapData.Scan0, srcData.Length); - - bitmap.UnlockBits(bitmapData); - - var isIndexed = imageRead.PixelFormat.IsIndexed(); - if (isIndexed) - { - var palette = bitmap.Palette; - var clut = imageRead.GetClut(); - var colorsCount = Math.Min(clut.Length / 4, palette.Entries.Length); - - for (var i = 0; i < colorsCount; i++) - { - palette.Entries[i] = Color.FromArgb( - clut[i * 4 + 3], - clut[i * 4 + 0], - clut[i * 4 + 1], - clut[i * 4 + 2]); - } - - bitmap.Palette = palette; - } - - return bitmap; - } - - private static bool IsIndexed(this PixelFormat pixelFormat) - { - switch (pixelFormat) - { - case PixelFormat.Indexed4: - case PixelFormat.Indexed8: - return true; - default: - return false; - } - } - - private static System.Drawing.Imaging.PixelFormat GetDrawingPixelFormat(this PixelFormat pixelFormat) - { - switch (pixelFormat) - { - case PixelFormat.Indexed4: - return System.Drawing.Imaging.PixelFormat.Format4bppIndexed; - case PixelFormat.Indexed8: - return System.Drawing.Imaging.PixelFormat.Format8bppIndexed; - case PixelFormat.Rgba8888: - return System.Drawing.Imaging.PixelFormat.Format32bppArgb; - default: - throw new NotImplementedException( - $"The reading from pixel format {pixelFormat} is not implemented."); - } - } - } -} diff --git a/OpenKh.Imaging/OpenKh.Imaging.csproj b/OpenKh.Imaging/OpenKh.Imaging.csproj index 1199c4581..cadc36b87 100644 --- a/OpenKh.Imaging/OpenKh.Imaging.csproj +++ b/OpenKh.Imaging/OpenKh.Imaging.csproj @@ -8,4 +8,8 @@ + + + + diff --git a/OpenKh.Imaging/PixelFormatExtensions.cs b/OpenKh.Imaging/PixelFormatExtensions.cs new file mode 100644 index 000000000..ea1570fca --- /dev/null +++ b/OpenKh.Imaging/PixelFormatExtensions.cs @@ -0,0 +1,63 @@ +using System; + +namespace OpenKh.Imaging +{ + public static class PixelFormatExtensions + { + public static bool IsIndexed(this PixelFormat pixelFormat) + { + switch (pixelFormat) + { + case PixelFormat.Indexed4: + case PixelFormat.Indexed8: + return true; + default: + return false; + } + } + + internal static System.Drawing.Imaging.PixelFormat GetDrawingPixelFormat(this PixelFormat pixelFormat) + { + switch (pixelFormat) + { + case PixelFormat.Indexed4: + return System.Drawing.Imaging.PixelFormat.Format4bppIndexed; + case PixelFormat.Indexed8: + return System.Drawing.Imaging.PixelFormat.Format8bppIndexed; + case PixelFormat.Rgba1555: + return System.Drawing.Imaging.PixelFormat.Format32bppArgb; + case PixelFormat.Rgb888: + return System.Drawing.Imaging.PixelFormat.Format24bppRgb; + case PixelFormat.Rgbx8888: + return System.Drawing.Imaging.PixelFormat.Format32bppRgb; + case PixelFormat.Rgba8888: + return System.Drawing.Imaging.PixelFormat.Format32bppArgb; + default: + throw new NotImplementedException( + $"The pixel format {pixelFormat} is not implemented."); + } + } + + internal static PixelFormat GetPixelFormat(this System.Drawing.Imaging.PixelFormat pixelFormat) + { + switch (pixelFormat) + { + case System.Drawing.Imaging.PixelFormat.Format4bppIndexed: + return PixelFormat.Indexed4; + case System.Drawing.Imaging.PixelFormat.Format8bppIndexed: + return PixelFormat.Indexed8; + case System.Drawing.Imaging.PixelFormat.Format24bppRgb: + return PixelFormat.Rgb888; + case System.Drawing.Imaging.PixelFormat.Format16bppArgb1555: + return PixelFormat.Rgba1555; + case System.Drawing.Imaging.PixelFormat.Format32bppArgb: + return PixelFormat.Rgba8888; + case System.Drawing.Imaging.PixelFormat.Format32bppRgb: + return PixelFormat.Rgbx8888; + default: + throw new NotImplementedException( + $"The pixel format {pixelFormat} is not implemented."); + } + } + } +} diff --git a/OpenKh.Imaging/Tm2.cs b/OpenKh.Imaging/Tm2.cs new file mode 100644 index 000000000..8dcb5455f --- /dev/null +++ b/OpenKh.Imaging/Tm2.cs @@ -0,0 +1,524 @@ +using OpenKh.Common; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using Xe.BinaryMapper; + +namespace OpenKh.Imaging +{ + public class Tm2 : IImageRead + { + private const uint MagicCode = 0x324D4954U; + private const int Version = 4; + private const int Format = 0; + private const int HeaderLength = 16; + + /// + /// Pixel Storage Mode, or PSM + /// Defines how pixel are arranged in each 32-bit word of local memory. + /// + public enum GsPSM + { + /// + /// RGBA32, uses 32-bit per pixel. + /// + GS_PSMCT32 = 0, + + /// + /// RGB24, uses 24-bit per pixel with the upper 8 bit unused. + /// + GS_PSMCT24 = 1, + + /// + /// RGBA16, pack two pixels in 32-bit in little endian order. + /// + GS_PSMCT16 = 2, + + /// + /// RGBA16, pack two pixels in 32-bit in little endian order. + /// + GS_PSMCT16S = 10, + + /// + /// 8-bit indexed, packing 4 pixels per 32-bit. + /// + GS_PSMT8 = 19, + + /// + /// 4-bit indexed, packing 8 pixels per 32-bit. + /// + GS_PSMT4 = 20, + + /// + /// 8-bit indexed, but the upper 24-bit are unused. + /// + GS_PSMT8H = 27, + + /// + /// 4-bit indexed, but the upper 24-bit are unused. + /// + GS_PSMT4HL = 36, + + /// + /// 4-bit indexed, where the bits 4-7 are evaluated and the rest discarded. + /// + GS_PSMT4HH = 44, + + /// + /// 32-bit Z buffer + /// + GS_PSMZ32 = 48, + + /// + /// 24-bit Z buffer with the upper 8-bit unused + /// + GS_PSMZ24 = 49, + + /// + /// 16-bit Z buffer, pack two pixels in 32-bit in little endian order. + /// + GS_PSMZ16 = 50, + + /// + /// 16-bit Z buffer, pack two pixels in 32-bit in little endian order. + /// + GS_PSMZ16S = 58, + }; + + public enum GsCPSM + { + GS_PSMCT32 = 0, // 32bit RGBA + GS_PSMCT24 = 1, + GS_PSMCT16 = 2, + GS_PSMCT16S = 10, + } + + private enum IMG_TYPE + { + IT_RGBA = 3, + IT_CLUT4 = 4, + IT_CLUT8 = 5, + }; + + private enum CLT_TYPE + { + CT_A1BGR5 = 1, + CT_XBGR8 = 2, + CT_ABGR8 = 3, + }; + + /// + /// register for image + /// 14 bit, texture buffer base pointer (address / 256) + /// 6 bit, texture buffer width (texels / 64) + /// 6 bit, pixel storage format (0 = 32bit RGBA) + /// 4 bit, width 2^n + /// 4 bit, height 2^n + /// 1 bit, 0 = RGB, 1 = RGBA + /// 2 bit, texture function (0=modulate, 1=decal, 2=hilight, 3=hilight2) + /// 14 bit, CLUT buffer base pointer (address / 256) + /// 4 bit, CLUT storage format + /// 1 bit, storage mode + /// 5 bit, offset + /// 3 bit, load control + /// + /// http://forum.xentax.com/viewtopic.php?f=16&t=4501&start=75 + /// + public class GsTex + { + public GsTex() + { + + } + + public GsTex(GsTex gsTex) + { + Data = gsTex.Data; + } + + public GsTex(GsTex gsTex, int width, int height) + { + Data = gsTex.Data; + TW = GetSizeRegister(width); + TH = GetSizeRegister(height); + } + + [Data] public long Data { get; set; } + + /// + /// Texture Base Pointer. + /// + public int TBP0 + { + get => GetBits(Data, 0, 14); + set => Data = SetBits(Data, 0, 14, value); + } + + /// + /// Texture Buffer Width. + /// + public int TBW + { + get => GetBits(Data, 14, 6); + set => Data = SetBits(Data, 14, 6, value); + } + + /// + /// Pixel Storage Mode. + /// Tells what is the format used to store the individual pixels. + /// + public GsPSM PSM + { + get => (GsPSM)GetBits(Data, 20, 6); + set => Data = SetBits(Data, 20, 6, (int)value); + } + + /// + /// Texture Width; power of 2. + /// + public int TW + { + get => GetBits(Data, 26, 4); + set => SetBits(Data, 26, 4, value); + } + + /// + /// Texture Height; power of 2 + /// + public int TH + { + get => GetBits(Data, 30, 4); + set => SetBits(Data, 30, 4, value); + } + + public bool TCC + { + get => GetBit(Data, 34); + set => Data = SetBit(Data, 34, value); + + } + + /// + /// Texture Function + /// + public int TFX + { + get => GetBits(Data, 35, 2); + set => Data = SetBits(Data, 35, 2, value); + } + + /// + /// Clut Base Pointer + /// + public int CBP + { + get => GetBits(Data, 37, 14); + set => Data = SetBits(Data, 37, 14, value); + } + + /// + /// Clut Pixel Storage mode + /// + public GsCPSM CPSM + { + get => (GsCPSM)GetBits(Data, 51, 4); + set => Data = SetBits(Data, 51, 4, (int)value); + } + + + /// + /// Clut storage mode field + /// false: store CLUT using CSM1, which swizzled the data every 8 colours. + /// true: store CLUT using CSM2, which is linear but slower. + /// + public bool CSM + { + get => GetBit(Data, 55); + set => Data = SetBit(Data, 55, value); + } + + /// + /// Clut Entry Offset + /// + public int CSA + { + get => GetBits(Data, 56, 5); + set => Data = SetBits(Data, 56, 5, value); + } + + /// + /// Clut Buffer Load Control + /// + public int CLD + { + get => GetBits(Data, 61, 3); + set => Data = SetBits(Data, 61, 3, value); + } + } + + private class Header + { + [Data] public uint MagicCode { get; set; } + [Data] public byte Version { get; set; } + [Data] public byte Format { get; set; } + [Data] public short ImageCount { get; set; } + [Data] public long Zero { get; set; } + } + + private class Picture + { + [Data] public int TotalSize { get; set; } + [Data] public int ClutSize { get; set; } + [Data] public int ImageSize { get; set; } + [Data] public short HeaderSize { get; set; } + [Data] public short ClutColorCount { get; set; } + [Data] public byte PictureFormat { get; set; } + [Data] public byte MipMapCount { get; set; } + [Data] public byte ClutType { get; set; } + [Data] public byte ImageType { get; set; } + [Data] public short Width { get; set; } + [Data] public short Height { get; set; } + [Data] public GsTex GsTex0 { get; set; } + [Data] public GsTex GsTex1 { get; set; } + [Data] public int GsRegs { get; set; } + [Data] public int GsClut { get; set; } + }; + + private class MipMap + { + [Data] public int GsMiptbp1_1 { get; set; } + [Data] public int GsMiptbp1_2 { get; set; } + [Data] public int GsMiptbp2_1 { get; set; } + [Data] public int GsMiptbp2_2 { get; set; } + [Data(Count = 8)] public int[] Sizes { get; set; } + } + + private readonly byte _imageFormat; + private readonly byte _mipMapCount; + private readonly byte _imageType; + private readonly byte _clutType; + private readonly GsTex _gsTex0; + private readonly GsTex _gsTex1; + private readonly int _gsReg; + private readonly int _gsPal; + private readonly byte[] _imageData; + private readonly byte[] _clutData; + private readonly MipMap _mipmap; + private bool IsClutSwizzled => (_clutType & 0x80) == 0; + + public Size Size { get; } + public PixelFormat PixelFormat => GetPixelFormat(_imageType); + public PixelFormat ClutFormat => GetPixelFormat(_clutType & 7); + + private Tm2(Stream stream, Picture picture) + { + _imageFormat = picture.PictureFormat; + _mipMapCount = picture.MipMapCount; + _imageType = picture.ImageType; + _clutType = picture.ClutType; + _gsTex0 = picture.GsTex0; + _gsTex1 = picture.GsTex1; + _gsReg = picture.GsRegs; + _gsPal = picture.GsClut; + Size = new Size(picture.Width, picture.Height); + + if (picture.MipMapCount > 1) + { + _mipmap = BinaryMapping.ReadObject(stream); + throw new NotImplementedException("Mipmaps are not currently supported."); + } + + _imageData = stream.ReadBytes(picture.ImageSize); + _clutData = stream.ReadBytes(picture.ClutSize); + if (IsClutSwizzled) + _clutData = SortClut(_clutData, ClutFormat, picture.ClutColorCount); + + ImageDataHelpers.InvertRedBlueChannels(_imageData, Size, PixelFormat); + } + + public static bool IsValid(Stream stream) => + stream.SetPosition(0).ReadInt32() == MagicCode && + stream.Length >= HeaderLength; + + public static IEnumerable Read(Stream stream) + { + if (!stream.CanRead || !stream.CanSeek) + throw new InvalidDataException($"Read or seek must be supported."); + + var header = BinaryMapping.ReadObject
(stream.SetPosition(0)); + if (header.Format != 0) + stream.Position = 128; + + if (stream.Length < HeaderLength || header.MagicCode != MagicCode) + throw new InvalidDataException("Invalid header"); + + return Enumerable.Range(0, header.ImageCount) + .Select(x => new Tm2(stream, BinaryMapping.ReadObject(stream))) + .ToArray(); + } + + public static void Write(Stream stream, IEnumerable images) + { + if (!stream.CanWrite || !stream.CanSeek) + throw new InvalidDataException($"Write or seek must be supported."); + + var myImages = images.ToArray(); + BinaryMapping.WriteObject(stream, new Header + { + MagicCode = MagicCode, + Version = Version, + Format = Format, + ImageCount = (short)myImages.Length, + Zero = 0, + }); + + foreach (var image in myImages) + { + var colorCount = image._clutData.Length > 0 ? image._clutData.Length * 8 / GetBitsPerPixel(image._clutType) : 0; + + BinaryMapping.WriteObject(stream, new Picture + { + TotalSize = 0x30 + image._imageData.Length + image._clutData.Length, + ClutSize = image._clutData.Length, + ImageSize = image._imageData.Length, + HeaderSize = 0x30, + ClutColorCount = (short)colorCount, + PictureFormat = image._imageFormat, + MipMapCount = image._mipMapCount, + ClutType = image._clutType, + ImageType = image._imageType, + Width = (short)image.Size.Width, + Height = (short)image.Size.Height, + GsTex0 = new GsTex(image._gsTex0, image.Size.Width, image.Size.Height), + GsTex1 = new GsTex(image._gsTex1), + GsRegs = image._gsReg, + GsClut = image._gsPal, + }); + + var data = ImageDataHelpers.GetInvertedRedBlueChannels(image._imageData, image.Size, image.PixelFormat); + stream.Write(data, 0, image._imageData.Length); + + var clut = image.IsClutSwizzled ? SortClut(image._clutData, image.ClutFormat, colorCount) : image._clutData; + stream.Write(clut, 0, image._clutData.Length); + } + } + + public byte[] GetData() => _imageData; + public byte[] GetClut() => _clutData; + + private static int GetBitsPerPixel(int format) + { + switch (format) + { + case 0: return 0; + case 1: return 16; + case 2: return 24; + case 3: return 32; + case 4: return 4; + case 5: return 8; + default: + throw new ArgumentOutOfRangeException($"The format ID {format} is invalid or not supported."); + } + } + + private static int GetSizeRegister(int realSize) => (int)Math.Ceiling(Math.Log(realSize, 2)); + + private static int GetBits(long Data, int position, int size) + { + var mask = (1 << size) - 1; + return (int)((Data >> position) & mask); + } + + private static long SetBits(long Data, int position, int size, int value) + { + var mask = (1 << size) - 1U; + return Data & ~(mask << position) | ((value & mask) << position); + } + + private static bool GetBit(long Data, int position) => GetBits(Data, position, 1) != 0; + private static long SetBit(long Data, int position, bool value) => SetBits(Data, position, 1, value ? 1 : 0); + + private static PixelFormat GetPixelFormat(int format) + { + switch (format) + { + case 0: return PixelFormat.Undefined; + case 1: return PixelFormat.Rgba1555; + case 2: return PixelFormat.Rgb888; + case 3: return PixelFormat.Rgba8888; + case 4: return PixelFormat.Indexed4; + case 5: return PixelFormat.Indexed8; + default: + throw new ArgumentOutOfRangeException($"The format ID {format} is invalid or not supported."); + } + } + + private static byte[] SortClut(byte[] clut, PixelFormat format, int colorCount) + { + if (colorCount != 256) + return clut; + + var index = 0; + var dst = ToIntArray(clut); + switch (format) + { + case PixelFormat.Rgba1555: + for (int i = 0; i < 8; i++) + { + for (int j = 0; j < 4; j++) + { + int tmp = dst[index + 4 + j]; + dst[index + 4 + j] = dst[index + 8 + j]; + dst[index + 8 + j] = tmp; + } + index += 16; + } + break; + case PixelFormat.Rgba8888: + for (int i = 0; i < 8; i++) + { + for (int j = 0; j < 8; j++) + { + int tmp = dst[index + 8 + j]; + dst[index + 8 + j] = dst[index + 16 + j]; + dst[index + 16 + j] = tmp; + } + index += 32; + } + break; + } + + return ToByteArray(dst); + } + + private static int[] ToIntArray(byte[] a) + { + var b = new int[a.Length / 4]; + for (var i = 0; i < b.Length; i++) + { + b[i] = a[i * 4 + 0] | + (a[i * 4 + 1] << 8) | + (a[i * 4 + 2] << 16) | + (a[i * 4 + 3] << 24); + } + + return b; + } + + private static byte[] ToByteArray(int[] src) + { + var dst = new byte[src.Length * 4]; + for (var i = 0; i < src.Length; i++) + { + dst[i * 4 + 0] = (byte)(src[i] >> 0); + dst[i * 4 + 1] = (byte)(src[i] >> 8); + dst[i * 4 + 2] = (byte)(src[i] >> 16); + dst[i * 4 + 3] = (byte)(src[i] >> 24); + } + + return dst; + } + } +} diff --git a/OpenKh.Kh2/Bar.cs b/OpenKh.Kh2/Bar.cs index d6e4ecf03..293d5fd14 100644 --- a/OpenKh.Kh2/Bar.cs +++ b/OpenKh.Kh2/Bar.cs @@ -228,13 +228,7 @@ private static int Align(int offset, Entry entry) alignment = magicCode == MagicCode ? 0x80 : 4; } - return Align(offset, alignment); - } - - private static int Align(int offset, int alignment) - { - var misalignment = offset % alignment; - return misalignment > 0 ? offset + alignment - misalignment : offset; + return Helpers.Align(offset, alignment); } public static bool IsValid(Stream stream) => diff --git a/OpenKh.Kh2/BaseTable.cs b/OpenKh.Kh2/BaseTable.cs new file mode 100644 index 000000000..7452cc0fa --- /dev/null +++ b/OpenKh.Kh2/BaseTable.cs @@ -0,0 +1,31 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Xe.BinaryMapper; + +namespace OpenKh.Kh2 +{ + public class BaseTable : IEnumerable + { + [Data] public int Id { get; set; } + [Data] public int Count { get => Items.TryGetCount(); set => Items = Items.CreateOrResize(value); } + [Data] public List Items { get; set; } + + static BaseTable() => BinaryMapping.SetMemberLengthMapping>(nameof(Items), (o, m) => o.Count); + + public static BaseTable Read(Stream stream) => BinaryMapping.ReadObject>(stream); + public static void Write(Stream stream, int id, List items) => + new BaseTable() + { + Id = id, + Items = items + }.Write(stream); + + + public void Write(Stream stream) => BinaryMapping.WriteObject(stream, this); + + public IEnumerator GetEnumerator() => Items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => Items.GetEnumerator(); + } +} \ No newline at end of file diff --git a/OpenKh.Kh2/Constants.cs b/OpenKh.Kh2/Constants.cs index 08e010a09..96d6792ae 100644 --- a/OpenKh.Kh2/Constants.cs +++ b/OpenKh.Kh2/Constants.cs @@ -1,21 +1,79 @@ namespace OpenKh.Kh2 { + public enum World + { + WorldZz, + EndOfSea, + TwilightTown, + DestinyIsland, + HollowBastion, + BeastCastle, + TheUnderworld, + Agrabah, + LandOfDragons, + HundredAcreWood, + PrideLands, + Atlantica, + DisneyCastle, + TimelessRiver, + HalloweenTown, + WorldMap, + PortRoyal, + SpaceParanoids, + WorldThatNeverWas + } + public static class Constants { public const int FontEuropeanSystemWidth = 18; public const int FontEuropeanSystemHeight = 24; public const int FontEuropeanEventWidth = 24; public const int FontEuropeanEventHeight = 32; + public const int FontJapaneseSystemWidth = 18; + public const int FontJapaneseSystemHeight = 18; + public const int FontJapaneseEventWidth = 24; + public const int FontJapaneseEventHeight = 24; + public const int FontTableSystemHeight = 256; + public const int FontTableEventHeight = 512; public const int FontIconWidth = 24; public const int FontIconHeight = 24; - public const int WorldCount = 19; + public const int PaletteCount = 9; + public const int WorldCount = (int)World.WorldThatNeverWas + 1; - public static readonly string[] Worlds = new string[WorldCount] + public static readonly string[] WorldIds = new string[WorldCount] { "zz", "es", "tt", "di", "hb", "bb", "he", "al", "mu", "po", "lk", "lm", "dc", "wi", "nm", "wm", "ca", "tr", "eh" }; + + public static readonly string[] Languages = new string[] + { + "jp", "us", "it", "sp", "fr", "gr", + }; + + public static readonly string[] WorldNames = new string[WorldCount] + { + "World ZZ", + "End of Sea", + "Twilight Town", + "Destiny Islands", + "Hollow Bastion", + "Beast's Castle", + "Olympus Coliseum", + "Agrabah", + "The Land of Dragons", + "100 Acre Wood", + "Pride Lands", + "Atlantica", + "Disney Castle", + "Timeless River", + "Halloween Town", + "World Map", + "Port Royal", + "Space Paranoids", + "World That Never Was" + }; } } diff --git a/OpenKh.Kh2/Extensions/ImageReadExtensions.cs b/OpenKh.Kh2/Extensions/ImageReadExtensions.cs new file mode 100644 index 000000000..4a35ca011 --- /dev/null +++ b/OpenKh.Kh2/Extensions/ImageReadExtensions.cs @@ -0,0 +1,10 @@ +using OpenKh.Imaging; + +namespace OpenKh.Kh2.Extensions +{ + public static class ImageReadExtensions + { + public static Imgd AsImgd(this IImageRead image, bool isSwizzled = false) => + new Imgd(image.Size, image.PixelFormat, image.GetData(), image.GetClut(), isSwizzled); + } +} diff --git a/OpenKh.Kh2/Idx.cs b/OpenKh.Kh2/Idx.cs index c1c1dcc2e..5c7408c9b 100644 --- a/OpenKh.Kh2/Idx.cs +++ b/OpenKh.Kh2/Idx.cs @@ -1,4 +1,5 @@ -using System; +using OpenKh.Common; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -98,6 +99,9 @@ [Data] public int Length [Data] public List Items { get; set; } + public static bool IsValid(Stream stream) => + stream.Length == new BinaryReader(stream.SetPosition(0)).ReadInt32() * 0x10 + 4; + /// /// Deserialize an IDX from a stream /// @@ -106,14 +110,14 @@ [Data] public int Length public static Idx Read(Stream stream) { BinaryMapping.SetMemberLengthMapping(nameof(Items), (o, m) => o.Length); - return BinaryMapping.ReadObject(stream); + return BinaryMapping.ReadObject(stream.SetPosition(0)); } /// /// Serialize an IDX to a stream /// /// Writable stream that will contain the IDX data - public void Write(Stream stream) => BinaryMapping.WriteObject(stream, this); + public void Write(Stream stream) => BinaryMapping.WriteObject(stream.SetPosition(0), this); /// /// Try to get an entry form a file name diff --git a/OpenKh.Kh2/Img.cs b/OpenKh.Kh2/Img.cs index 3d541e92a..365e15d3d 100644 --- a/OpenKh.Kh2/Img.cs +++ b/OpenKh.Kh2/Img.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using Xe.IO; @@ -43,6 +44,14 @@ public Img(Stream stream, Idx idx, bool loadAllIdx) public Idx Idx { get; } + public bool FileOpen(string fileName, Action callback) + { + bool result; + if (result = Idx.TryGetEntry(fileName, out var entry)) + callback(FileOpen(entry)); + return result; + } + public Stream FileOpen(string fileName) { if (Idx.TryGetEntry(fileName, out var entry)) diff --git a/OpenKh.Kh2/Imgd.cs b/OpenKh.Kh2/Imgd.cs index 735c16ca1..f13d60b86 100644 --- a/OpenKh.Kh2/Imgd.cs +++ b/OpenKh.Kh2/Imgd.cs @@ -18,7 +18,7 @@ public partial class Imgd : IImageRead private const short SubFormat4bpp = 4; public static bool IsValid(Stream stream) => - stream.Length >= 4 && new BinaryReader(stream).PeekInt32() == MagicCode; + stream.Length >= 4 && stream.SetPosition(0).ReadInt32() == MagicCode; private readonly short format; private readonly int swizzled; @@ -58,7 +58,7 @@ private Imgd(Stream stream) Clut = reader.ReadBytes(palLength); } - public static Imgd Read(Stream stream) => new Imgd(stream); + public static Imgd Read(Stream stream) => new Imgd(stream.SetPosition(0)); public void Write(Stream stream) { @@ -76,7 +76,7 @@ public void Write(Stream stream) writer.Write(dataOffset); writer.Write(Data.Length); writer.Write(palOffset); - writer.Write(Clut.Length); + writer.Write(Clut?.Length ?? 0); writer.Write(-1); writer.Write((short)Size.Width); writer.Write((short)Size.Height); @@ -96,7 +96,9 @@ public void Write(Stream stream) writer.Write(swizzled); writer.Write(Data, 0, Data.Length); - writer.Write(Clut, 0, Clut.Length); + + if (Clut != null) + writer.Write(Clut, 0, Clut.Length); } public Size Size { get; } @@ -131,7 +133,7 @@ public byte[] GetClut() case Format8bpp: return GetClut8(); case Format4bpp: return GetClut4(); default: - throw new NotSupportedException($"The format {format} is not supported."); + throw new NotSupportedException($"The format {format} is not supported or does not contain any palette."); } } diff --git a/OpenKh.Kh2/Imgz.cs b/OpenKh.Kh2/Imgz.cs index 42daaa838..3230a4b48 100644 --- a/OpenKh.Kh2/Imgz.cs +++ b/OpenKh.Kh2/Imgz.cs @@ -48,13 +48,13 @@ public static IEnumerable OpenAsStream(Stream stream) } public static bool IsValid(Stream stream) => - stream.Length >= 4 && new BinaryReader(stream).PeekInt32() == MagicCode; + stream.Length >= 4 && stream.SetPosition(0).ReadInt32() == MagicCode; - public static IEnumerable Open(Stream stream) => - OpenAsStream(stream).Select(x => Imgd.Read(x)); + public static IEnumerable Read(Stream stream) => + OpenAsStream(stream.SetPosition(0)).Select(x => Imgd.Read(x)); - public static void Save(Stream stream, IEnumerable images) + public static void Write(Stream stream, IEnumerable images) { if (!stream.CanWrite) throw new InvalidDataException($"Read or seek must be supported."); diff --git a/OpenKh.Kh2/Layout.cs b/OpenKh.Kh2/Layout.cs index 4ff5d8304..56958645c 100644 --- a/OpenKh.Kh2/Layout.cs +++ b/OpenKh.Kh2/Layout.cs @@ -117,7 +117,6 @@ private void WriteSequences(Stream stream) stream.Position = currentPosition; stream.Write(offsets); - } public static bool IsValid(Stream stream) => diff --git a/OpenKh.Kh2/Mdlx.Map.cs b/OpenKh.Kh2/Mdlx.Map.cs new file mode 100644 index 000000000..c8181c337 --- /dev/null +++ b/OpenKh.Kh2/Mdlx.Map.cs @@ -0,0 +1,232 @@ +// Inspired by Kddf2's khkh_xldM. +// Original source code: https://gitlab.com/kenjiuno/khkh_xldM/blob/master/khkh_xldMii/Mdlxfst.cs + +using OpenKh.Common; +using OpenKh.Common.Ps2; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xe.BinaryMapper; + +namespace OpenKh.Kh2 +{ + public partial class Mdlx + { + public class SubModelMapHeader + { + [Data] public int Type { get; set; } + [Data] public int Unk04 { get; set; } + [Data] public int Unk08 { get; set; } + [Data] public int NextOffset { get; set; } + [Data] public int DmaChainMapCount { get; set; } + [Data] public short va4 { get; set; } + [Data] public short Count1 { get; set; } + [Data] public int Offset1 { get; set; } + [Data] public int Offset2 { get; set; } + } + + private class DmaChainMap + { + [Data] public int VifOffset { get; set; } + [Data] public int TextureIndex { get; set; } + [Data] public int Unk08 { get; set; } + [Data] public int Unk0c { get; set; } + } + + public class M4 + { + public int unk04; + public int unk08; + public int nextOffset; + public short va4; + public short count1; + public List alb1t2; + public List alb2; + public int[] unknownTable; + public List VifPackets; + } + + public class VifPacketDescriptor + { + public byte[] VifPacket { get; set; } + public int TextureId { get; set; } + public int Unk08 { get; set; } + public int Unk0c { get; set; } + public ushort[] DmaPerVif { get; set; } + } + + private static M4 ReadAsMap(Stream stream) + { + var header = BinaryMapping.ReadObject(stream); + if (header.Type != Map) throw new NotSupportedException("Type must be 2 for maps"); + + var dmaChainMaps = For(header.DmaChainMapCount, () => BinaryMapping.ReadObject(stream)); + + stream.Position = header.Offset1; + + // The original game engine ignores header.Count1 for some reason + var count1 = (short)((stream.ReadInt32() - header.Offset1) / 4); + stream.Position -= 4; + + var alb2 = For(count1, () => stream.ReadInt32()) + .Select(offset => ReadAlb2t2(stream.SetPosition(offset)).ToArray()) + .ToList(); + + stream.Position = header.Offset2; + var offb1t2t2 = stream.ReadInt32(); + var unknownTableCount = (offb1t2t2 - header.Offset2) / 4 - 1; + var unknownTable = For(unknownTableCount, () => stream.ReadInt32()) + .ToArray(); + + stream.Position = offb1t2t2; + var alb1t2 = ReadAlb2t2(stream).ToList(); + + var vifPackets = dmaChainMaps + .Select(dmaChain => + { + var currentVifOffset = dmaChain.VifOffset; + + DmaTag dmaTag; + var packet = new List(); + var sizePerDma = new List(); + do + { + stream.Position = currentVifOffset; + dmaTag = BinaryMapping.ReadObject(stream); + var packets = stream.ReadBytes(8 + 16 * dmaTag.Qwc); + + packet.AddRange(packets); + + sizePerDma.Add(dmaTag.Qwc); + currentVifOffset += 16 + 16 * dmaTag.Qwc; + } while (dmaTag.TagId < 2); + + return new VifPacketDescriptor + { + VifPacket = packet.ToArray(), + TextureId = dmaChain.TextureIndex, + Unk08 = dmaChain.Unk08, + Unk0c = dmaChain.Unk0c, + DmaPerVif = sizePerDma.ToArray(), + }; + }) + .ToList(); + + return new M4 + { + unk04 = header.Unk04, + unk08 = header.Unk08, + nextOffset = header.NextOffset, + va4 = header.va4, + count1 = header.Count1, + + alb1t2 = alb1t2.ToList(), + alb2 = alb2, + unknownTable = unknownTable, + VifPackets = vifPackets + }; + } + + private static void WriteAsMap(Stream stream, M4 mapModel) + { + var mapHeader = new SubModelMapHeader + { + Type = Map, + DmaChainMapCount = mapModel.VifPackets.Count, + + Unk04 = mapModel.unk04, + Unk08 = mapModel.unk08, + NextOffset = mapModel.nextOffset, + va4 = mapModel.va4, + Count1 = mapModel.count1, // in a form, ignored by the game engine + }; + + BinaryMapping.WriteObject(stream, mapHeader); + + var dmaChainMapDescriptorOffset = (int)stream.Position; + stream.Position += mapModel.VifPackets.Count * 0x10; + + mapHeader.Offset1 = (int)stream.Position; + stream.Position += mapModel.alb2.Count * 4; + var alb2Offsets = new List(); + foreach (var alb2 in mapModel.alb2) + { + alb2Offsets.Add((int)stream.Position); + WriteAlb2(stream, alb2); + } + + var endAlb2Offset = Helpers.Align((int)stream.Position, 4); + stream.Position = mapHeader.Offset1; + foreach (var alb2Offset in alb2Offsets) + stream.Write(alb2Offset); + stream.Position = endAlb2Offset; + + mapHeader.Offset2 = endAlb2Offset; + stream.Write(endAlb2Offset + 4 + mapModel.unknownTable.Length * 4); + stream.Write(mapModel.unknownTable); + WriteAlb2(stream, mapModel.alb1t2); + + stream.AlignPosition(0x10); + + var dmaChainVifOffsets = new List(); + foreach (var dmaChainMap in mapModel.VifPackets) + { + var vifPacketIndex = 0; + dmaChainVifOffsets.Add((int)stream.Position); + + foreach (var packetCount in dmaChainMap.DmaPerVif) + { + BinaryMapping.WriteObject(stream, new DmaTag + { + Qwc = packetCount, + Address = 0, + TagId = packetCount > 0 ? 1 : 6, + Irq = false, + }); + + var packetLength = packetCount * 0x10 + 8; + stream.Write(dmaChainMap.VifPacket, vifPacketIndex, packetLength); + + vifPacketIndex += packetLength; + } + } + + stream.AlignPosition(0x80); + stream.SetLength(stream.Position); + + stream.Position = dmaChainMapDescriptorOffset; + for (var i = 0; i < mapModel.VifPackets.Count; i++) + { + var dmaChainMap = mapModel.VifPackets[i]; + BinaryMapping.WriteObject(stream, new DmaChainMap + { + VifOffset = dmaChainVifOffsets[i], + TextureIndex = dmaChainMap.TextureId, + Unk08 = dmaChainMap.Unk08, + Unk0c = dmaChainMap.Unk0c + }); + } + + stream.Position = 0; + BinaryMapping.WriteObject(stream, mapHeader); + } + + private static IEnumerable ReadAlb2t2(Stream stream) + { + while (true) + { + var data = stream.ReadUInt16(); + if (data == 0xFFFF) break; + yield return data; + } + } + + private static void WriteAlb2(Stream stream, IEnumerable alb2t2) + { + foreach (var data in alb2t2) + stream.Write(data); + stream.Write((ushort)0xFFFF); + } + } +} diff --git a/OpenKh.Kh2/Mdlx.Model.cs b/OpenKh.Kh2/Mdlx.Model.cs new file mode 100644 index 000000000..964b6b8f6 --- /dev/null +++ b/OpenKh.Kh2/Mdlx.Model.cs @@ -0,0 +1,372 @@ +// Inspired by Kddf2's khkh_xldM. +// Original source code: https://gitlab.com/kenjiuno/khkh_xldM/blob/master/khkh_xldMii/Mdlxfst.cs + +using OpenKh.Common; +using OpenKh.Common.Ps2; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xe.BinaryMapper; +using Xe.IO; + +namespace OpenKh.Kh2 +{ + public partial class Mdlx + { + private static readonly VifCode DefaultVifCode = new VifCode + { + Opcode = VifOpcode.STCYCL, + Interrupt = false, + Num = 1, + Immediate = 0x0100, + }; + private static readonly DmaPacket CloseDmaPacket = new DmaPacket + { + DmaTag = new DmaTag + { + Qwc = 0, + TagId = 1, + Irq = false, + Address = 0 + }, + VifCode = new VifCode + { + Opcode = VifOpcode.NOP, + Interrupt = false, + Num = 0, + Immediate = 0x1700, + }, + Parameter = 0 + }; + + public class Bone + { + [Data] public int Index { get; set; } + [Data] public int Parent { get; set; } + [Data] public int Unk08 { get; set; } + [Data] public int Unk0c { get; set; } + [Data] public float ScaleX { get; set; } + [Data] public float ScaleY { get; set; } + [Data] public float ScaleZ { get; set; } + [Data] public float ScaleW { get; set; } + [Data] public float RotationX { get; set; } + [Data] public float RotationY { get; set; } + [Data] public float RotationZ { get; set; } + [Data] public float RotationW { get; set; } + [Data] public float TranslationX { get; set; } + [Data] public float TranslationY { get; set; } + [Data] public float TranslationZ { get; set; } + [Data] public float TranslationW { get; set; } + } + + public class DmaPacket + { + [Data] public DmaTag DmaTag { get; set; } + [Data] public VifCode VifCode { get; set; } + [Data] public int Parameter { get; set; } + } + + private class SubModelHeader + { + [Data] public int Type { get; set; } + [Data] public int Unk04 { get; set; } + [Data] public int Unk08 { get; set; } + [Data] public int NextOffset { get; set; } + [Data] public short BoneCount { get; set; } + [Data] public short Unk { get; set; } + [Data] public int BoneOffset { get; set; } + [Data] public int UnkDataOffset { get; set; } + [Data] public int DmaChainCount { get; set; } + } + + private class DmaChainHeader + { + [Data] public int Unk00 { get; set; } + [Data] public int TextureIndex { get; set; } + [Data] public int Unk08 { get; set; } + [Data] public int Unused1 { get; set; } + [Data] public int DmaOffset { get; set; } + [Data] public int Count1aOffset { get; set; } + [Data] public int DmaLength { get; set; } + [Data] public int Unused2 { get; set; } + } + + public class SubModel + { + public int Type { get; set; } + public int Unk04 { get; set; } + public int Unk08 { get; set; } + public short BoneCount { get; set; } + public short Unk { get; set; } + public int DmaChainCount { get; set; } + + public List Bones { get; internal set; } + public byte[] UnknownData { get; internal set; } + public List DmaChains { get; internal set; } + } + + public class DmaChain + { + public int Unk00 { get; set; } + public int TextureIndex { get; set; } + public int Unk08 { get; set; } + public int DmaLength { get; set; } + public List DmaVifs { get; set; } + } + + public class DmaVif + { + public int TextureIndex { get; } + public int[] Alaxi { get; } + public byte[] VifPacket { get; } + public int BaseAddress { get; } + + public DmaVif(int texi, int[] alaxi, byte[] bin, int baseAddress) + { + TextureIndex = texi; + Alaxi = alaxi; + VifPacket = bin; + BaseAddress = baseAddress; + } + } + + private static IEnumerable ReadAsModel(Stream stream) + { + var currentOffset = 0; + var nextOffset = ReservedArea; + while (nextOffset != 0) + { + currentOffset += nextOffset; + var subStream = new SubStream(stream, currentOffset, stream.Length - currentOffset); + if (subStream.Length == 0) + yield break; + + nextOffset = ReadSubModel(subStream, out var subModel); + + yield return subModel; + } + } + + private static int ReadSubModel(Stream stream, out SubModel subModel) + { + var header = BinaryMapping.ReadObject(stream); + subModel = new SubModel + { + Type = header.Type, + Unk04 = header.Unk04, + Unk08 = header.Unk08, + BoneCount = header.BoneCount, + Unk = header.Unk, + DmaChainCount = header.DmaChainCount, + }; + + var dmaChainHeaders = For(subModel.DmaChainCount, () => BinaryMapping.ReadObject(stream)); + + stream.Position = header.UnkDataOffset; + subModel.UnknownData = stream.ReadBytes(0x120); + + if (header.BoneOffset != 0) + { + stream.Position = header.BoneOffset; + subModel.Bones = For(subModel.BoneCount, () => ReadBone(stream)).ToList(); + } + + subModel.DmaChains = dmaChainHeaders.Select(x => ReadDmaChain(stream, x)).ToList(); + + return header.NextOffset; + } + + private static DmaChain ReadDmaChain(Stream stream, DmaChainHeader dmaChainHeader) + { + var dmaVifs = new List(); + var count1a = stream.SetPosition(dmaChainHeader.Count1aOffset).ReadInt32(); + var alv1 = For(count1a, () => stream.ReadInt32()).ToList(); + + var offsetDmaPackets = new List(); + var alaxi = new List(); + var alaxref = new List(); + + offsetDmaPackets.Add(dmaChainHeader.DmaOffset); + + var offsetDmaBase = dmaChainHeader.DmaOffset + 0x10; + + for (var i = 0; i < count1a; i++) + { + if (alv1[i] == -1) + { + offsetDmaPackets.Add(offsetDmaBase + 0x10); + offsetDmaBase += 0x20; + + alaxi.Add(alaxref.ToArray()); + alaxref.Clear(); + } + else + { + offsetDmaBase += 0x10; + alaxref.Add(alv1[i]); + } + } + + alaxi.Add(alaxref.ToArray()); + alaxref.Clear(); + + var dmaPackets = offsetDmaPackets + .Select(offset => ReadTags(stream.SetPosition(offset)).ToArray()) + .ToArray(); + + for (var i = 0; i < offsetDmaPackets.Count; i++) + { + var dmaTag = dmaPackets[i][0].DmaTag; + var baseAddress = dmaTag.Qwc != 0 ? dmaPackets[i][1].Parameter : 0; + stream.Position = dmaTag.Address & 0x7FFFFFFF; + var vifPacket = stream.ReadBytes(dmaTag.Qwc * 0x10); + dmaVifs.Add(new DmaVif(dmaChainHeader.TextureIndex, alaxi[i], vifPacket, baseAddress)); + } + + return new DmaChain + { + Unk00 = dmaChainHeader.Unk00, + TextureIndex = dmaChainHeader.TextureIndex, + Unk08 = dmaChainHeader.Unk08, + DmaLength = dmaChainHeader.DmaLength, + DmaVifs = dmaVifs, + }; + } + + private static IEnumerable ReadTags(Stream stream) + { + while (true) + { + var dmaPacket = BinaryMapping.ReadObject(stream); + yield return dmaPacket; + + if (dmaPacket.DmaTag.Qwc == 0) + yield break; + } + } + + private static void WriteSubModel(Stream stream, SubModel subModel, int baseAddress) + { + var header = new SubModelHeader + { + Type = subModel.Type, + Unk04 = subModel.Unk04, + Unk08 = subModel.Unk08, + Unk = subModel.Unk, + DmaChainCount = subModel.DmaChains.Count, + }; + + stream.Position += 0x20; // skip header + stream.Position += subModel.DmaChainCount * 0x20; + + if (subModel.Type == Entity) + { + header.UnkDataOffset = (int)stream.Position; + stream.Write(subModel.UnknownData); + + header.BoneOffset = (int)stream.Position; + header.BoneCount = (short)subModel.Bones.Count; + foreach (var bone in subModel.Bones) + WriteBone(stream, bone); + } + else if (subModel.Type == Shadow) + { + header.UnkDataOffset = 0; + header.BoneOffset = 0; + header.BoneCount = subModel.BoneCount; + } + else + throw new NotImplementedException($"Submodel type {subModel.Type} not supported."); + + var dmaChainHeaders = subModel.DmaChains.Select(x => WriteDmaChain(stream, x)).ToList(); + + stream.SetLength(stream.AlignPosition(0x80).Position); + header.NextOffset = baseAddress >= 0 ? (int)(baseAddress + stream.Position) : 0; + + stream.Position = 0; + BinaryMapping.WriteObject(stream, header); + foreach (var dmaChainHeader in dmaChainHeaders) + BinaryMapping.WriteObject(stream, dmaChainHeader); + } + + private static DmaChainHeader WriteDmaChain(Stream stream, DmaChain dmaChain) + { + var dmaChainHeader = new DmaChainHeader + { + Unk00 = dmaChain.Unk00, + TextureIndex = dmaChain.TextureIndex, + Unk08 = dmaChain.Unk08, + DmaLength = dmaChain.DmaLength, + }; + + var dmaVifs = dmaChain.DmaVifs; + + var vifPacketOffsets = new List(); + foreach (var dmaVif in dmaVifs) + { + vifPacketOffsets.Add((int)stream.Position); + stream.Write(dmaVif.VifPacket); + } + + dmaChainHeader.DmaOffset = (int)stream.Position; + + for (var i = 0; i < dmaVifs.Count; i++) + { + if (dmaVifs[i].BaseAddress > 0) + { + BinaryMapping.WriteObject(stream, new DmaPacket + { + DmaTag = new DmaTag + { + Qwc = (ushort)(dmaVifs[i].VifPacket.Length / 0x10), + TagId = 3, + Irq = false, + Address = vifPacketOffsets[i] + }, + VifCode = new VifCode { }, + Parameter = 0 + }); + + for (var j = 0; j < dmaVifs[i].Alaxi.Length; j++) + { + BinaryMapping.WriteObject(stream, new DmaPacket + { + DmaTag = new DmaTag + { + Qwc = 4, + TagId = 3, + Irq = false, + Address = dmaVifs[i].Alaxi[j] + }, + VifCode = DefaultVifCode, + Parameter = dmaVifs[i].BaseAddress + j * 4 + }); + } + } + + BinaryMapping.WriteObject(stream, CloseDmaPacket); + } + + var alv1 = new List(); + foreach (var vif in dmaVifs) + { + alv1.AddRange(vif.Alaxi); + alv1.Add(-1); + } + alv1.RemoveAt(alv1.Count - 1); + + dmaChainHeader.Count1aOffset = (int)stream.Position; + stream.Write(alv1.Count); + foreach (var alvItem in alv1) + stream.Write(alvItem); + + stream.AlignPosition(0x10); + + return dmaChainHeader; + } + + private static Bone ReadBone(Stream stream) => BinaryMapping.ReadObject(stream); + private static void WriteBone(Stream stream, Bone bone) => BinaryMapping.WriteObject(stream, bone); + } +} diff --git a/OpenKh.Kh2/Mdlx.cs b/OpenKh.Kh2/Mdlx.cs new file mode 100644 index 000000000..3069c6a8a --- /dev/null +++ b/OpenKh.Kh2/Mdlx.cs @@ -0,0 +1,73 @@ +using OpenKh.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xe.IO; + +namespace OpenKh.Kh2 +{ + public partial class Mdlx + { + private const int Map = 2; + private const int Entity = 3; + private const int Shadow = 4; + private const int ReservedArea = 0x90; + + public List SubModels { get; } + public M4 MapModel { get; } + + private Mdlx(Stream stream) + { + var type = ReadMdlxType(stream); + stream.Position = 0; + + switch (type) + { + case Map: + MapModel = ReadAsMap(new SubStream(stream, ReservedArea, stream.Length - ReservedArea)); + break; + case Entity: + SubModels = ReadAsModel(stream).ToList(); + break; + } + } + + public bool IsMap => MapModel != null; + + public void Write(Stream realStream) + { + var stream = new MemoryStream(); + if (IsMap) + WriteAsMap(stream, MapModel); + else + WriteAsModel(stream, SubModels); + + realStream.Position = ReservedArea; + realStream.Write(stream.GetBuffer(), 0, (int)stream.Length); + } + + private static void WriteAsModel(Stream stream, List subModels) + { + var baseAddress = 0; + for (var i = 0; i < subModels.Count; i++) + { + if (i + 1 >= subModels.Count) + baseAddress = -1; + + var subModelStream = new MemoryStream(); + WriteSubModel(subModelStream, subModels[i], baseAddress); + subModelStream.SetPosition(0).Copy(stream, (int)subModelStream.Length); + } + } + + public static Mdlx Read(Stream stream) => + new Mdlx(stream.SetPosition(0)); + + private static int ReadMdlxType(Stream stream) => + stream.SetPosition(ReservedArea).ReadInt32(); + + private static T[] For(int count, Func func) => + Enumerable.Range(0, count).Select(_ => func()).ToArray(); + } +} diff --git a/OpenKh.Kh2/Messages/Encoders.cs b/OpenKh.Kh2/Messages/Encoders.cs index 79f1a7ea2..ca39c7965 100644 --- a/OpenKh.Kh2/Messages/Encoders.cs +++ b/OpenKh.Kh2/Messages/Encoders.cs @@ -1,10 +1,32 @@ using OpenKh.Kh2.Messages.Internals; +using System; +using System.Collections.Generic; namespace OpenKh.Kh2.Messages { public static class Encoders { + internal class InternationalSystemEncoder : IMessageEncoder + { + private readonly IMessageDecode _decode = new InternationalSystemDecode(); + private readonly IMessageEncode _encode = new InternationalSystemEncode(); + + public List Decode(byte[] data) => _decode.Decode(data); + public byte[] Encode(List messageCommands) => _encode.Encode(messageCommands); + } + + internal class JapaneseSystemEncoder : IMessageEncoder + { + private readonly IMessageDecode _decode = new JapaneseSystemDecode(); + private readonly IMessageEncode _encode = new JapaneseSystemEncode(); + + public List Decode(byte[] data) => _decode.Decode(data); + public byte[] Encode(List messageCommands) => _encode.Encode(messageCommands); + } + public static IMessageEncoder InternationalSystem { get; } = new InternationalSystemEncoder(); + public static IMessageEncoder JapaneseSystem { get; } = + new JapaneseSystemEncoder(); } } diff --git a/OpenKh.Kh2/Messages/Internals/BaseMessageDecoder.cs b/OpenKh.Kh2/Messages/Internals/BaseMessageDecoder.cs index 93108b755..4c71fd4cf 100644 --- a/OpenKh.Kh2/Messages/Internals/BaseMessageDecoder.cs +++ b/OpenKh.Kh2/Messages/Internals/BaseMessageDecoder.cs @@ -1,11 +1,20 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace OpenKh.Kh2.Messages.Internals { - internal partial class BaseMessageDecoder + internal interface IDecoder + { + bool IsEof(int offset = 0); + byte Peek(int offset); + byte Next(); + bool WrapTable(ref byte ch, ref byte parameter); + void AppendComplex(string str); + } + + internal partial class BaseMessageDecoder : IDecoder { private readonly Dictionary _table; private readonly List _entries; @@ -22,31 +31,83 @@ internal BaseMessageDecoder( _data = data; } - internal List Decode() + internal List Decode(Func handler = null) { while (!IsEof()) { + if (handler?.Invoke(this) ?? false) + continue; + byte ch = Next(); - if (!_table.TryGetValue(ch, out var cmdModel) || cmdModel == null) - throw new NotImplementedException($"Command {ch:X02} not implemented yet"); - - if (cmdModel.Command == MessageCommand.PrintText) - AppendChar(cmdModel.Text[0]); - else if (cmdModel.Command == MessageCommand.PrintComplex) - AppendText(cmdModel.Text); - else if (cmdModel.Command == MessageCommand.Unsupported) - AppendEntry(cmdModel.Command, new byte[] { cmdModel.RawData }); - else - AppendEntry(cmdModel); + var cmdModel = GetCommandModel(ch); + + + switch (cmdModel.Command) + { + case MessageCommand.PrintText: + Append(cmdModel.Text[0]); + break; + case MessageCommand.PrintComplex: + AppendComplex(cmdModel.Text); + break; + case MessageCommand.Table2: + case MessageCommand.Table3: + case MessageCommand.Table4: + case MessageCommand.Table5: + case MessageCommand.Table6: + case MessageCommand.Table7: + case MessageCommand.Table8: + AppendFromTable(cmdModel, ch, Next()); + break; + case MessageCommand.Unsupported: + AppendEntry(cmdModel.Command, new byte[] { cmdModel.RawData }); + break; + default: + AppendEntry(cmdModel); + break; + } } FlushTextBuilder(); return _entries; } - private bool IsEof() => _index >= _data.Length; + private void AppendFromTable(BaseCmdModel cmdModel, byte ch, byte parameter) + { + if (WrapTable(ref ch, ref parameter)) + AppendFromTable(GetCommandModel(ch), ch, parameter); + else + Append((cmdModel as TableCmdModel).GetText(parameter)); + } + + private BaseCmdModel GetCommandModel(byte ch) + { + if (!_table.TryGetValue(ch, out var commandModel) || commandModel == null) + throw new NotImplementedException($"Command {ch:X02} not implemented yet"); + + return commandModel; + } + public bool IsEof(int offset = 0) => _index + offset >= _data.Length; + public byte Peek(int offset) => _data[_index + offset]; public byte Next() => _data[_index++]; + public bool WrapTable(ref byte ch, ref byte parameter) + { + if (ch >= 0x20) + return false; + + var data = (ushort)((ch << 8) | parameter); + if (data >= 0x1e40) + { + data -= 0x310; + + ch = (byte)(data >> 8); + parameter = (byte)(data & 0xff); + return true; + } + + return false; + } private StringBuilder RequestTextBuilder() { @@ -81,8 +142,9 @@ private void AppendEntry(MessageCommand command, byte[] data) }); } - private void AppendChar(char ch) => RequestTextBuilder().Append(ch); - private void AppendText(string str) + private void Append(char ch) => RequestTextBuilder().Append(ch); + private void Append(string str) => RequestTextBuilder().Append(str); + public void AppendComplex(string str) { FlushTextBuilder(); RequestTextBuilder().Append(str); diff --git a/OpenKh.Kh2/Messages/Internals/InternationalSystemDecode.cs b/OpenKh.Kh2/Messages/Internals/InternationalSystemDecode.cs index 1376045a3..ada09a639 100644 --- a/OpenKh.Kh2/Messages/Internals/InternationalSystemDecode.cs +++ b/OpenKh.Kh2/Messages/Internals/InternationalSystemDecode.cs @@ -1,9 +1,12 @@ using System.Collections.Generic; +using System.Linq; namespace OpenKh.Kh2.Messages.Internals { internal class InternationalSystemDecode : IMessageDecode { + private static readonly char[] _tableGeneric = Enumerable.Range(0, 0x100).Select(x => '?').ToArray(); + public static readonly Dictionary _table = new Dictionary { [0x00] = new SimpleCmdModel(MessageCommand.End), @@ -31,13 +34,13 @@ internal class InternationalSystemDecode : IMessageDecode [0x16] = new SingleDataCmdModel(MessageCommand.Unknown16), [0x17] = new DataCmdModel(MessageCommand.DelayAndFade, 2), [0x18] = new DataCmdModel(MessageCommand.Unknown18, 2), - [0x19] = new SingleDataCmdModel(MessageCommand.Unknown19), - [0x1a] = new SingleDataCmdModel(MessageCommand.Unknown1a), - [0x1b] = new SingleDataCmdModel(MessageCommand.Unknown1b), - [0x1c] = new SingleDataCmdModel(MessageCommand.Unknown1c), - [0x1d] = new SingleDataCmdModel(MessageCommand.Unknown1d), - [0x1e] = new SingleDataCmdModel(MessageCommand.Unknown1e), - [0x1f] = new SingleDataCmdModel(MessageCommand.Unknown1f), + [0x19] = new TableCmdModel(MessageCommand.Table2, _tableGeneric), + [0x1a] = new TableCmdModel(MessageCommand.Table3, _tableGeneric), + [0x1b] = new TableCmdModel(MessageCommand.Table4, _tableGeneric), + [0x1c] = new TableCmdModel(MessageCommand.Table5, _tableGeneric), + [0x1d] = new TableCmdModel(MessageCommand.Table6, _tableGeneric), + [0x1e] = new TableCmdModel(MessageCommand.Table7, _tableGeneric), + [0x1f] = new TableCmdModel(MessageCommand.Table8, _tableGeneric), [0x20] = new TextCmdModel('⬛'), [0x21] = new TextCmdModel('0'), [0x22] = new TextCmdModel('1'), diff --git a/OpenKh.Kh2/Messages/Internals/InternationalSystemEncoder.cs b/OpenKh.Kh2/Messages/Internals/InternationalSystemEncoder.cs deleted file mode 100644 index 2be5208ea..000000000 --- a/OpenKh.Kh2/Messages/Internals/InternationalSystemEncoder.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace OpenKh.Kh2.Messages.Internals -{ - internal class InternationalSystemEncoder : IMessageEncoder - { - private readonly IMessageDecode _decode = new InternationalSystemDecode(); - private readonly IMessageEncode _encode = new InternationalSystemEncode(); - - public List Decode(byte[] data) => _decode.Decode(data); - public byte[] Encode(List messageCommands) => _encode.Encode(messageCommands); - } -} diff --git a/OpenKh.Kh2/Messages/Internals/JapaneseSystemDecode.cs b/OpenKh.Kh2/Messages/Internals/JapaneseSystemDecode.cs new file mode 100644 index 000000000..551cc73bb --- /dev/null +++ b/OpenKh.Kh2/Messages/Internals/JapaneseSystemDecode.cs @@ -0,0 +1,405 @@ +using System.Collections.Generic; + +namespace OpenKh.Kh2.Messages.Internals +{ + internal class JapaneseSystemDecode : IMessageDecode + { + public static readonly Dictionary _table = new Dictionary + { + [0x00] = new SimpleCmdModel(MessageCommand.End), + [0x01] = new TextCmdModel(' '), + [0x02] = new SimpleCmdModel(MessageCommand.NewLine), + [0x03] = new SimpleCmdModel(MessageCommand.Reset), + [0x04] = new SingleDataCmdModel(MessageCommand.Theme), + [0x05] = new DataCmdModel(MessageCommand.Unknown05, 6), + [0x06] = new SingleDataCmdModel(MessageCommand.Unknown06), + [0x07] = new DataCmdModel(MessageCommand.Color, 4), + [0x08] = new DataCmdModel(MessageCommand.Unknown08, 3), + [0x09] = new SingleDataCmdModel(MessageCommand.PrintIcon), + [0x0a] = new SingleDataCmdModel(MessageCommand.TextScale), + [0x0b] = new SingleDataCmdModel(MessageCommand.TextWidth), + [0x0c] = new SingleDataCmdModel(MessageCommand.LineSpacing), + [0x0d] = new SimpleCmdModel(MessageCommand.Unknown0d), + [0x0e] = new SingleDataCmdModel(MessageCommand.Unknown0e), + [0x0f] = new DataCmdModel(MessageCommand.Unknown0f, 5), + [0x10] = new SimpleCmdModel(MessageCommand.Clear), + [0x11] = new DataCmdModel(MessageCommand.Position, 4), + [0x12] = new DataCmdModel(MessageCommand.Unknown12, 2), + [0x13] = new DataCmdModel(MessageCommand.Unknown13, 4), + [0x14] = new DataCmdModel(MessageCommand.Delay, 2), + [0x15] = new DataCmdModel(MessageCommand.CharDelay, 2), + [0x16] = new SingleDataCmdModel(MessageCommand.Unknown16), + [0x17] = null, + [0x18] = new DataCmdModel(MessageCommand.Unknown18, 2), + [0x19] = new TableCmdModel(MessageCommand.Table2, JapaneseTable._table2), + [0x1a] = new TableCmdModel(MessageCommand.Table3, JapaneseTable._table3), + [0x1b] = new TableCmdModel(MessageCommand.Table4, JapaneseTable._table4), + [0x1c] = new TableCmdModel(MessageCommand.Table5, JapaneseTable._table5), + [0x1d] = new TableCmdModel(MessageCommand.Table6, JapaneseTable._table6), + [0x1e] = new TableCmdModel(MessageCommand.Table7, JapaneseTable._table7), + [0x1f] = new TableCmdModel(MessageCommand.Table8, JapaneseTable._table8), + [0x20] = new TextCmdModel('⬛'), + [0x21] = new TextCmdModel('0'), + [0x22] = new TextCmdModel('1'), + [0x23] = new TextCmdModel('2'), + [0x24] = new TextCmdModel('3'), + [0x25] = new TextCmdModel('4'), + [0x26] = new TextCmdModel('5'), + [0x27] = new TextCmdModel('6'), + [0x28] = new TextCmdModel('7'), + [0x29] = new TextCmdModel('8'), + [0x2a] = new TextCmdModel('9'), + [0x2b] = new TextCmdModel('+'), + [0x2c] = new TextCmdModel('−'), + [0x2d] = new TextCmdModel('ₓ'), + [0x2e] = new TextCmdModel('A'), + [0x2f] = new TextCmdModel('B'), + [0x30] = new TextCmdModel('C'), + [0x31] = new TextCmdModel('D'), + [0x32] = new TextCmdModel('E'), + [0x33] = new TextCmdModel('F'), + [0x34] = new TextCmdModel('G'), + [0x35] = new TextCmdModel('H'), + [0x36] = new TextCmdModel('I'), + [0x37] = new TextCmdModel('J'), + [0x38] = new TextCmdModel('K'), + [0x39] = new TextCmdModel('L'), + [0x3a] = new TextCmdModel('M'), + [0x3b] = new TextCmdModel('N'), + [0x3c] = new TextCmdModel('O'), + [0x3d] = new TextCmdModel('P'), + [0x3e] = new TextCmdModel('Q'), + [0x3f] = new TextCmdModel('R'), + [0x40] = new TextCmdModel('S'), + [0x41] = new TextCmdModel('T'), + [0x42] = new TextCmdModel('U'), + [0x43] = new TextCmdModel('V'), + [0x44] = new TextCmdModel('W'), + [0x45] = new TextCmdModel('X'), + [0x46] = new TextCmdModel('Y'), + [0x47] = new TextCmdModel('Z'), + [0x48] = new TextCmdModel('!'), + [0x49] = new TextCmdModel('?'), + [0x4a] = new TextCmdModel('%'), + [0x4b] = new TextCmdModel('/'), + [0x4c] = new TextCmdModel('※'), + [0x4d] = new TextCmdModel('、'), + [0x4e] = new TextCmdModel('。'), + [0x4f] = new TextCmdModel('.'), + [0x50] = new TextCmdModel(','), + [0x51] = new TextCmdModel('·'), + [0x52] = new TextCmdModel(':'), + [0x53] = new TextCmdModel('…'), + [0x54] = new TextCmdModel("-"), + [0x55] = new TextCmdModel('–'), + [0x56] = new TextCmdModel('〜'), + [0x57] = new TextCmdModel("'"), + [0x58] = new TextCmdModel("‟"), + [0x59] = new TextCmdModel("„"), + [0x5a] = new TextCmdModel('('), + [0x5b] = new TextCmdModel(')'), + [0x5c] = new TextCmdModel('「'), + [0x5d] = new TextCmdModel('」'), + [0x5e] = new TextCmdModel('『'), + [0x5f] = new TextCmdModel('』'), + [0x60] = new TextCmdModel('“'), + [0x61] = new TextCmdModel('”'), + [0x62] = new TextCmdModel('['), + [0x63] = new TextCmdModel(']'), + [0x64] = new TextCmdModel('<'), + [0x65] = new TextCmdModel('>'), + [0x66] = new TextCmdModel('-'), + [0x67] = new TextCmdModel("–"), + [0x68] = new TextCmdModel('⤷'), // Used only in EVT + [0x69] = new TextCmdModel('⇾'), // Used only in EVT + [0x6a] = new TextCmdModel('⇽'), // Used only in EVT + [0x6b] = new TextCmdModel('♩'), + [0x6c] = new TextCmdModel("全"), + [0x6d] = new TextCmdModel("合"), + [0x6e] = new TextCmdModel("成"), + [0x6f] = new TextCmdModel("半"), + [0x70] = new TextCmdModel('◯'), + [0x71] = new TextCmdModel('✕'), + [0x72] = new TextCmdModel('△'), + [0x73] = new TextCmdModel('☐'), + [0x74] = new TextCmdModel('▴'), + [0x75] = new TextCmdModel('▾'), + [0x76] = new TextCmdModel('▸'), + [0x77] = new TextCmdModel('◂'), + [0x78] = null, + [0x79] = null, + [0x7a] = null, + [0x7b] = null, + [0x7c] = null, + [0x7d] = null, + [0x7e] = null, + [0x7f] = null, + [0x80] = null, + [0x81] = null, + [0x82] = new TextCmdModel('⭑'), + [0x83] = new TextCmdModel('⭒'), + [0x84] = new TextCmdModel("III"), + [0x85] = new TextCmdModel("VII"), + [0x86] = new TextCmdModel("VIII"), + [0x87] = new TextCmdModel("X"), + [0x88] = new TextCmdModel("(R)"), + [0x89] = new TextCmdModel("o"), + [0x8a] = new TextCmdModel("n"), + [0x8b] = new TextCmdModel("r"), + [0x8c] = new UnsupportedCmdModel(0x8c), + [0x8d] = new TextCmdModel('前'), + [0x8e] = new TextCmdModel('選'), + [0x8f] = new TextCmdModel('一'), + [0x90] = new TextCmdModel('あ'), + [0x91] = new TextCmdModel('い'), + [0x92] = new TextCmdModel('う'), + [0x93] = new TextCmdModel('え'), + [0x94] = new TextCmdModel('お'), + [0x95] = new TextCmdModel('か'), + [0x96] = new TextCmdModel('き'), + [0x97] = new TextCmdModel('く'), + [0x98] = new TextCmdModel('け'), + [0x99] = new TextCmdModel('こ'), + [0x9a] = new TextCmdModel('さ'), + [0x9b] = new TextCmdModel('し'), + [0x9c] = new TextCmdModel('す'), + [0x9d] = new TextCmdModel('せ'), + [0x9e] = new TextCmdModel('そ'), + [0x9f] = new TextCmdModel('た'), + [0xa0] = new TextCmdModel('ち'), + [0xa1] = new TextCmdModel('つ'), + [0xa2] = new TextCmdModel('て'), + [0xa3] = new TextCmdModel('と'), + [0xa4] = new TextCmdModel('な'), + [0xa5] = new TextCmdModel('に'), + [0xa6] = new TextCmdModel('ぬ'), + [0xa7] = new TextCmdModel('ね'), + [0xa8] = new TextCmdModel('の'), + [0xa9] = new TextCmdModel('は'), + [0xaa] = new TextCmdModel('ひ'), + [0xab] = new TextCmdModel('ふ'), + [0xac] = new TextCmdModel('へ'), + [0xad] = new TextCmdModel('ほ'), + [0xae] = new TextCmdModel('ま'), + [0xaf] = new TextCmdModel('み'), + [0xb0] = new TextCmdModel('む'), + [0xb1] = new TextCmdModel('め'), + [0xb2] = new TextCmdModel('も'), + [0xb3] = new TextCmdModel('や'), + [0xb4] = new TextCmdModel('ゆ'), + [0xb5] = new TextCmdModel('よ'), + [0xb6] = new TextCmdModel('ら'), + [0xb7] = new TextCmdModel('り'), + [0xb8] = new TextCmdModel('る'), + [0xb9] = new TextCmdModel('れ'), + [0xba] = new TextCmdModel('ろ'), + [0xbb] = new TextCmdModel('ゎ'), + [0xbc] = new TextCmdModel('を'), + [0xbd] = new TextCmdModel('ん'), + [0xbe] = new TextCmdModel('が'), + [0xbf] = new TextCmdModel('ぎ'), + [0xc0] = new TextCmdModel('ぐ'), + [0xc1] = new TextCmdModel('げ'), + [0xc2] = new TextCmdModel('ご'), + [0xc3] = new TextCmdModel('ざ'), + [0xc4] = new TextCmdModel('じ'), + [0xc5] = new TextCmdModel('ず'), + [0xc6] = new TextCmdModel('ぜ'), + [0xc7] = new TextCmdModel('ぞ'), + [0xc8] = new TextCmdModel('だ'), + [0xc9] = new TextCmdModel('ぢ'), + [0xca] = new TextCmdModel('づ'), + [0xcb] = new TextCmdModel('で'), + [0xcc] = new TextCmdModel('ど'), + [0xcd] = new TextCmdModel('ば'), + [0xce] = new TextCmdModel('び'), + [0xcf] = new TextCmdModel('ぶ'), + [0xd0] = new TextCmdModel('べ'), + [0xd1] = new TextCmdModel('ぼ'), + [0xd2] = new TextCmdModel('ぱ'), + [0xd3] = new TextCmdModel('ぴ'), + [0xd4] = new TextCmdModel('ぷ'), + [0xd5] = new TextCmdModel('ぺ'), + [0xd6] = new TextCmdModel('ぽ'), + [0xd7] = new TextCmdModel('ぁ'), + [0xd8] = new TextCmdModel('ぃ'), + [0xd9] = new TextCmdModel('ぅ'), + [0xda] = new TextCmdModel('ぇ'), + [0xdb] = new TextCmdModel('ぉ'), + [0xdc] = new TextCmdModel('ゃ'), + [0xdd] = new TextCmdModel('ゅ'), + [0xde] = new TextCmdModel('ょ'), + [0xdf] = new TextCmdModel('っ'), + [0xe0] = new TextCmdModel('ア'), + [0xe1] = new TextCmdModel('イ'), + [0xe2] = new TextCmdModel('ウ'), + [0xe3] = new TextCmdModel('エ'), + [0xe4] = new TextCmdModel('オ'), + [0xe5] = new TextCmdModel('カ'), + [0xe6] = new TextCmdModel('キ'), + [0xe7] = new TextCmdModel('ク'), + [0xe8] = new TextCmdModel('ケ'), + [0xe9] = new TextCmdModel('コ'), + [0xea] = new TextCmdModel('サ'), + [0xeb] = new TextCmdModel('シ'), + [0xec] = new TextCmdModel('ス'), + [0xed] = new TextCmdModel('セ'), + [0xee] = new TextCmdModel('ソ'), + [0xef] = new TextCmdModel('タ'), + [0xf0] = new TextCmdModel('チ'), + [0xf1] = new TextCmdModel('ツ'), + [0xf2] = new TextCmdModel('テ'), + [0xf3] = new TextCmdModel('ト'), + [0xf4] = new TextCmdModel('ナ'), + [0xf5] = new TextCmdModel('ニ'), + [0xf6] = new TextCmdModel('ヌ'), + [0xf7] = new TextCmdModel('ネ'), + [0xf8] = new TextCmdModel('ノ'), + [0xf9] = new TextCmdModel('ハ'), + [0xfa] = new TextCmdModel('ヒ'), + [0xfb] = new TextCmdModel('フ'), + [0xfc] = new TextCmdModel('ヘ'), + [0xfd] = new TextCmdModel('ホ'), + [0xfe] = new TextCmdModel('マ'), + [0xff] = new TextCmdModel('ミ'), + }; + + public List Decode(byte[] data) => + new BaseMessageDecoder(_table, data).Decode(decoder => + { + if (decoder.IsEof(1)) + return false; + + var ch = decoder.Peek(0); + var parameter = decoder.Peek(1); + decoder.WrapTable(ref ch, ref parameter); + + switch (ch) + { + case 0x19: + if (parameter == 0xb2) + return AppendComplex(decoder, "XIII"); + break; + case 0x1b: + switch (parameter) + { + case 0x54: return AppendComplex(decoder, "I"); + case 0x55: return AppendComplex(decoder, "II"); + case 0x56: return AppendComplex(decoder, "IV"); + case 0x57: return AppendComplex(decoder, "V"); + case 0x58: return AppendComplex(decoder, "VI"); + case 0x59: return AppendComplex(decoder, "IX"); + } + break; + } + + return false; + }); + + private bool AppendComplex(IDecoder decoder, string value) + { + decoder.Next(); + decoder.Next(); + decoder.AppendComplex(value); + return true; + } + } + + internal static class JapaneseTable + { + public static readonly char[] _table2 = new char[0x100] + { + 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', 'リ', 'ル', 'レ', 'ロ', 'ワ', 'ヲ', 'ン', 'ガ', 'ギ', 'グ', 'ゲ', 'ゴ', 'ザ', 'ジ', 'ズ', 'ゼ', 'ゾ', 'ダ', 'ヂ', 'ヅ', 'デ', + 'ド', 'バ', 'ビ', 'ブ', 'ベ', 'ボ', 'ヴ', 'パ', 'ピ', 'プ', 'ペ', 'ポ', 'ァ', 'ィ', 'ゥ', 'ェ', 'ォ', 'ャ', 'ュ', 'ョ', 'ッ', '端', '子', '接', '続', '正', '常', '発', + '生', '使', '用', '専', '付', '属', '取', '扱', '説', '明', '書', '指', '示', '従', '修', '復', '下', '空', '容', '量', '不', '足', '以', '上', '必', '要', '開', '始', + '本', '魔', '石', '晶', '結', '大', '紋', '盾', '凍', '透', '燃', '水', '気', '炎', '士', '守', '証', '吹', '闘', '力', '木', '人', '飲', '過', '丸', '騎', '去', '魚', + '剣', '荒', '氏', '杖', '束', '太', '鳥', '導', '布', '風', '満', '約', '源', '出', '実', '海', '思', '王', '自', '名', '喚', '召', '地', '家', '火', '森', '皆', '許', + '恐', '古', '轟', '魂', '臭', '章', '雲', '雪', '然', '達', '嘆', '潰', '瓜', '伝', '怒', '悲', '怖', '鳴', '免', '雷', '林', '様', '巻', '片', '翼', '天', '?', '?', + + /*sys1-2 _*/ + '亡', '者', '囚', '封', '印', '迷', '東', '棟', '西', '冥', '_', '?', '?', '?', '事', '典', '匹', '貓', '姬', '規', '失', '敗', '神', '箱', '兵', '教', '跡', '率', + '組', '造', '図', '情', '多', '報', '分', '由', '立', '具', '星', '質', '流', '替', '役', '優', '雄', '連', '判', '斷', '共', '有', '工', '改', '考', '強', '好', '消', + '求', '捨', '收', '順', '助', '身', '轉', '同', '渡', '錄', '的', '直', '運', '英', '距', '驚', '惠', '系', '効', '鉱', '三', '止', '字', '狀', '心', '振', '絕', '線', + '像', '打', '態', '彈', + }; + + public static readonly char[] _table3 = new char[0x100] + { + '珍', '投', '当', '內', '配', '白', '半', '費', '文', '枚', '在', '通', '特', '択', '動', '備', '武', '類', '覽', '技', '陸', '押', '換', '起', '经', '裝', '種', '殊', + '作', '初', '主', '視', '員', '引', '泳', '活', '寄', '材', '全', '增', '速', '短', '倒', '落', '料', '形', '中', '表', '間', '敵', '來', '丘', '見', '橋', '黑', '原', + '船', '長', '道', '秘', '闇', '数', '变', '化', '時', '高', '型', '段', '食', '登', '場', '賊', '入', '飛', '少', '最', '行', '回', '持', '成', '合', '手', '防', '御', + '擊', '攻', '法', '耐', '久', '一', '度', '低', '超', '操', '縦', '性', '調', '整', '遠', '複', '機', '能', '追', '加','前', '方', '向', '部', '小', '定', '屋', '居', + '隱', '奧', '遺', '暗', '安', '淵', '園', '胃', '江', '館', '犬', '客', '験', '穴', '会', '口', '窟', '広', '宮', '庫', '峽', '研', '究', '棺', '鬼', '計', '巻', '虛', + '基', '果', '球', '界', '交', '休', '街', '議', '群', '岩', '月', '拷', '逆', '獄', '後', '所', '宿', '赤', '私', '室', '斎', '層', '草', '砂', '深', '倉', '集', '敷', + '世', '息', '想', '女', '城', '蔵', '乗', '島', '竹', '滝', '通', '置', '底', '沈', '庭', '潮', '吊', '袋', '腸', '扉', '堂', '洞', '殿', '台', '沼', '難', '浜', '辺', + '壁', '宝', '破', '腹', '氷', '拝', '噴', '崩', '番', '漠', '物', '墓', '密', '門', '默', '問', '遊', '谷', '離', '路', '裏', '綠', '廊', '楼', '牢', '礼', '巨', '樹', + '意', '俺', '降', '壊', '今', '急', '光', '怪', '凶', '舵', '近', '確', '旗', '減', '画', '昇', '青', '左', '色', '捜', '先', '勝', '選', '受', '次', '丈', '点', '注', + '他', '知', '体', '駄', + }; + + public static readonly char[] _table4 = new char[0x100] + { + '何', '吞', '返', '夫', '僕', '暴', '面', '目', '無', '待', '戾', '認', '電', '切', '完', '了', '保', '既', '可', '込', + '決', '新', '差', '商', '設', '削', '除', '獣', '值', '抜', '仲', '品', '未', '読', '野', '右', '素', '個', '移', '位', '憶', '器', '更', '記', '期', '観', '級', '現', + + /*sys2-1*/ + '外', '送', '梱', '包', '謁', '礎', '格', '納', '盤', '信', '枢', '港', '号', '死', '山', '薬', '砦', '市', '店', '護', '住', '処', '廃', '墟', '試', '練', '埋', '営', + '関', '村', '尾', '根', '頂', '場', '玉', '座', '_', '_', '_', '_', '_', '_', 'α', 'β', 'γ', '二', '四', '五', '六', '七', '几', '九', '零', '壱', '弐', '參', + '百', '式', '號', '駅', '宅', '町', '車', '塔', '階', '絡', '狭', '象', '存', '渓', '建', '製', '施', '忘', '却', '桟', '停', '泊', '摩', '災', '床', '滅', '危', '波', + '岸', '話', '再', '彼', '姿', '聞', '言', '戦', '隊', '着', '国', '進', '訪', '服', '救', '残', '帰', '元', '呪', '幻', '得', '逃', '解', '貸', '金', '囲', '探', '脱', + '協', '男', '到', '帝', '都', '相', '頼', '盗', '影', '代', '宰', '緒', '皇', '血', '妃', '任', '放', '誰', '財', '務', '捕', '声', '終', '越', '閉', '聾', '平', '杯', + '窓', '騒', '音', '償', 'ヶ', '準', '葉', '告', '父', '喜', '習', '々', '犯', '勢', '吉', '違', '真', '胸', '険', '催', '途', '日', '届', '友', '狙', '和', '突', '奪', + '軍', '棒', '娘', '抱', '景', '異', '恋', '応', '映', '略', '支', '顔', '命', '別', '呼', '散', '叩', '感', '提', '揺', '荷', '挑', '駆', '談', '銅', '味', '慣', '謎', + '笑', '博', '輝', '畑', '宣', '況', '泥', '早', '演', '悩', '川', '弱', + }; + + public static readonly char[] _table5 = new char[0x100] + { + '係', '資', '張', '希', '非', '如', '迎', '軽', '暮', '派', '旅', '責', '督', '退', '縮', '頭', + '謝', '貸', '曲', '偉', '並', '借', '継', '絵', '伏', '治', '功', '与', '楽', '望', '殺', '親', '美', '勇', '染', '眠', '走', '契', '隙', '拠', '討', '拒', '族', '歌', + '良', '夢', '預', '覚', '価', '予', '争', '惑', '儀', '贈', '為', '崖', '周', '板', '穏', '割', '術', '誕', '伐', '疲', '渉', '狼', '互', '煙', '夜', '因', '爆', '嵐', + '漂', '固', '砲', '側', '看', '抑', '絆', '末', '紡', '誘', '歪', '貫', '柱', '黄', '昏', '旋', '律', '辿', '混', '沌', '創', '祭', '壇', '精', '妖', '買', '吸', '克', + '歴', '歩', '北', '対', '司', '夏', '静', '帯', '師', '反', '総', '限', '売', '利', '秒', '写', '響', '委', '両', '角', '列', '菓', '補', '販', '各', '桜', '虹', '紫', + '年', '禁', '灰', '超', '矢', '触', '重', '十', + '充', '仮', '霊', '幽', '拾', '符', '冒', '第', '件', '背', '綱', '負', '花', '匠', '標', '頑', '射', '紙', '柄', '制', + '峠', '困', '範', '陣', '君', '倍', '奏', '躍', '昨', '簡', '網', '願', '奇', '便', '浮', '郊', '単', '侵', '域', '叫', '詳', '索', '検', '渦', '幕', '官', '項', '念', + + /*sys2-2*/ + '還', '題', '没', '描', '令', '→', '←', '♪', '艦', '聖', '竜', '龍', '忍', '苦', '評', '仕', '賢', '距', '&', '採', '鉱', '坑', '積', '夕', '錬', '斬', '霸', '一', + '編', '語', '条', '理', '拡', '央', '冠', '極', '烈', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', + 't', 'u', 'v', 'w', 'x', 'y', 'z', '欲', '兜', '限', '臨', '威', '乱', '故', '郷', '悪' + }; + + public static readonly char[] _table6 = new char[0x100] + { + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + }; + + public static readonly char[] _table7 = new char[0x40] + { + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', + }; + + public static readonly char[] _table8 = new char[0] + { + }; + } +} diff --git a/OpenKh.Kh2/Messages/Internals/JapaneseSystemEncode.cs b/OpenKh.Kh2/Messages/Internals/JapaneseSystemEncode.cs new file mode 100644 index 000000000..dd2471013 --- /dev/null +++ b/OpenKh.Kh2/Messages/Internals/JapaneseSystemEncode.cs @@ -0,0 +1,148 @@ +using OpenKh.Common.Exceptions; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace OpenKh.Kh2.Messages.Internals +{ + internal class JapaneseSystemEncode : IMessageEncode + { + private static readonly Dictionary> _tableCommands = + JapaneseSystemDecode._table + .Where(x => x.Value != null && x.Value.Command != MessageCommand.PrintText) + .GroupBy(x => x.Value.Command) + .ToDictionary(x => x.Key, x => x.First()); + + private static readonly Dictionary _tableCharacters = + GenerateCharacterDictionary(); + + private static readonly Dictionary _tableComplex = + JapaneseSystemDecode._table + .Where(x => x.Value?.Command == MessageCommand.PrintComplex) + .Select(x => new + { + Key = x.Value.Text, + Value = new byte[] { x.Key } + }) + .Concat(new[] + { + new { Key = "XIII", Value = new byte[] { 0x19, 0xb2 } }, + new { Key = "I", Value = new byte[] { 0x1b, 0x54 } }, + new { Key = "II", Value = new byte[] { 0x1b, 0x55 } }, + new { Key = "IV", Value = new byte[] { 0x1b, 0x56 } }, + new { Key = "V", Value = new byte[] { 0x1b, 0x57 } }, + new { Key = "VI", Value = new byte[] { 0x1b, 0x58 } }, + new { Key = "IX", Value = new byte[] { 0x1b, 0x59 } }, + }) + .ToDictionary(x => x.Key, x => x.Value); + + private void AppendEncodedMessageCommand(List list, MessageCommandModel messageCommand) + { + if (messageCommand.Command == MessageCommand.PrintText) + AppendEncodedText(list, messageCommand.Text); + else if (messageCommand.Command == MessageCommand.PrintComplex) + AppendEncodedComplex(list, messageCommand.Text); + else if (messageCommand.Command == MessageCommand.Unsupported) + list.AddRange(messageCommand.Data); + else + AppendEncodedCommand(list, messageCommand.Command, messageCommand.Data); + } + + private void AppendEncodedCommand(List list, MessageCommand command, byte[] data) + { + if (!_tableCommands.TryGetValue(command, out var pair)) + throw new ArgumentException($"The command {command} it is not supported by the specified encoding."); + + list.Add(pair.Key); + for (var i = 0; i < pair.Value.Length; i++) + list.Add(data[i]); + } + + private void AppendEncodedText(List list, string text) + { + foreach (var ch in text) + AppendEncodedChar(list, ch); + } + + private void AppendEncodedComplex(List list, string text) + { + if (!_tableComplex.TryGetValue(text, out var data)) + throw new ParseException(text, 0, "Complex text does not exists"); + + list.AddRange(data); + } + + private void AppendEncodedChar(List list, char ch) + { + if (!_tableCharacters.TryGetValue(ch, out var data)) + throw new CharacterNotSupportedException(ch); + + if (data.Item1 != 0) + list.Add(data.Item1); + list.Add(data.Item2); + } + + public byte[] Encode(List messageCommands) + { + var list = new List(100); + foreach (var model in messageCommands) + AppendEncodedMessageCommand(list, model); + + return list.ToArray(); + } + + private static Dictionary GenerateCharacterDictionary() + { + var pairs = GenerateCharacterKeyValuePair(MessageCommand.PrintText) + .Concat(GenerateCharacterKeyValuePairFromTable(MessageCommand.Table2, JapaneseTable._table2)) + .Concat(GenerateCharacterKeyValuePairFromTable(MessageCommand.Table3, JapaneseTable._table3)) + .Concat(GenerateCharacterKeyValuePairFromTable(MessageCommand.Table4, JapaneseTable._table4)) + .Concat(GenerateCharacterKeyValuePairFromTable(MessageCommand.Table5, JapaneseTable._table5)) + .Concat(GenerateCharacterKeyValuePairFromTable(MessageCommand.Table6, JapaneseTable._table6)) + .Concat(GenerateCharacterKeyValuePairFromTable(MessageCommand.Table7, JapaneseTable._table7)) + .Concat(GenerateCharacterKeyValuePairFromTable(MessageCommand.Table8, JapaneseTable._table8)); + +#if DEBUG + var stringBuilder = new StringBuilder(); + foreach (var item in pairs + .GroupBy(x => x.Key) + .Where(x => x.Count() > 1)) + { + var ch = item.Key; + var data1 = item.First().Value; + var data2 = item.Skip(1).First().Value; + + stringBuilder.AppendLine($"Character '{ch}' ({data1.Item1:X02} {data1.Item2:X02}) is duplicate (with {data2.Item1:X02} {data2.Item2:X02})."); + } + + Debug.WriteLine(stringBuilder); +#endif + + return pairs + .GroupBy(x => x.Key) + .ToDictionary(x => x.Key, x => x.First().Value); + } + + private static IEnumerable> GenerateCharacterKeyValuePair( + MessageCommand messageCommand) => + JapaneseSystemDecode._table + .Where(x => x.Value?.Command == messageCommand) + .Select(x => new KeyValuePair(x.Value.Text[0], (0, x.Key))); + + private static IEnumerable> GenerateCharacterKeyValuePairFromTable( + MessageCommand messageCommand, char[] table) => + JapaneseSystemDecode._table + .Where(x => x.Value?.Command == messageCommand) + .Select(x => table.Select((ch, i) => new + { + TableId = x.Key, + Character = ch, + Data = (byte)i + })) + .SelectMany(x => x) + .Where(x => x.Character != '_' && x.Character != '?') + .Select(x => new KeyValuePair(x.Character, (x.TableId, x.Data))); + } +} diff --git a/OpenKh.Kh2/Messages/Internals/TableCmdModel.cs b/OpenKh.Kh2/Messages/Internals/TableCmdModel.cs new file mode 100644 index 000000000..399a15d09 --- /dev/null +++ b/OpenKh.Kh2/Messages/Internals/TableCmdModel.cs @@ -0,0 +1,15 @@ +namespace OpenKh.Kh2.Messages.Internals +{ + internal class TableCmdModel : BaseCmdModel + { + private readonly char[] _table; + + public TableCmdModel(MessageCommand messageCommand, char[] table) + { + Command = messageCommand; + _table = table; + } + + public string GetText(byte data) => $"{_table[data]}"; + } +} diff --git a/OpenKh.Kh2/Messages/MessageCommand.cs b/OpenKh.Kh2/Messages/MessageCommand.cs index c779ae5f3..1e49a9a06 100644 --- a/OpenKh.Kh2/Messages/MessageCommand.cs +++ b/OpenKh.Kh2/Messages/MessageCommand.cs @@ -29,13 +29,13 @@ public enum MessageCommand Unknown16, DelayAndFade, Unknown18, - Unknown19, - Unknown1a, - Unknown1b, - Unknown1c, - Unknown1d, - Unknown1e, - Unknown1f, + Table2, + Table3, + Table4, + Table5, + Table6, + Table7, + Table8, Unsupported, } } diff --git a/OpenKh.Kh2/Messages/MsgSerializer.cs b/OpenKh.Kh2/Messages/MsgSerializer.cs index 943a15815..6c3aa1558 100644 --- a/OpenKh.Kh2/Messages/MsgSerializer.cs +++ b/OpenKh.Kh2/Messages/MsgSerializer.cs @@ -201,50 +201,50 @@ private class SerializerModel }, new SerializerModel { - Name = "unk19", - Command = MessageCommand.Unknown19, + Name = "t2", + Command = MessageCommand.Table2, Serializer = x => ToStringRawData(x.Data), Deserializer = x => FromStringToByte(x) }, new SerializerModel { - Name = "unk1a", - Command = MessageCommand.Unknown1a, + Name = "t3", + Command = MessageCommand.Table3, Serializer = x => ToStringRawData(x.Data), Deserializer = x => FromStringToByte(x) }, new SerializerModel { - Name = "unk1b", - Command = MessageCommand.Unknown1b, + Name = "t4", + Command = MessageCommand.Table4, Serializer = x => ToStringRawData(x.Data), Deserializer = x => FromStringToByte(x) }, new SerializerModel { - Name = "unk1c", - Command = MessageCommand.Unknown1c, + Name = "t5", + Command = MessageCommand.Table5, Serializer = x => ToStringRawData(x.Data), Deserializer = x => FromStringToByte(x) }, new SerializerModel { - Name = "unk1d", - Command = MessageCommand.Unknown1d, + Name = "t6", + Command = MessageCommand.Table6, Serializer = x => ToStringRawData(x.Data), Deserializer = x => FromStringToByte(x) }, new SerializerModel { - Name = "unk1e", - Command = MessageCommand.Unknown1e, + Name = "t7", + Command = MessageCommand.Table7, Serializer = x => ToStringRawData(x.Data), Deserializer = x => FromStringToByte(x) }, new SerializerModel { - Name = "unk1f", - Command = MessageCommand.Unknown1f, + Name = "t8", + Command = MessageCommand.Table8, Serializer = x => ToStringRawData(x.Data), Deserializer = x => FromStringToByte(x) }, diff --git a/OpenKh.Kh2/ModelTexture.cs b/OpenKh.Kh2/ModelTexture.cs new file mode 100644 index 000000000..c6f86afea --- /dev/null +++ b/OpenKh.Kh2/ModelTexture.cs @@ -0,0 +1,440 @@ +using OpenKh.Common; +using OpenKh.Imaging; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using Xe.BinaryMapper; + +namespace OpenKh.Kh2 +{ + public class ModelTexture + { + private class Header + { + [Data] public int MagicCode { get; set; } + [Data] public int ColorCount { get; set; } + [Data] public int TextureInfoCount { get; set; } + [Data] public int GsInfoCount { get; set; } + [Data] public int Offset1 { get; set; } + [Data] public int Texinf1off { get; set; } + [Data] public int Texinf2off { get; set; } + [Data] public int PictureOffset { get; set; } + [Data] public int PaletteOffset { get; set; } + } + + public class Texture : IImageRead + { + private readonly byte[] _data; + private readonly byte[] _palette; + private readonly int _csp; + private readonly int _csa; + + public Texture(int width, int height, PixelFormat pixelFormat, byte[] data, byte[] palette, int csp, int csa) + { + Size = new Size(width, height); + PixelFormat = pixelFormat; + _data = data; + _palette = palette; + _csp = csp; + _csa = csa; + } + + public Size Size { get; } + + public PixelFormat PixelFormat { get; } + + public byte[] GetClut() + { + switch (PixelFormat) + { + case PixelFormat.Indexed8: return GetClut8(_palette, _csp, _csa); + case PixelFormat.Indexed4: return GetClut4(_palette, _csp, _csa); + default: + throw new NotSupportedException($"The format {PixelFormat} is not supported or does not contain any palette."); + } + } + + public byte[] GetData() + { + switch (PixelFormat) + { + case PixelFormat.Rgba8888: + case PixelFormat.Rgbx8888: + return GetData32bpp(_data); + case PixelFormat.Indexed8: + return Ps2.Decode8(Ps2.Encode32(_data, Size.Width / 128, Size.Height / 64), Size.Width / 128, Size.Height / 64); + case PixelFormat.Indexed4: + return Ps2.Decode4(Ps2.Encode32(_data, Size.Width / 128, Size.Height / 128), Size.Width / 128, Size.Height / 128); + default: + throw new NotSupportedException($"The format {PixelFormat} is not supported."); + } + } + } + + private class TextureInfo + { + public int Data24 { get; set; } + public int Data28 { get; set; } + public int Data40 { get; set; } + public int Data44 { get; set; } + public int Data60 { get; set; } + public int Data70 { get; set; } + public int PictureOffset { get; set; } + public int Data7c { get; set; } + public int Data80 { get; set; } + } + + private class _TextureInfo + { + [Data] public int Data00 { get; set; } + [Data] public int Data04 { get; set; } + [Data] public int Data08 { get; set; } + [Data] public int Data0c { get; set; } + [Data] public int Data10 { get; set; } + [Data] public int Data14 { get; set; } + [Data] public int Data18 { get; set; } + [Data] public int Data1c { get; set; } + [Data] public int Data20 { get; set; } + [Data] public int Data24 { get; set; } + [Data] public int Data28 { get; set; } + [Data] public int Data2c { get; set; } + [Data] public int Data30 { get; set; } + [Data] public int Data34 { get; set; } + [Data] public int Data38 { get; set; } + [Data] public int Data3c { get; set; } + [Data] public int Data40 { get; set; } + [Data] public int Data44 { get; set; } + [Data] public int Data48 { get; set; } + [Data] public int Data4c { get; set; } + [Data] public int Data50 { get; set; } + [Data] public int Data54 { get; set; } + [Data] public int Data58 { get; set; } + [Data] public int Data5c { get; set; } + [Data] public int Data60 { get; set; } + [Data] public int Data64 { get; set; } + [Data] public int Data68 { get; set; } + [Data] public int Data6c { get; set; } + [Data] public int Data70 { get; set; } + [Data] public int PictureOffset { get; set; } + [Data] public int Data78 { get; set; } + [Data] public int Data7c { get; set; } + [Data] public int Data80 { get; set; } + [Data] public int Data84 { get; set; } + [Data] public int Data88 { get; set; } + [Data] public int Data8c { get; set; } + } + + private class GsInfo + { + public long Data30 { get; set; } + public long Data40 { get; set; } + public long Data50 { get; set; } + public long Data60 { get; set; } + public Tm2.GsTex GsTex0 { get; set; } + public long Data78 { get; set; } + public long Data80 { get; set; } + } + + private class _GsInfo + { + [Data] public long Data00 { get; set; } + [Data] public long Data08 { get; set; } + [Data] public long Data10 { get; set; } + [Data] public long Data18 { get; set; } + [Data] public long Data20 { get; set; } + [Data] public long Data28 { get; set; } + [Data] public long Data30 { get; set; } + [Data] public long Data38 { get; set; } + [Data] public long Data40 { get; set; } + [Data] public long Data48 { get; set; } + [Data] public long Data50 { get; set; } + [Data] public long Data58 { get; set; } + [Data] public long Data60 { get; set; } + [Data] public long Data68 { get; set; } + [Data] public Tm2.GsTex GsTex0 { get; set; } + [Data] public long Data78 { get; set; } + [Data] public long Data80 { get; set; } + [Data] public long Data88 { get; set; } + [Data] public long Data90 { get; set; } + [Data] public long Data98 { get; set; } + } + + private const int MagicCode = 0; + private const int HeaderLength = 0x24; + + private List _textureInfo; + private List _gsInfo; + + public List Images { get; } + + private byte[] OffsetData { get; } + private byte[] PictureData { get; } + private byte[] PaletteData { get; } + private byte[] FooterData { get; } + + private ModelTexture(Stream stream) + { + var header = BinaryMapping.ReadObject
(stream.SetPosition(0)); + if (header.MagicCode == -1) + return; + + var offset1Size = header.Texinf1off - header.Offset1; + var Texinf2offSize = header.PictureOffset - header.Texinf2off; + var pictureSize = header.PaletteOffset - header.PictureOffset; + var paletteSize = header.ColorCount * 4; + var footerSize = (int)stream.Length - (header.PaletteOffset + paletteSize); + if (footerSize < 0) + throw new NotFiniteNumberException("Invalid texture"); + + stream.Position = header.Offset1; + OffsetData = stream.ReadBytes(header.GsInfoCount); + + stream.Position = header.Texinf1off; + _textureInfo = Enumerable.Range(0, header.TextureInfoCount + 1) + .Select(_ => BinaryMapping.ReadObject<_TextureInfo>(stream)) + .Select(x => new TextureInfo + { + Data24 = x.Data24, + Data28 = x.Data28, + Data40 = x.Data40, + Data44 = x.Data44, + Data60 = x.Data60, + Data70 = x.Data70, + PictureOffset = x.PictureOffset, + Data7c = x.Data7c, + Data80 = x.Data80 + }) + .ToList(); + + stream.Position = header.Texinf2off; + _gsInfo = Enumerable.Range(0, header.GsInfoCount) + .Select(_ => BinaryMapping.ReadObject<_GsInfo>(stream)) + .Select(x => new GsInfo + { + Data30 = x.Data30, + Data40 = x.Data40, + Data50 = x.Data50, + Data60 = x.Data60, + GsTex0 = x.GsTex0, + Data78 = x.Data78, + Data80 = x.Data80 + }) + .ToList(); + + stream.Position = header.PictureOffset; + PictureData = stream.ReadBytes(pictureSize); + + stream.Position = header.PaletteOffset; + PaletteData = stream.ReadBytes(paletteSize); + + FooterData = stream.ReadBytes(footerSize); + + // TODO HACK Dirty hack to calculate what the base palette offset for CBP is. + // Probably the offset is already located in TextureInfo, but I could not + // yet find the value... + var paletteBaseOffset = _gsInfo.Min(x => x.GsTex0.CBP); + + Images = new List(); + for (var i = 0; i < header.GsInfoCount; i++) + { + var texInfo = _textureInfo[OffsetData[i] + 1]; + var gsTex = _gsInfo[i].GsTex0; + + var width = 1 << gsTex.TW; + var height = 1 << gsTex.TH; + var pixelFormat = GetPixelFormat(gsTex.PSM); + var dataLength = width * height / (pixelFormat == PixelFormat.Indexed4 ? 2 : 1); + var data = stream.SetPosition(texInfo.PictureOffset).ReadBytes(dataLength); + + Images.Add(new Texture(width, height, pixelFormat, data, PaletteData, gsTex.CBP - paletteBaseOffset, gsTex.CSA)); + } + } + + public void Write(Stream stream) + { + stream.Position = HeaderLength; + stream.Write(OffsetData); + stream.AlignPosition(0x10); + + var texInfo1Offset = (int)stream.Position; + foreach (var textureInfo in _textureInfo) + BinaryMapping.WriteObject(stream, new _TextureInfo + { + Data00 = 0x10000006, + Data04 = 0x00000000, + Data08 = 0x13000000, + Data0c = 0x50000006, + Data10 = 0x00000004, + Data14 = 0x10000000, + Data18 = 0x0000000e, + Data1c = 0x00000000, + Data20 = 0x00000000, + Data24 = textureInfo.Data24, + Data28 = textureInfo.Data28, + Data2c = 0x00000000, + Data30 = 0x00000000, + Data34 = 0x00000000, + Data38 = 0x00000051, + Data3c = 0x00000000, + Data40 = textureInfo.Data40, + Data44 = textureInfo.Data44, + Data48 = 0x00000052, + Data4c = 0x00000000, + Data50 = 0x00000000, + Data54 = 0x00000000, + Data58 = 0x00000053, + Data5c = 0x00000000, + Data60 = textureInfo.Data60, + Data64 = 0x08000000, + Data68 = 0x00000000, + Data6c = 0x00000000, + Data70 = textureInfo.Data70, + PictureOffset = textureInfo.PictureOffset, + Data78 = 0x00000000, + Data7c = textureInfo.Data7c, + Data80 = textureInfo.Data80, + Data84 = 0x00000000, + Data88 = 0x13000000, + Data8c = 0x00000000, + }); + + var texInfo2Offset = (int)stream.Position; + foreach (var textureInfo in _gsInfo) + BinaryMapping.WriteObject(stream, new _GsInfo + { + Data00 = 0x0000000010000008, + Data08 = 0x5000000813000000, + Data10 = 0x1000000000008007, + Data18 = 0x000000000000000e, + Data20 = 0x0000000000000000, + Data28 = 0x000000000000003f, + Data30 = textureInfo.Data30, + Data38 = 0x0000000000000034, + Data40 = textureInfo.Data40, + Data48 = 0x0000000000000036, + Data50 = textureInfo.Data50, + Data58 = 0x0000000000000016, + Data60 = textureInfo.Data60, + Data68 = 0x0000000000000014, + GsTex0 = textureInfo.GsTex0, + Data78 = textureInfo.Data78, + Data80 = textureInfo.Data80, + Data88 = 0x0000000000000008, + Data90 = 0x0000000060000000, + Data98 = 0x0000000013000000, + }); + + stream.AlignPosition(0x80); + + var pictureOffset = (int)stream.Position; + stream.Write(PictureData); + + var paletteOffset = (int)stream.Position; + stream.Write(PaletteData); + + stream.Write(FooterData); + + var writer = new BinaryWriter(stream.SetPosition(0)); + writer.Write(MagicCode); + writer.Write(PaletteData.Length / 4); + writer.Write(_textureInfo.Count - 1); + writer.Write(_gsInfo.Count); + writer.Write(HeaderLength); + writer.Write(texInfo1Offset); + writer.Write(texInfo2Offset); + writer.Write(pictureOffset); + writer.Write(paletteOffset); + } + + private static PixelFormat GetPixelFormat(Tm2.GsPSM psm) + { + switch (psm) + { + case Tm2.GsPSM.GS_PSMT8: return PixelFormat.Indexed8; + case Tm2.GsPSM.GS_PSMT4: return PixelFormat.Indexed4; + default: + throw new NotSupportedException($"GsPSM format {psm} not supported"); + } + } + + private static byte[] GetData32bpp(byte[] data) + { + var newData = new byte[data.Length]; + for (var i = 0; i < newData.Length - 3; i += 4) + { + newData[i + 0] = data[i + 2]; + newData[i + 1] = data[i + 1]; + newData[i + 2] = data[i + 0]; + newData[i + 3] = Ps2.FromPs2Alpha(data[i + 3]); + } + + return newData; + } + + private static byte[] GetClut4(byte[] clut, int cbp, int csa) + { + var data = new byte[16 * 4]; + for (var i = 0; i < 16; i++) + { + var srcIndex = GetClutPointer(i, cbp, csa); + + data[i * 4 + 0] = clut[srcIndex * 4 + 0]; + data[i * 4 + 1] = clut[srcIndex * 4 + 1]; + data[i * 4 + 2] = clut[srcIndex * 4 + 2]; + data[i * 4 + 3] = Ps2.FromPs2Alpha(clut[srcIndex * 4 + 3]); + } + + return data; + } + + private static byte[] GetClut8(byte[] clut, int cbp, int csa) + { + var data = new byte[256 * 4]; + for (var i = 0; i < 256; i++) + { + var srcIndex = GetClutPointer(i, cbp, csa); + + data[i * 4 + 0] = clut[srcIndex * 4 + 0]; + data[i * 4 + 1] = clut[srcIndex * 4 + 1]; + data[i * 4 + 2] = clut[srcIndex * 4 + 2]; + data[i * 4 + 3] = Ps2.FromPs2Alpha(clut[srcIndex * 4 + 3]); + } + + return data; + } + + public static ModelTexture Read(Stream stream) => + new ModelTexture(stream); + + public static bool IsValid(Stream stream) + { + if (stream.Length < HeaderLength) + return false; + + var header = BinaryMapping.ReadObject
(stream.SetPosition(0)); + if (header.MagicCode != MagicCode) + return false; + + var streamLength = stream.Length; + if (header.Offset1 > streamLength || + header.Texinf1off > streamLength || + header.Texinf2off > streamLength || + header.PictureOffset > streamLength || + header.PaletteOffset > streamLength || + header.TextureInfoCount <= 0 || + header.GsInfoCount <= 0) + return false; + + return true; + } + + public static int GetClutPointer(int index, int cbp, int csa) + { + return (index & 7) + (index & 8) * 8 + (index & 16) / 2 + (index & ~31) * 4 + + (cbp & 7) * 0x4 + (cbp & 8) * 0x80 + (cbp & 16) * 0x2 + +(cbp & ~31) * 0x40 + + (csa & 1) * 0x8 + (csa & 14) * 0x40; + } + } +} diff --git a/OpenKh.Kh2/Msg.cs b/OpenKh.Kh2/Msg.cs index 2460d4c03..7155125af 100644 --- a/OpenKh.Kh2/Msg.cs +++ b/OpenKh.Kh2/Msg.cs @@ -9,6 +9,7 @@ public class Msg { private static uint MagicCode = 1; private static byte Terminator = 0; + public static ushort FallbackMessage = 2780; public class Entry { diff --git a/OpenKh.Kh2/Objentry.cs b/OpenKh.Kh2/Objentry.cs new file mode 100644 index 000000000..8e5988699 --- /dev/null +++ b/OpenKh.Kh2/Objentry.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xe.BinaryMapper; + +namespace OpenKh.Kh2 +{ + public class Objentry + { + public enum Type : byte + { + Player = 0x0, + PartyMember = 0x1, + Dummy = 0x2, + Boss = 0x3, + NormalEnemy = 0x4, + Keyblade = 0x5, + Placeholders = 0x6, //??? + WorldSavePoint = 0x7, + Neutral = 0x8, + OutOfPartyPartner = 0x9, + Chest = 0xA, + Moogle = 0xB, + GiantBoss = 0xC, + Unknown1 = 0xD, + PauseMenuDummy = 0xE, + NPC = 0xF, + Unknown2 = 0x10, + WorldMapObject = 0x11, + DropPrize = 0x12, + Summon = 0x13, + ShopPoint = 0x14, + NormalEnemy2 = 0x15, + CrowdSpawner = 0x16, + Unknown3 = 0x17, //pots in hercules world? + } + [Data] public ushort ObjectId { get; set; } + [Data] public ushort Unknown02 { get; set; } + [Data] public Type ObjectType { get; set; } + [Data] public byte Unknown05{ get; set; } + [Data] public byte Unknown06 { get; set; } + [Data] public byte WeaponJoint { get; set; } + [Data(Count = 32)] public string ModelName { get; set; } + [Data(Count = 32)] public string AnimationName { get; set; } + [Data] public uint Unknown48 { get; set; } + [Data] public ushort NeoStatus { get; set; } + [Data] public ushort NeoMoveset { get; set; } + [Data] public uint Unknown50 { get; set; } + [Data] public byte SpawnLimiter { get; set; } + [Data] public byte Unknown55 { get; set; } + [Data] public byte Unknown56{ get; set; } + [Data] public byte Unknown57{ get; set; } + [Data] public ushort SpawnObject1 { get; set; } + [Data] public ushort SpawnObject2 { get; set; } + [Data] public ushort SpawnObject3 { get; set; } + [Data] public ushort Unknown5e { get; set; } + + public static BaseTable Read(Stream stream) => BaseTable.Read(stream); + public static void Write(Stream stream, IEnumerable entries) => + BaseTable.Write(stream, 3, entries.ToList()); + } +} diff --git a/OpenKh.Kh2/OpenKh.Kh2.csproj b/OpenKh.Kh2/OpenKh.Kh2.csproj index ef1c5eb8f..bf467ca44 100644 --- a/OpenKh.Kh2/OpenKh.Kh2.csproj +++ b/OpenKh.Kh2/OpenKh.Kh2.csproj @@ -6,7 +6,7 @@ - + diff --git a/OpenKh.Kh2/System/BaseSystem.cs b/OpenKh.Kh2/System/BaseSystem.cs new file mode 100644 index 000000000..9f77c1792 --- /dev/null +++ b/OpenKh.Kh2/System/BaseSystem.cs @@ -0,0 +1,20 @@ +using OpenKh.Common; +using System.Collections.Generic; +using System.IO; +using Xe.BinaryMapper; + +namespace OpenKh.Kh2.System +{ + public class BaseSystem + { + [Data] public short Id { get; set; } + [Data] public short Count { get => (short)Items.TryGetCount(); set => Items = Items.CreateOrResize(value); } + [Data] public List Items { get; set; } + + static BaseSystem() => BinaryMapping.SetMemberLengthMapping>(nameof(Items), (o, m) => o.Count); + + public static BaseSystem Read(Stream stream) => BinaryMapping.ReadObject>(stream.SetPosition(0)); + + public void Write(Stream stream) => BinaryMapping.WriteObject(stream, this); + } +} \ No newline at end of file diff --git a/OpenKh.Kh2/System/Item.cs b/OpenKh.Kh2/System/Item.cs new file mode 100644 index 000000000..eb3bb05e0 --- /dev/null +++ b/OpenKh.Kh2/System/Item.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using System.IO; +using Xe.BinaryMapper; + +namespace OpenKh.Kh2.System +{ + public class Item + { + public enum Type : byte + { + Consumable, + Boost, + Keyblade, + Staff, + Shield, + PingWeapon, + AuronWeapon, + BeastWeapon, + JackWeapon, + DummyWeapon, + RikuWeapon, + SimbaWeapon, + JackSparrowWeapon, + TronWeapon, + Armor, + Accessory, + Synthesis, + Recipe, + Magic, + Ability, + Summon, + Form, + Map, + Report, + } + + public enum Rank : byte + { + C, + B, + A, + S + } + + public class Entry + { + [Data] public ushort Id { get; set; } + [Data] public Type Type { get; set; } + [Data] public byte Flag0 { get; set; } + [Data] public byte Flag1 { get; set; } + [Data] public Rank Rank { get; set; } + [Data] public ushort StatEntry { get; set; } + [Data] public ushort Name { get; set; } + [Data] public ushort Description { get; set; } + [Data] public ushort ShopBuy { get; set; } + [Data] public ushort ShopSell { get; set; } + [Data] public ushort Command { get; set; } + [Data] public ushort Slot { get; set; } + [Data] public short Picture { get; set; } + [Data] public byte Icon1 { get; set; } + [Data] public byte Icon2 { get; set; } + } + + public class Stat + { + [Data] public ushort Id { get; set; } + [Data] public ushort Ability { get; set; } + [Data] public byte Attack { get; set; } + [Data] public byte Magic { get; set; } + [Data] public byte Defense { get; set; } + [Data] public byte AbilityPoints { get; set; } + [Data] public byte Unknown08 { get; set; } + [Data] public byte FireResistance { get; set; } + [Data] public byte IceResistance { get; set; } + [Data] public byte LightningResistance { get; set; } + [Data] public byte DarkResistance { get; set; } + [Data] public byte Unknown0d { get; set; } + [Data] public byte GeneralResistance { get; set; } + [Data] public byte Unknown { get; set; } + } + + private class SubItemReader + { + [Data] public int Id { get; set; } + [Data] public int Count { get => Items.TryGetCount(); set => Items = Items.CreateOrResize(value); } + [Data] public List Items { get; set; } + + static SubItemReader() => BinaryMapping.SetMemberLengthMapping>(nameof(Items), (o, m) => o.Count); + + public static SubItemReader Read(Stream stream) => BinaryMapping.ReadObject>(stream); + + public void Write(Stream stream) => BinaryMapping.WriteObject(stream, this); + } + + [Data] public List Items1 { get; set; } + [Data] public List Items2 { get; set; } + + public static Item Read(Stream stream) + { + stream.Position = 0; + var one = SubItemReader.Read(stream); + var two = SubItemReader.Read(stream); + + return new Item + { + Items1 = one.Items, + Items2 = two.Items + }; + } + + public void Write(Stream stream) + { + new SubItemReader + { + Id = 6, + Items = Items1 + }.Write(stream); + + new SubItemReader + { + Id = 0, + Items = Items2 + }.Write(stream); + } + } +} diff --git a/OpenKh.Kh2/System/Trsr.cs b/OpenKh.Kh2/System/Trsr.cs new file mode 100644 index 000000000..b656be4aa --- /dev/null +++ b/OpenKh.Kh2/System/Trsr.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xe.BinaryMapper; + +namespace OpenKh.Kh2.System +{ + public class Trsr + { + public const short MagicHeader = 3; + + public enum TrsrType : byte + { + Chest, + Event + } + + [Data] public ushort Id { get; set; } + [Data] public ushort ItemId { get; set; } + [Data] public TrsrType Type { get; set; } + [Data] public byte World { get; set; } + [Data] public byte Room { get; set; } + [Data] public byte RoomChestIndex { get; set; } + [Data] public short EventId { get; set; } + [Data] public short OverallChestIndex { get; set; } + + public static List Read(Stream stream) => BaseSystem.Read(stream).Items; + + public static void Write(Stream stream, IEnumerable items) => new BaseSystem + { + Id = MagicHeader, + Items = items.ToList() + }.Write(stream); + } +} diff --git a/OpenKh.Kh2/Tm2.cs b/OpenKh.Kh2/Tm2.cs deleted file mode 100644 index f8c926a54..000000000 --- a/OpenKh.Kh2/Tm2.cs +++ /dev/null @@ -1,327 +0,0 @@ -using OpenKh.Imaging; -using System; -using System.IO; - -namespace OpenKh.Kh2 -{ - public class Tm2 - { - private const uint MagicCode = 0x324D4954U; - - private enum GsPSM - { - GS_PSMCT32 = 0, // 32bit RGBA - GS_PSMCT24 = 1, - GS_PSMCT16 = 2, - GS_PSMCT16S = 10, - GS_PSMT8 = 19, - GS_PSMT4 = 20, - GS_PSMT8H = 27, - GS_PSMT4HL = 36, - GS_PSMT4HH = 44, - GS_PSMZ32 = 48, - GS_PSMZ24 = 49, - GS_PSMZ16 = 50, - GS_PSMZ16S = 58, - }; - - public enum GsCPSM - { - GS_PSMCT32 = 0, // 32bit RGBA - GS_PSMCT24 = 1, - GS_PSMCT16 = 2, - GS_PSMCT16S = 10, - } - - private enum IMG_TYPE - { - IT_RGBA = 3, - IT_CLUT4 = 4, - IT_CLUT8 = 5, - }; - - private enum CLT_TYPE - { - CT_A1BGR5 = 1, - CT_XBGR8 = 2, - CT_ABGR8 = 3, - }; - - /// - /// register for image - /// 14 bit, texture buffer base pointer (address / 256) - /// 6 bit, texture buffer width (texels / 64) - /// 6 bit, pixel storage format (0 = 32bit RGBA) - /// 4 bit, width 2^n - /// 4 bit, height 2^n - /// 1 bit, 0 = RGB, 1 = RGBA - /// 2 bit, texture function (0=modulate, 1=decal, 2=hilight, 3=hilight2) - /// 14 bit, CLUT buffer base pointer (address / 256) - /// 4 bit, CLUT storage format - /// 1 bit, storage mode - /// 5 bit, offset - /// 3 bit, load control - /// - /// http://forum.xentax.com/viewtopic.php?f=16&t=4501&start=75 - /// - private struct GsTex0 - { - private long data; - - public int TBP0 - { - get => (int)(data >> 0) & 0x3FFF; - set => data = (data & ~(0x3FFF << 0)) + (value & 0x3FFF); - } - - public int TBW - { - get => (int)(data >> 14) & 0x3F; - set => data = (data & ~(0x3F << 14)) + (value & 0x3F); - } - - public GsPSM PSM - { - get => (GsPSM)((data >> 20) & 0x3F); - set => data = (data & ~(0x3F << 20)) + ((int)value & 0x3F); - } - - public int TW - { - get => (int)(data >> 26) & 0xF; - set => data = (data & ~(0xF << 26)) + (value & 0xF); - } - - public int TH - { - get => (int)(data >> 30) & 0xF; - set => data = (data & ~(0xF << 30)) + (value & 0xF); - } - - public int TCC - { - get => (int)(data >> 34) & 1; - set => data = (data & ~(1 << 34)) + (value & 1); - } - - public int TFX - { - get => (int)(data >> 35) & 3; - set => data = (data & ~(3 << 35)) + (value & 3); - } - - public int CBP - { - get => (int)(data >> 37) & 0x3FFF; - set => data = (data & ~(0x3FFF << 37)) + (value & 0x3FFF); - } - - public GsCPSM CPSM - { - get => (GsCPSM)((data >> 51) & 0xF); - set => data = (data & ~(0xF << 51)) + ((int)value & 0xF); - } - - public int CSM - { - get => (int)(data >> 55) & 1; - set => data = (data & ~(1 << 55)) + (value & 1); - } - - public int CSA - { - get => (int)(data >> 56) & 0x1F; - set => data = (data & ~(0x1F << 56)) + (value & 0x1F); - } - - public int CLD - { - get => (int)(data >> 61) & 7; - set => data = (data & ~(7 << 61)) + (value & 7); - } - - public void Read(BinaryReader reader) - { - data = reader.ReadInt64(); - } - - public void Write(BinaryWriter writer) - { - writer.Write(data); - } - } - - - /// - /// description of image - /// 4 byte, total size - /// 4 byte, palette size - /// 4 byte, image size - /// 2 byte, head size - /// 2 byte, how palettes there are - /// 2 byte, how palettes are used - /// 1 byte, palette format - /// 1 byte, image format - /// 2 byte, width - /// 2 byte, height - /// GsTex0, for two times - /// 4 byte, gsreg - /// 4 byte, gspal - /// - private struct TM2Pic - { - public int size; - public int palSize; - public int imgSize; - public short headSize; - public short howPal; - public short howPalUsed; - public byte palFormat; - public byte imgFormat; - public short width; - public short height; - public GsTex0 gstex1; - public GsTex0 gstex2; - public int gsreg; - public int gspal; - - public PixelFormat ImageFormat - { - get - { - switch (imgFormat) - { - case 2: return PixelFormat.Rgb888; - case 3: return PixelFormat.Rgba8888; - case 4: return PixelFormat.Indexed4; - case 5: return PixelFormat.Indexed8; - default: - throw new ArgumentOutOfRangeException($"imgFormat {imgFormat} invalid or not supported."); - } - } - } - - public int BitsPerPixel - { - get - { - switch (imgFormat) - { - case 2: return 24; - case 3: return 32; - case 4: return 4; - case 5: return 8; - default: - throw new ArgumentOutOfRangeException(nameof(imgFormat), $"{imgFormat} invalid or not supported."); - } - } - } - - public PixelFormat PaletteFormat - { - get - { - switch (palFormat) - { - case 0: return PixelFormat.Undefined; - case 1: return PixelFormat.Rgba1555; - case 2: return PixelFormat.Rgbx8888; - case 3: return PixelFormat.Rgba8888; - default: - throw new ArgumentOutOfRangeException(nameof(palFormat), $"{palFormat} invalid or not supported."); - } - } - } - - public void Read(BinaryReader reader) - { - size = reader.ReadInt32(); - palSize = reader.ReadInt32(); - imgSize = reader.ReadInt32(); - headSize = reader.ReadInt16(); - howPal = reader.ReadInt16(); - howPalUsed = reader.ReadInt16(); - palFormat = reader.ReadByte(); - imgFormat = reader.ReadByte(); - width = reader.ReadInt16(); - height = reader.ReadInt16(); - gstex1.Read(reader); - gstex2.Read(reader); - gsreg = reader.ReadInt32(); - gspal = reader.ReadInt32(); - } - - public void Write(BinaryWriter writer) - { - writer.Write(size); - writer.Write(palSize); - writer.Write(imgSize); - writer.Write(headSize); - writer.Write(howPal); - writer.Write(howPalUsed); - writer.Write(palFormat); - writer.Write(imgFormat); - writer.Write(width); - writer.Write(height); - gstex1.Write(writer); - gstex2.Write(writer); - writer.Write(gsreg); - writer.Write(gspal); - } - }; - - private readonly TM2Pic pic = new TM2Pic(); - private readonly byte[] imgData; - private readonly byte[] palData; - - public Tm2(Stream stream) - { - if (!stream.CanRead || !stream.CanSeek) - throw new InvalidDataException($"Read or seek must be supported."); - - var reader = new BinaryReader(stream); - if (stream.Length < 16L || reader.ReadUInt32() != MagicCode) - throw new InvalidDataException("Invalid header"); - - short version = reader.ReadInt16(); - short imagesCount = reader.ReadInt16(); - int unk08 = reader.ReadInt16(); - int unk0c = reader.ReadInt16(); - pic.Read(reader); - - var imgPos = (int)stream.Position; - var palPos = imgPos = pic.imgSize; - imgData = reader.ReadBytes(pic.imgSize); - palData = reader.ReadBytes(pic.palSize); - } - - private void InvertRedBlueChannels(byte[] data, PixelFormat format) - { - switch (format) - { - case PixelFormat.Rgb888: - for (int i = 0; i < pic.width * pic.height; i++) - { - byte tmp = data[i * 3 + 0]; - data[i * 3 + 0] = data[i * 3 + 2]; - data[i * 3 + 2] = tmp; - } - break; - case PixelFormat.Rgba8888: - for (int i = 0; i < pic.width * pic.height; i++) - { - byte tmp = data[i * 4 + 0]; - data[i * 4 + 0] = data[i * 4 + 2]; - data[i * 4 + 2] = tmp; - } - break; - case PixelFormat.Indexed4: - for (int i = 0; i < pic.width * pic.height / 2; i++) - { - data[i] = (byte)(((data[i] & 0x0F) << 4) | (data[i] >> 4)); - } - break; - } - } - } -} diff --git a/OpenKh.Tests/Bbs/ArcTests.cs b/OpenKh.Tests/Bbs/ArcTests.cs new file mode 100644 index 000000000..fd9703a85 --- /dev/null +++ b/OpenKh.Tests/Bbs/ArcTests.cs @@ -0,0 +1,121 @@ +using OpenKh.Common; +using OpenKh.Bbs; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace OpenKh.Tests.Bbs +{ + public class ArcTests : Common + { + private static readonly string FileName = "Bbs/res/arctest.arc"; + + [Fact] + public void IsValidTest() + { + using (var stream = new MemoryStream()) + { + stream.WriteByte(0x41); + stream.WriteByte(0x52); + stream.WriteByte(0x43); + stream.WriteByte(0x00); + Assert.True(Arc.IsValid(stream)); + } + } + + [Fact] + public void IsNotValidWhenHeaderDoesNotMatchTest() + { + using (var stream = new MemoryStream()) + { + stream.WriteByte(1); + stream.WriteByte(2); + stream.WriteByte(3); + stream.WriteByte(4); + Assert.False(Arc.IsValid(stream)); + } + } + + [Fact] + public void IsNotValidWhenStreamIsNotLongEnoughTest() + { + using (var stream = new MemoryStream()) + { + stream.WriteByte(1); + stream.WriteByte(2); + stream.WriteByte(3); + Assert.False(Arc.IsValid(stream)); + } + } + + [Fact] + public void ReadCorrectAmountOfEntries() => FileOpenRead(FileName, stream => + { + var entries = Arc.Read(stream); + Assert.Equal(3, entries.Count()); + }); + + [Fact] + public void ReadEntryNamesCorrectly() => FileOpenRead(FileName, stream => + { + var entries = Arc.Read(stream).ToArray(); + Assert.Equal("TBoxDtTe.itb", entries[0].Name); + Assert.Equal("ColeDtTe.itc", entries[1].Name); + Assert.Equal("FileNameTest", entries[2].Name); + }); + + [Fact] + public void ReadEntryFullPathCorrectly() => FileOpenRead(FileName, stream => + { + var entries = Arc.Read(stream).ToArray(); + Assert.Equal("TBoxDtTe.itb", entries[0].Path); + Assert.Equal("ColeDtTe.itc", entries[1].Path); + Assert.Equal("arc/effect/FileNameTest", entries[2].Path); + }); + + [Fact] + public void ReadEntryLengthCorrectly() => FileOpenRead(FileName, stream => + { + var entries = Arc.Read(stream).ToArray(); + Assert.Equal(0x3ec, entries[0].Data.Length); + Assert.Equal(0xbc, entries[1].Data.Length); + }); + + [Fact] + public void ReadEntryContentCorrectly() => FileOpenRead(FileName, stream => + { + var entries = Arc.Read(stream).ToArray(); + Assert.Equal(0x42, entries[0].Data[2]); + Assert.Equal(0x43, entries[1].Data[2]); + }); + + [Fact] + public void IsPointerFieldShouldBeCorrectlyPopulated() => FileOpenRead(FileName, stream => + { + var entries = Arc.Read(stream).ToArray(); + Assert.False(entries[0].IsLink); + Assert.False(entries[1].IsLink); + Assert.True(entries[2].IsLink); + }); + + [Fact] + public void PointersShouldHaveNullData() => FileOpenRead(FileName, stream => + { + var entries = Arc.Read(stream).ToArray(); + Assert.Null(entries[2].Data); + }); + + [Fact] + public void WritesBackCorrectly() => FileOpenRead(FileName, stream => + Helpers.AssertStream(stream, x => + { + var entries = Arc.Read(stream); + + var outStream = new MemoryStream(); + entries.Write(outStream); + + return outStream; + })); + } +} diff --git a/OpenKh.Tests/Bbs/BbsaTests.cs b/OpenKh.Tests/Bbs/BbsaTests.cs index 96b0acbc4..65fe2b3f5 100644 --- a/OpenKh.Tests/Bbs/BbsaTests.cs +++ b/OpenKh.Tests/Bbs/BbsaTests.cs @@ -29,5 +29,47 @@ public void TestLba() Assert.Equal(0x333CE - 100, offset); Assert.Equal(0x38C, size); } + + [Theory] + [InlineData(0x0050414D, "arc/map")] + [InlineData(0x4E455645, "arc/event")] + [InlineData(0x30004350, "arc/pc_terra")] + [InlineData(0x00000000, "arc_")] + [InlineData(0x80000000, "sound/bgm")] + [InlineData(0xc0000000, "lua")] + [InlineData(0x90000000, "sound/se/common")] + [InlineData(0x91000000, "sound/se/event/ex")] + [InlineData(0x91010000, "sound/se/event/dp")] + [InlineData(0x920a0000, "sound/se/footstep/di")] + [InlineData(0xd0000000, "message/jp/system")] + [InlineData(0xd1200000, "message/en/map")] + [InlineData(0xa1570000, "sound/voice/fr/event/jf")] + public void CalculateDirectoryFromHash(uint hash, string directory) + { + var actual = Bbsa.GetDirectoryName(hash); + + Assert.Equal(directory, actual); + } + + [Theory] + [InlineData(0x0050414D, "arc/map")] + [InlineData(0x4E455645, "arc/event")] + [InlineData(0x30004350, "arc/pc_terra")] + [InlineData(0x00000000, "arc_")] + [InlineData(0x80000000, "sound/bgm")] + [InlineData(0xc0000000, "lua")] + [InlineData(0x90000000, "sound/se/common")] + [InlineData(0x91000000, "sound/se/event/ex")] + [InlineData(0x91010000, "sound/se/event/dp")] + [InlineData(0x920a0000, "sound/se/footstep/di")] + [InlineData(0xd0000000, "message/jp/system")] + [InlineData(0xd1200000, "message/en/map")] + [InlineData(0xa1570000, "sound/voice/fr/event/jf")] + public void CalculateHashFromDirectory(uint hash, string directory) + { + var actual = Bbsa.GetDirectoryHash(directory); + + Assert.Equal(hash, actual); + } } } diff --git a/OpenKh.Tests/Bbs/EventTableTests.cs b/OpenKh.Tests/Bbs/EventTableTests.cs new file mode 100644 index 000000000..eb871efe7 --- /dev/null +++ b/OpenKh.Tests/Bbs/EventTableTests.cs @@ -0,0 +1,42 @@ +using OpenKh.Bbs; +using System.IO; +using Xunit; + +namespace OpenKh.Tests.Bbs +{ + public class EventTableTests : Common + { + private const string FilePath = "Bbs/res/event-table.bin"; + + [Fact] + public void ReadEntriesCountCorrectly() => FileOpenRead(FilePath, stream => + { + var events = Event.Read(stream); + Assert.Equal(92, events.Count); + }); + + [Fact] + public void ParseSingleEntryCorrectly() => FileOpenRead(FilePath, stream => + { + var events = Event.Read(stream); + var @event = events[22]; + Assert.Equal(534, @event.Id); + Assert.Equal(301, @event.EventIndex); + Assert.Equal(4, @event.World); + Assert.Equal(7, @event.Room); + Assert.Equal(61, @event.Unknown06); + }); + + [Fact] + public void WritesBackCorrectly() => FileOpenRead(FilePath, stream => + Helpers.AssertStream(stream, x => + { + var events = Event.Read(stream); + + var outStream = new MemoryStream(); + Event.Write(outStream, events); + + return outStream; + })); + } +} diff --git a/OpenKh.Tests/Bbs/res/arctest.arc b/OpenKh.Tests/Bbs/res/arctest.arc new file mode 100644 index 000000000..4c75a4ace Binary files /dev/null and b/OpenKh.Tests/Bbs/res/arctest.arc differ diff --git a/OpenKh.Tests/Bbs/res/event-table.bin b/OpenKh.Tests/Bbs/res/event-table.bin new file mode 100644 index 000000000..7de7940ec Binary files /dev/null and b/OpenKh.Tests/Bbs/res/event-table.bin differ diff --git a/OpenKh.Tests/Helpers.cs b/OpenKh.Tests/Helpers.cs index e6d2b3092..1be840f6b 100644 --- a/OpenKh.Tests/Helpers.cs +++ b/OpenKh.Tests/Helpers.cs @@ -21,7 +21,13 @@ public static void AssertStream(Stream expectedStream, Func func var actualData = actualStream.ReadAllBytes(); Assert.Equal(expectedData.Length, actualData.Length); - Assert.Equal(expectedData, actualData); + + for (var i = 0; i < expectedData.Length; i++) + { + var ch1 = expectedData[i]; + var ch2 = actualData[i]; + Assert.True(ch1 == ch2, $"Expected {ch1:X02} but found {ch2:X02} at {i:X}"); + } } public static void UseAsset(string assetName, Action action) => diff --git a/OpenKh.Tests/Imaging/Tm2Tests.cs b/OpenKh.Tests/Imaging/Tm2Tests.cs new file mode 100644 index 000000000..18e8cc1e9 --- /dev/null +++ b/OpenKh.Tests/Imaging/Tm2Tests.cs @@ -0,0 +1,56 @@ +using OpenKh.Common; +using OpenKh.Imaging; +using System.IO; +using System.Linq; +using Xunit; + +namespace OpenKh.Tests.Imaging +{ + public class Tm2Tests + { + [Theory] + [InlineData(new byte[] { 0x54, 0x49, 0x4d, 0x32 }, 16, true)] + [InlineData(new byte[] { 0x54, 0x49, 0x4d, 0x31 }, 16, false)] + [InlineData(new byte[] { 0x54, 0x49, 0x4d, 0x32 }, 15, false)] + public void IsValidTest(byte[] header, int length, bool expected) => new MemoryStream() + .Using(stream => + { + stream.Write(header, 0, header.Length); + stream.SetLength(length); + + Assert.Equal(expected, Tm2.IsValid(stream)); + }); + + [Theory] + [InlineData("image-8bit-128-128", 128, 128, PixelFormat.Indexed8)] + [InlineData("image-8bit-512-272", 512, 272, PixelFormat.Indexed8)] + [InlineData("image-32bit-480-279", 480, 279, PixelFormat.Rgba8888)] + public void ReadImagePropertiesTest( + string fileName, + int width, + int height, + PixelFormat pixelFormat) => File.OpenRead($"Imaging/res/{fileName}.tm2").Using(stream => + { + var image = Tm2.Read(stream).Single(); + + Assert.Equal(width, image.Size.Width); + Assert.Equal(height, image.Size.Height); + Assert.Equal(pixelFormat, image.PixelFormat); + }); + + [Theory] + [InlineData("image-8bit-128-128")] + [InlineData("image-8bit-512-272")] + [InlineData("image-32bit-480-279")] + public void IsWritingBackCorrectly(string fileName) => File.OpenRead($"Imaging/res/{fileName}.tm2").Using(x => + Helpers.AssertStream(x, stream => + { + var images = Tm2.Read(stream); + + var newStream = new MemoryStream(); + Tm2.Write(newStream, images); + + return newStream; + })); + } +} diff --git a/OpenKh.Tests/Imaging/res/image-32bit-480-279.tm2 b/OpenKh.Tests/Imaging/res/image-32bit-480-279.tm2 new file mode 100644 index 000000000..a3110824e Binary files /dev/null and b/OpenKh.Tests/Imaging/res/image-32bit-480-279.tm2 differ diff --git a/OpenKh.Tests/Imaging/res/image-8bit-128-128.tm2 b/OpenKh.Tests/Imaging/res/image-8bit-128-128.tm2 new file mode 100644 index 000000000..51cbe8daa Binary files /dev/null and b/OpenKh.Tests/Imaging/res/image-8bit-128-128.tm2 differ diff --git a/OpenKh.Tests/Imaging/res/image-8bit-512-272.tm2 b/OpenKh.Tests/Imaging/res/image-8bit-512-272.tm2 new file mode 100644 index 000000000..3d63e127e Binary files /dev/null and b/OpenKh.Tests/Imaging/res/image-8bit-512-272.tm2 differ diff --git a/OpenKh.Tests/OpenKh.Tests.csproj b/OpenKh.Tests/OpenKh.Tests.csproj index 203f33d32..9369951fb 100644 --- a/OpenKh.Tests/OpenKh.Tests.csproj +++ b/OpenKh.Tests/OpenKh.Tests.csproj @@ -23,6 +23,24 @@ + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -89,15 +107,38 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest PreserveNewest + + PreserveNewest + PreserveNewest + + + + + diff --git a/OpenKh.Tests/kh2/ImgdTests.cs b/OpenKh.Tests/kh2/ImgdTests.cs index 85a16e25e..3f5373067 100644 --- a/OpenKh.Tests/kh2/ImgdTests.cs +++ b/OpenKh.Tests/kh2/ImgdTests.cs @@ -20,7 +20,6 @@ public void IsValidTest() stream.WriteByte(0x44); stream.Position = 0; Assert.True(Imgd.IsValid(stream)); - Assert.Equal(0, stream.Position); } } diff --git a/OpenKh.Tests/kh2/ImgzTests.cs b/OpenKh.Tests/kh2/ImgzTests.cs index 582ff212a..029a52e11 100644 --- a/OpenKh.Tests/kh2/ImgzTests.cs +++ b/OpenKh.Tests/kh2/ImgzTests.cs @@ -17,7 +17,6 @@ public void IsValidTest() stream.WriteByte(0x5a); stream.Position = 0; Assert.True(Imgz.IsValid(stream)); - Assert.Equal(0, stream.Position); } } } diff --git a/OpenKh.Tests/kh2/MdlxTests.cs b/OpenKh.Tests/kh2/MdlxTests.cs new file mode 100644 index 000000000..cb4b98009 --- /dev/null +++ b/OpenKh.Tests/kh2/MdlxTests.cs @@ -0,0 +1,141 @@ +using OpenKh.Common; +using OpenKh.Kh2; +using System.IO; +using System.Linq; +using Xunit; + +namespace OpenKh.Tests.kh2 +{ + public class MdlxTests + { + private const string FileName = "kh2/res/p_ex.vif"; + private const string MapFileName = "kh2/res/map.vif"; + + [Fact] + public void ShouldReadTheCorrectAmountOfSubModels() => File.OpenRead(FileName).Using(stream => + { + var model = Mdlx.Read(stream).SubModels; + + Assert.Equal(2, model.Count); + }); + + [Fact] + public void ShouldReadVifPackets() => File.OpenRead(FileName).Using(stream => + { + var dmaChain = Mdlx.Read(stream).SubModels[0].DmaChains; + Assert.Equal(26, dmaChain[0].DmaVifs.Count); + Assert.Equal(4, dmaChain.Count); + Assert.Equal(58, dmaChain.Sum(x => x.DmaVifs.Count)); + + var dmaVif = dmaChain[0].DmaVifs[0]; + Assert.Equal(0, dmaVif.TextureIndex); + Assert.Equal(10, dmaVif.Alaxi.Length); + Assert.Equal(53, dmaVif.Alaxi[0]); + Assert.Equal(22, dmaVif.Alaxi[9]); + Assert.Equal(1600, dmaVif.VifPacket.Length); + Assert.Equal(1, dmaVif.VifPacket[0]); + Assert.Equal(248, dmaVif.VifPacket[324]); + }); + + [Fact] + public void ShouldReadBones() => File.OpenRead(FileName).Using(stream => + { + var bones = Mdlx.Read(stream).SubModels[0].Bones; + Assert.Equal(228, bones.Count); + + var bone = bones[0]; + Assert.Equal(0, bone.Index); + Assert.Equal(-1, bone.Parent); + Assert.Equal(0, bone.Unk08); + Assert.Equal(3, bone.Unk0c); + Assert.Equal(1, bone.ScaleX); + Assert.Equal(0, bone.RotationX); + Assert.Equal(0, bone.TranslationX); + Assert.Equal(1, bone.ScaleY); + Assert.Equal(4.71, bone.RotationY, 2); + Assert.Equal(102.62, bone.TranslationY, 2); + Assert.Equal(1, bone.ScaleZ); + Assert.Equal(4.71, bone.RotationZ, 2); + Assert.Equal(0, bone.TranslationZ); + Assert.Equal(0, bone.ScaleW); + Assert.Equal(0, bone.RotationW); + Assert.Equal(0, bone.TranslationW); + }); + + [Fact] + public void ShouldWriteBackTheExactSameFile() => File.OpenRead(FileName).Using(stream => + { + Helpers.AssertStream(stream, inStream => + { + var mdlx = Mdlx.Read(inStream); + + var outStream = new MemoryStream(); + mdlx.Write(outStream); + + return outStream; + }); + }); + + [Fact] + public void ReadAlb1t2Table() => File.OpenRead(MapFileName).Using(stream => + { + var alb1t2 = Mdlx.Read(stream).MapModel.alb1t2; + Assert.Equal(97, alb1t2.Count); + + Assert.Equal(0, alb1t2[0]); + Assert.Equal(1, alb1t2[1]); + Assert.Equal(2, alb1t2[2]); + Assert.Equal(96, alb1t2[96]); + }); + + [Fact] + public void ReadAlb2() => File.OpenRead(MapFileName).Using(stream => + { + var alb2 = Mdlx.Read(stream).MapModel.alb2; + Assert.Equal(9, alb2.Count); + + Assert.Equal(20, alb2[0].Length); + Assert.Equal(1, alb2[0][0]); + Assert.Equal(91, alb2[0][19]); + + Assert.Equal(3, alb2[8].Length); + Assert.Equal(30, alb2[8][0]); + Assert.Equal(68, alb2[8][2]); + }); + + [Fact] + public void ReadVifPackets() => File.OpenRead(MapFileName).Using(stream => + { + var alvifpkt = Mdlx.Read(stream).MapModel.VifPackets; + Assert.Equal(97, alvifpkt.Count); + + var packet1 = alvifpkt[0]; + Assert.Equal(1, packet1.TextureId); + Assert.Equal(1184, packet1.VifPacket.Length); + Assert.Equal(1, packet1.VifPacket[0]); + Assert.Equal(0, packet1.VifPacket[10]); + Assert.Equal(64, packet1.VifPacket[100]); + Assert.Equal(76, packet1.VifPacket[1170]); + + var packet2 = alvifpkt[96]; + Assert.Equal(2, packet2.TextureId); + Assert.Equal(960, packet2.VifPacket.Length); + Assert.Equal(1, packet2.VifPacket[0]); + Assert.Equal(117, packet2.VifPacket[500]); + }); + + [Fact] + public void WriteMapBack() => File.OpenRead(MapFileName).Using(stream => + { + Helpers.AssertStream(stream, inStream => + { + var mdlx = Mdlx.Read(inStream); + + var outStream = new MemoryStream(); + mdlx.Write(outStream); + + return outStream; + }); + }); + } +} diff --git a/OpenKh.Tests/kh2/ModelTextureTests.cs b/OpenKh.Tests/kh2/ModelTextureTests.cs new file mode 100644 index 000000000..5111b4208 --- /dev/null +++ b/OpenKh.Tests/kh2/ModelTextureTests.cs @@ -0,0 +1,155 @@ +using OpenKh.Common; +using OpenKh.Imaging; +using OpenKh.Kh2; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace OpenKh.Tests.kh2 +{ + public class ModelTextureTests + { + private const string FileName1 = "kh2/res/model_texture1.tex"; + private const string FileName2 = "kh2/res/model_texture2.tex"; + + [Fact] + public void IsValidReturnsTrueWhenStreamContainsValidData() => File.OpenRead(FileName1).Using(stream => + { + Assert.True(ModelTexture.IsValid(stream)); + }); + + [Theory] + [InlineData(FileName1, 1)] + [InlineData(FileName2, 3)] + public void ReadCorrectAmountOfTextures(string fileName, int expectedCount) => File.OpenRead(fileName).Using(stream => + { + var modelTexture = ModelTexture.Read(stream); + + Assert.Equal(expectedCount, modelTexture.Images.Count); + }); + + [Fact] + public void CreateImagesWithTheCorrectInformation() => File.OpenRead(FileName2).Using(stream => + { + var modelTexture = ModelTexture.Read(stream); + + var image0 = modelTexture.Images[0]; + Assert.Equal(256, image0.Size.Width); + Assert.Equal(256, image0.Size.Height); + Assert.Equal(PixelFormat.Indexed8, image0.PixelFormat); + Assert.Equal(256 * 256, image0.GetData().Length); + + var image1 = modelTexture.Images[1]; + Assert.Equal(256, image1.Size.Width); + Assert.Equal(256, image1.Size.Height); + Assert.Equal(PixelFormat.Indexed8, image1.PixelFormat); + Assert.Equal(256 * 256, image1.GetData().Length); + + var image2 = modelTexture.Images[2]; + Assert.Equal(128, image2.Size.Width); + Assert.Equal(64, image2.Size.Height); + Assert.Equal(PixelFormat.Indexed8, image2.PixelFormat); + Assert.Equal(128 * 64, image2.GetData().Length); + }); + + [Theory] + [InlineData(FileName1)] + [InlineData(FileName2)] + public void WriteBackTheSameFile(string fileName) => File.OpenRead(fileName).Using(stream => Helpers.AssertStream(stream, inStream => + { + var outStream = new MemoryStream(); + ModelTexture.Read(inStream).Write(outStream); + return outStream; + })); + + [Fact] + public void Read4bitPaletteCorrectly() => File.OpenRead(FileName1).Using(stream => + { + var clut = ModelTexture.Read(stream).Images.First().GetClut(); + + Assert.Equal(87, clut[0]); + Assert.Equal(98, clut[1]); + Assert.Equal(106, clut[2]); + Assert.Equal(255, clut[3]); + + Assert.Equal(95, clut[4]); + Assert.Equal(105, clut[5]); + Assert.Equal(114, clut[6]); + Assert.Equal(255, clut[7]); + + Assert.Equal(108, clut[16]); + Assert.Equal(118, clut[17]); + Assert.Equal(128, clut[18]); + Assert.Equal(255, clut[19]); + + Assert.Equal(134, clut[32]); + Assert.Equal(147, clut[33]); + Assert.Equal(158, clut[34]); + Assert.Equal(255, clut[35]); + }); + + [Fact] + public void Read8bitPaletteCorrectly() => File.OpenRead(FileName2).Using(stream => + { + var images = ModelTexture.Read(stream).Images; + + AssertPalette(images, 0, 0, 0, 0, 0); + AssertPalette(images, 0, 4, 10, 10, 10); + AssertPalette(images, 0, 8, 11, 15, 23); + AssertPalette(images, 0, 16, 23, 19, 29); + AssertPalette(images, 0, 32, 23, 35, 56); + AssertPalette(images, 0, 64, 57, 51, 71); + AssertPalette(images, 0, 128, 153, 96, 7); + + AssertPalette(images, 1, 3, 22, 18, 27); + }); + + [Theory] + [InlineData(0, 0, 0, 0x0)] + [InlineData(1, 0, 0, 0x4)] + [InlineData(2, 0, 0, 0x8)] + [InlineData(4, 0, 0, 0x10)] + [InlineData(7, 0, 0, 0x1c)] + [InlineData(8, 0, 0, 0x100)] + [InlineData(16, 0, 0, 0x20)] + [InlineData(32, 0, 0, 0x200)] + [InlineData(64, 0, 0, 0x400)] + [InlineData(0, 4, 0, 0x40)] + [InlineData(8, 4, 0, 0x140)] + [InlineData(16, 4, 0, 0x60)] + [InlineData(32, 4, 0, 0x240)] + [InlineData(0, 0x08, 0, 0x1000)] + [InlineData(0, 0x08, 1, 0x1020)] + [InlineData(0, 0x08, 2, 0x1200)] + [InlineData(0, 0x08, 3, 0x1220)] + [InlineData(0, 0x08, 4, 0x1400)] + [InlineData(0, 0x08, 8, 0x1800)] + [InlineData(0, 0x0c, 0, 0x1040)] + [InlineData(0, 0x10, 0, 0x80)] + [InlineData(0, 0x20, 0, 0x2000)] + public void PointerTest(int index, int cbp, int csa, int expectedPointer) + { + Assert.Equal(expectedPointer / 4, ModelTexture.GetClutPointer(index, cbp, csa)); + } + + private void AssertPalette(List textures, int imageIndex, int colorIndex, byte r, byte g, byte b) + { + var texture = textures[imageIndex]; + var clut = texture.GetClut(); + + try + { + Assert.Equal(r, clut[colorIndex * 4 + 0]); + Assert.Equal(g, clut[colorIndex * 4 + 1]); + Assert.Equal(b, clut[colorIndex * 4 + 2]); + } + catch + { + Console.WriteLine($"Error for texture {imageIndex}"); + throw; + } + } + } +} diff --git a/OpenKh.Tests/kh2/MsgEncoderTests.cs b/OpenKh.Tests/kh2/MsgEncoderTests.cs index c37e75704..afdec8fa7 100644 --- a/OpenKh.Tests/kh2/MsgEncoderTests.cs +++ b/OpenKh.Tests/kh2/MsgEncoderTests.cs @@ -1,5 +1,6 @@ using OpenKh.Kh2.Messages; using System.Collections.Generic; +using System.Linq; using Xunit; namespace OpenKh.Tests.kh2 @@ -91,13 +92,6 @@ public void DecodeColorCommandCorrectly() [InlineData(0x16, "123456789")] //[InlineData(0x17, "")] [InlineData(0x18, "23456789")] - [InlineData(0x19, "123456789")] - [InlineData(0x1a, "123456789")] - [InlineData(0x1b, "123456789")] - [InlineData(0x1c, "123456789")] - [InlineData(0x1d, "123456789")] - [InlineData(0x1e, "123456789")] - [InlineData(0x1f, "123456789")] public void DecodeTheRightAmountOfCharacters(byte commandId, string expectedText) { var decoded = Encoders.InternationalSystem.Decode(new byte[] @@ -168,13 +162,6 @@ public void SimpleReEncodeTest() [InlineData(0x16)] //[InlineData(0x17)] [InlineData(0x18)] - [InlineData(0x19)] - [InlineData(0x1a)] - [InlineData(0x1b)] - [InlineData(0x1c)] - [InlineData(0x1d)] - [InlineData(0x1e)] - [InlineData(0x1f)] public void AdvancedReEncodeTest(byte commandByte) { var expected = new byte[] @@ -187,5 +174,73 @@ public void AdvancedReEncodeTest(byte commandByte) Assert.Equal(expected, encoded); } + + [Theory] + [InlineData(0x1a, 0x00, '珍')] + [InlineData(0x1b, 0x00, '何')] + [InlineData(0x1c, 0x00, '係')] + [InlineData(0x1e, 0x40, '外')] + [InlineData(0x1f, 0x00, '銅')] + [InlineData(0x1f, 0xc7, '念')] + [InlineData(0x1f, 0xc8, '還')] + [InlineData(0x1f, 0xDF, '夕')] + public void DecodeJapaneseTextCorrectly(byte command, byte data, char expected) + { + var decoded = Encoders.JapaneseSystem.Decode(new byte[] { command, data }); + + Assert.NotEmpty(decoded); + Assert.Equal(expected, decoded.Single().Text.Single()); + } + + [Theory] + [InlineData(0x84, 0x00, "III")] + [InlineData(0x85, 0x00, "VII")] + [InlineData(0x86, 0x00, "VIII")] + [InlineData(0x87, 0x00, "X")] + [InlineData(0x19, 0xb2, "XIII")] + [InlineData(0x1b, 0x54, "I")] + [InlineData(0x1b, 0x55, "II")] + [InlineData(0x1b, 0x56, "IV")] + [InlineData(0x1b, 0x57, "V")] + [InlineData(0x1b, 0x58, "VI")] + [InlineData(0x1b, 0x59, "IX")] + [InlineData(0x1e, 0x66, "IV")] + public void DecodeRomanNumbersFromJapaneseTable(byte command, byte data, string expected) + { + var decoded = Encoders.JapaneseSystem.Decode(new byte[] { command, data }); + + Assert.NotEmpty(decoded); + Assert.Equal(MessageCommand.PrintComplex, decoded.First().Command); + Assert.Equal(expected, decoded.First().Text); + } + + [Theory] + [InlineData(0x84, 0x00, "III")] + [InlineData(0x85, 0x00, "VII")] + [InlineData(0x86, 0x00, "VIII")] + [InlineData(0x87, 0x00, "X")] + [InlineData(0x19, 0xb2, "XIII")] + [InlineData(0x1b, 0x54, "I")] + [InlineData(0x1b, 0x55, "II")] + [InlineData(0x1b, 0x56, "IV")] + [InlineData(0x1b, 0x57, "V")] + [InlineData(0x1b, 0x58, "VI")] + [InlineData(0x1b, 0x59, "IX")] + public void EncodeRomanNumbersForJapaneseTable(byte command, byte data, string textSource) + { + var encoded = Encoders.JapaneseSystem.Encode(new List + { + new MessageCommandModel + { + Command = MessageCommand.PrintComplex, + Text = textSource + } + }); + + if (data == 0) + Assert.Equal(new byte[] { command }, encoded); + else + Assert.Equal(new byte[] { command, data }, encoded); + } } } diff --git a/OpenKh.Tests/kh2/ObjentryTests.cs b/OpenKh.Tests/kh2/ObjentryTests.cs new file mode 100644 index 000000000..5c8ae99fe --- /dev/null +++ b/OpenKh.Tests/kh2/ObjentryTests.cs @@ -0,0 +1,28 @@ +using OpenKh.Kh2; +using System.IO; +using Xunit; + +namespace OpenKh.Tests.kh2 +{ + public class ObjentryTests + { + [Fact] + public void HasRightEntryCount() => Common.FileOpenRead("kh2/res/00objentry.bin", stream => + { + var table = BaseTable.Read(stream); + Assert.Equal(0x076C, table.Count); + }); + + [Fact] + public void WriteBackTheSameFile() => Common.FileOpenRead("kh2/res/00objentry.bin", stream => + { + Helpers.AssertStream(stream, inStream => + { + var outStream = new MemoryStream(); + Objentry.Write(outStream, Objentry.Read(inStream)); + + return outStream; + }); + }); + } +} diff --git a/OpenKh.Tests/kh2/SystemItemTests.cs b/OpenKh.Tests/kh2/SystemItemTests.cs new file mode 100644 index 000000000..c13d97c1c --- /dev/null +++ b/OpenKh.Tests/kh2/SystemItemTests.cs @@ -0,0 +1,30 @@ +using OpenKh.Kh2.System; +using System.IO; +using Xunit; + +namespace OpenKh.Tests.kh2 +{ + public class SystemItemTests + { + [Fact] + public void CheckForLength() => Common.FileOpenRead("kh2/res/item.bin", stream => + { + var entries = Item.Read(stream); + Assert.Equal(535, entries.Items1.Count); + Assert.Equal(151, entries.Items2.Count); + }); + + [Fact] + public void ShouldWriteTheExactSameFile() => Common.FileOpenRead("kh2/res/item.bin", stream => + { + Helpers.AssertStream(stream, x => + { + var outStream = new MemoryStream(); + + Item.Read(x).Write(outStream); + + return outStream; + }); + }); + } +} diff --git a/OpenKh.Tests/kh2/TrsrTests.cs b/OpenKh.Tests/kh2/TrsrTests.cs new file mode 100644 index 000000000..b30eb0826 --- /dev/null +++ b/OpenKh.Tests/kh2/TrsrTests.cs @@ -0,0 +1,16 @@ +using Xunit; +using OpenKh.Common; +using OpenKh.Kh2.System; + +namespace OpenKh.Tests.kh2 +{ + public class TrsrTests + { + [Fact] + public void CheckNewTrsr() => Common.FileOpenRead(@"kh2/res/trsr.bin", x => x.Using(stream => + { + var table = BaseSystem.Read(stream); + Assert.Equal(0x1AE, table.Count); + })); + } +} \ No newline at end of file diff --git a/OpenKh.Tests/kh2/res/00objentry.bin b/OpenKh.Tests/kh2/res/00objentry.bin new file mode 100644 index 000000000..dae1486a5 Binary files /dev/null and b/OpenKh.Tests/kh2/res/00objentry.bin differ diff --git a/OpenKh.Tests/kh2/res/item.bin b/OpenKh.Tests/kh2/res/item.bin new file mode 100644 index 000000000..e25f3ae7b Binary files /dev/null and b/OpenKh.Tests/kh2/res/item.bin differ diff --git a/OpenKh.Tests/kh2/res/map.vif b/OpenKh.Tests/kh2/res/map.vif new file mode 100644 index 000000000..2117c1b0f Binary files /dev/null and b/OpenKh.Tests/kh2/res/map.vif differ diff --git a/OpenKh.Tests/kh2/res/model_texture1.tex b/OpenKh.Tests/kh2/res/model_texture1.tex new file mode 100644 index 000000000..042988787 Binary files /dev/null and b/OpenKh.Tests/kh2/res/model_texture1.tex differ diff --git a/OpenKh.Tests/kh2/res/model_texture2.tex b/OpenKh.Tests/kh2/res/model_texture2.tex new file mode 100644 index 000000000..afe348c16 Binary files /dev/null and b/OpenKh.Tests/kh2/res/model_texture2.tex differ diff --git a/OpenKh.Tests/kh2/res/p_ex.vif b/OpenKh.Tests/kh2/res/p_ex.vif new file mode 100644 index 000000000..f77fecb74 Binary files /dev/null and b/OpenKh.Tests/kh2/res/p_ex.vif differ diff --git a/OpenKh.Tests/kh2/res/trsr.bin b/OpenKh.Tests/kh2/res/trsr.bin new file mode 100644 index 000000000..110d27e4c Binary files /dev/null and b/OpenKh.Tests/kh2/res/trsr.bin differ diff --git a/OpenKh.Tools.ImgdViewer/App.xaml b/OpenKh.Tools.BbsEventTableEditor/App.xaml similarity index 55% rename from OpenKh.Tools.ImgdViewer/App.xaml rename to OpenKh.Tools.BbsEventTableEditor/App.xaml index 5d0d32c89..4cf57fa28 100644 --- a/OpenKh.Tools.ImgdViewer/App.xaml +++ b/OpenKh.Tools.BbsEventTableEditor/App.xaml @@ -1,8 +1,8 @@ - + xmlns:local="clr-namespace:OpenKh.Tools.BbsEventTableEditor" + StartupUri="Views/MainWindow.xaml"> diff --git a/OpenKh.Tools.BbsEventTableEditor/App.xaml.cs b/OpenKh.Tools.BbsEventTableEditor/App.xaml.cs new file mode 100644 index 000000000..b67e7d280 --- /dev/null +++ b/OpenKh.Tools.BbsEventTableEditor/App.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows; + +namespace OpenKh.Tools.BbsEventTableEditor +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/OpenKh.Tools.BbsEventTableEditor/OpenKh.Tools.BbsEventTableEditor.csproj b/OpenKh.Tools.BbsEventTableEditor/OpenKh.Tools.BbsEventTableEditor.csproj new file mode 100644 index 000000000..e0b151533 --- /dev/null +++ b/OpenKh.Tools.BbsEventTableEditor/OpenKh.Tools.BbsEventTableEditor.csproj @@ -0,0 +1,28 @@ + + + + WinExe + net472 + true + BBS Event table editor + OpenKH + BBS Event table editor - OpenKH + https://github.com/Xeeynamo/OpenKh + Copyright © 2019 + true + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/OpenKh.Tools.BbsEventTableEditor/ViewModels/EventViewModel.cs b/OpenKh.Tools.BbsEventTableEditor/ViewModels/EventViewModel.cs new file mode 100644 index 000000000..45dc6dac2 --- /dev/null +++ b/OpenKh.Tools.BbsEventTableEditor/ViewModels/EventViewModel.cs @@ -0,0 +1,88 @@ +using OpenKh.Bbs; +using System; +using System.Collections.Generic; +using System.Linq; +using Xe.Tools; + +namespace OpenKh.Tools.BbsEventTableEditor.ViewModels +{ + public class EventViewModel : BaseNotifyPropertyChanged + { + private static readonly Dictionary _worlds = Constants.WorldNames + .Select((x, i) => new { Name = x, Id = i }) + .ToDictionary(x => (byte)x.Id, x => x.Name); + + public EventViewModel(Event @event) + { + Event = @event; + } + + public Event Event { get; private set; } + + public IEnumerable> Worlds => _worlds; + + public ushort Id + { + get => Event.Id; + set + { + Event.Id = value; + OnPropertyChanged(nameof(Name)); + } + } + + public ushort EventIndex + { + get => Event.EventIndex; + set + { + Event.EventIndex = (byte)Math.Min(999, (int)value); + OnPropertyChanged(nameof(Name)); + OnPropertyChanged(nameof(EventPath)); + } + } + + public byte World + { + get => Event.World; + set + { + Event.World = (byte)Math.Min(Constants.Worlds.Length, value); + OnPropertyChanged(nameof(Name)); + OnPropertyChanged(nameof(EventPath)); + } + } + + public byte Room + { + get => Event.Room; + set + { + Event.Room = (byte)Math.Min(99, (int)value); + OnPropertyChanged(nameof(Name)); + OnPropertyChanged(nameof(MapPath)); + } + } + + public ushort Unknown06 + { + get => Event.Unknown06; + set + { + Event.Unknown06 = value; + OnPropertyChanged(nameof(Name)); + } + } + + private string WorldId => World >= 0 && World < Constants.Worlds.Length ? + Constants.Worlds[World] : "{invalid}"; + + public string Name => $"{Id} {WorldId.ToUpper()} {Room:D02} {EventIndex:D03}"; + + public string MapPath => $"arc/map/{WorldId}{Room:D02}.arc"; + + public string EventPath => $"event/{WorldId}/{WorldId}_{EventIndex:D03}.exa"; + + public override string ToString() => Name; + } +} diff --git a/OpenKh.Tools.BbsEventTableEditor/ViewModels/EventsViewModel.cs b/OpenKh.Tools.BbsEventTableEditor/ViewModels/EventsViewModel.cs new file mode 100644 index 000000000..6ac9fe944 --- /dev/null +++ b/OpenKh.Tools.BbsEventTableEditor/ViewModels/EventsViewModel.cs @@ -0,0 +1,36 @@ +using OpenKh.Bbs; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using Xe.Tools.Wpf.Models; + +namespace OpenKh.Tools.BbsEventTableEditor.ViewModels +{ + public class EventsViewModel : GenericListModel + { + private EventViewModel _selectedItem; + + public EventsViewModel() : + this(new Event[0]) + { } + + public EventsViewModel(IEnumerable events) : + base(events.Select(x => new EventViewModel(x))) + { + + } + + public Visibility GuideVisibility => IsItemSelected ? Visibility.Collapsed : Visibility.Visible; + public Visibility EditVisibility => IsItemSelected ? Visibility.Visible : Visibility.Collapsed; + + protected override void OnSelectedItem(EventViewModel item) + { + OnPropertyChanged(nameof(GuideVisibility)); + OnPropertyChanged(nameof(EditVisibility)); + base.OnSelectedItem(item); + } + + protected override EventViewModel OnNewItem() => + new EventViewModel(new Event()); + } +} diff --git a/OpenKh.Tools.BbsEventTableEditor/ViewModels/MainViewModel.cs b/OpenKh.Tools.BbsEventTableEditor/ViewModels/MainViewModel.cs new file mode 100644 index 000000000..a153e4978 --- /dev/null +++ b/OpenKh.Tools.BbsEventTableEditor/ViewModels/MainViewModel.cs @@ -0,0 +1,183 @@ +using OpenKh.Bbs; +using OpenKh.Common; +using OpenKh.Tools.Common; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Windows; +using Xe.Tools; +using Xe.Tools.Wpf.Commands; +using Xe.Tools.Wpf.Dialogs; + +namespace OpenKh.Tools.BbsEventTableEditor.ViewModels +{ + public class MainViewModel : BaseNotifyPropertyChanged + { + private static string ApplicationName = Utilities.GetApplicationName(); + private Window Window => Application.Current.Windows.OfType().FirstOrDefault(x => x.IsActive); + private string _fileName; + private EventsViewModel _eventsViewModel; + + public string Title => $"{Path.GetFileName(FileName) ?? "untitled"} | {ApplicationName}"; + + private string FileName + { + get => _fileName; + set + { + _fileName = value; + OnPropertyChanged(nameof(Title)); + } + } + + public RelayCommand OpenCommand { get; } + public RelayCommand SaveCommand { get; } + public RelayCommand SaveAsCommand { get; } + public RelayCommand ExitCommand { get; } + public RelayCommand ExportEventsListCommand { get; } + public RelayCommand ExportUsedEventsCommand { get; } + public RelayCommand ExportUsedMapsCommand { get; } + public RelayCommand AboutCommand { get; } + + public EventsViewModel EventsViewModel + { + get => _eventsViewModel; + private set { _eventsViewModel = value; OnPropertyChanged(); } + } + + public IEnumerable Events + { + get => EventsViewModel?.Items?.Select(x => x.Event) ?? new Event[0]; + set => EventsViewModel = new EventsViewModel(value); + } + + public MainViewModel() + { + OpenCommand = new RelayCommand(x => + { + var fd = FileDialog.Factory(Window, FileDialog.Behavior.Open, new[] + { + ("Event table (EVENT_TE, EVENT_VE, EVENT_AQ)", "*"), + ("All files", "*") + }); + + if (fd.ShowDialog() == true) + { + OpenFile(fd.FileName); + } + }, x => true); + + SaveCommand = new RelayCommand(x => + { + if (!string.IsNullOrEmpty(FileName)) + { + SaveFile(FileName, FileName); + } + else + { + SaveAsCommand.Execute(x); + } + }, x => true); + + SaveAsCommand = new RelayCommand(x => + { + var fd = FileDialog.Factory(Window, FileDialog.Behavior.Save); + if (fd.ShowDialog() == true) + { + SaveFile(FileName, fd.FileName); + FileName = fd.FileName; + } + }, x => true); + + ExitCommand = new RelayCommand(x => + { + Window.Close(); + }, x => true); + + ExportEventsListCommand = new RelayCommand(x => + { + var fd = FileDialog.Factory(Window, FileDialog.Behavior.Save); + fd.DefaultFileName = CreateExportFilePath("events_list.txt"); + if (fd.ShowDialog() == true) + { + File.CreateText(fd.FileName).Using(stream => + { + foreach (var item in Events) + { + stream.WriteLine($"ID {item.Id:X03} MAP {Constants.Worlds[item.World]}_{item.Room:D02} EVENT {item.EventIndex:D03}"); + } + }); + } + }, x => true); + + ExportUsedEventsCommand = new RelayCommand(x => + { + var fd = FileDialog.Factory(Window, FileDialog.Behavior.Save); + fd.DefaultFileName = CreateExportFilePath("events_used.txt"); + if (fd.ShowDialog() == true) + { + File.CreateText(fd.FileName).Using(stream => + { + foreach (var item in Events) + { + stream.WriteLine($"event/{Constants.Worlds[item.World]}/{Constants.Worlds[item.World]}_{item.EventIndex:D03}.exa"); + } + }); + } + }, x => true); + + ExportUsedMapsCommand = new RelayCommand(x => + { + var fd = FileDialog.Factory(Window, FileDialog.Behavior.Save); + fd.DefaultFileName = CreateExportFilePath("maps_used.txt"); + if (fd.ShowDialog() == true) + { + File.CreateText(fd.FileName).Using(stream => + { + foreach (var item in Events) + { + stream.WriteLine($"arc/map/{Constants.Worlds[item.World]}{item.Room:D02}.arc"); + } + }); + } + }, x => true); + + AboutCommand = new RelayCommand(x => + { + new AboutDialog(Assembly.GetExecutingAssembly()).ShowDialog(); + }, x => true); + + EventsViewModel = new EventsViewModel(); + } + + public bool OpenFile(string fileName) => File.OpenRead(fileName).Using(stream => + { + if (!Event.IsValid(stream)) + { + MessageBox.Show(Window, $"{Path.GetFileName(fileName)} is not a valid event file.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + return false; + } + + Events = Event.Read(stream); + FileName = fileName; + return true; + }); + + public void SaveFile(string previousFileName, string fileName) + { + File.Create(fileName).Using(stream => + { + Event.Write(stream, Events); + }); + } + + private string CreateExportFilePath(string newFileName) + { + var dirName = Path.GetDirectoryName(FileName); + var fileName = Path.GetFileNameWithoutExtension(FileName); + + return Path.Combine(dirName, $"{fileName}_{newFileName}"); + } + } +} diff --git a/OpenKh.Tools.BbsEventTableEditor/Views/EventsView.xaml b/OpenKh.Tools.BbsEventTableEditor/Views/EventsView.xaml new file mode 100644 index 000000000..0ccac9a69 --- /dev/null +++ b/OpenKh.Tools.BbsEventTableEditor/Views/EventsView.xaml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OpenKh.Tools.BbsEventTableEditor/Views/EventsView.xaml.cs b/OpenKh.Tools.BbsEventTableEditor/Views/EventsView.xaml.cs new file mode 100644 index 000000000..e8325ecff --- /dev/null +++ b/OpenKh.Tools.BbsEventTableEditor/Views/EventsView.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace OpenKh.Tools.BbsEventTableEditor.Views +{ + /// + /// Interaction logic for EventsView.xaml + /// + public partial class EventsView : UserControl + { + public EventsView() + { + InitializeComponent(); + } + } +} diff --git a/OpenKh.Tools.ImgdViewer/Views/ImgdView.xaml b/OpenKh.Tools.BbsEventTableEditor/Views/MainWindow.xaml similarity index 52% rename from OpenKh.Tools.ImgdViewer/Views/ImgdView.xaml rename to OpenKh.Tools.BbsEventTableEditor/Views/MainWindow.xaml index cab8fc70a..95878c097 100644 --- a/OpenKh.Tools.ImgdViewer/Views/ImgdView.xaml +++ b/OpenKh.Tools.BbsEventTableEditor/Views/MainWindow.xaml @@ -1,21 +1,18 @@ - - - - - - - - - - - + Title="{Binding Title}" Height="350" Width="450"> + + + + + + + @@ -27,17 +24,22 @@ - - - + + + + - - + + + diff --git a/OpenKh.Tools.BbsEventTableEditor/Views/MainWindow.xaml.cs b/OpenKh.Tools.BbsEventTableEditor/Views/MainWindow.xaml.cs new file mode 100644 index 000000000..800663997 --- /dev/null +++ b/OpenKh.Tools.BbsEventTableEditor/Views/MainWindow.xaml.cs @@ -0,0 +1,17 @@ +using OpenKh.Tools.BbsEventTableEditor.ViewModels; +using System.Windows; + +namespace OpenKh.Tools.BbsEventTableEditor.Views +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + DataContext = new MainViewModel(); + } + } +} diff --git a/OpenKh.Tools.Common/Controls/DrawPanel.cs b/OpenKh.Tools.Common/Controls/DrawPanel.cs index 1d477578d..650e9a298 100644 --- a/OpenKh.Tools.Common/Controls/DrawPanel.cs +++ b/OpenKh.Tools.Common/Controls/DrawPanel.cs @@ -287,7 +287,6 @@ private void ResizeRenderingEngine(int width, int height) drawing.Surface?.Dispose(); drawing.Surface = drawing.CreateSurface( width, height, Xe.Drawing.PixelFormat.Format32bppArgb, SurfaceType.InputOutput); - DoRender(); } } } diff --git a/OpenKh.Tools.Common/Controls/KingdomTextArea.cs b/OpenKh.Tools.Common/Controls/KingdomTextArea.cs index 9828c24d7..f8dcf2c32 100644 --- a/OpenKh.Tools.Common/Controls/KingdomTextArea.cs +++ b/OpenKh.Tools.Common/Controls/KingdomTextArea.cs @@ -46,7 +46,8 @@ public void NewLine(int fontHeight) private const int IconWidth = Constants.FontIconWidth; private const int IconHeight = Constants.FontIconHeight; - + private const int CharactersPerTextureBlock = 392; + private const int CharactersPerTexture = 784; public static DependencyProperty ContextProperty = DependencyPropertyUtils.GetDependencyProperty( nameof(Context), (o, x) => o.SetContext(x)); @@ -62,11 +63,15 @@ public void NewLine(int fontHeight) private byte[] _fontSpacing; private byte[] _iconSpacing; private IImageRead _imageFont; + private IImageRead _imageFont2; private IImageRead _imageIcon; private ISurface _surfaceFont; + private ISurface _surfaceFont2; private ISurface _surfaceIcon; private int _charPerRow; private int _iconPerRow; + private int _tableHeight; + private int _charTableHeight; private IMessageEncode _encode; public KingdomTextContext Context @@ -103,6 +108,7 @@ protected override void OnDrawCreate() { base.OnDrawCreate(); GetOrInitializeSurface(ref _surfaceFont, _imageFont); + GetOrInitializeSurface(ref _surfaceFont2, _imageFont2); GetOrInitializeSurface(ref _surfaceIcon, _imageIcon); } @@ -197,7 +203,7 @@ private void SetColor(DrawContext context, byte[] data) context.Color.R = data[0] / 255.0f; context.Color.G = data[1] / 255.0f; context.Color.B = data[2] / 255.0f; - context.Color.A = data[3] / 255.0f; + context.Color.A = Math.Min(data[3] * 2, Byte.MaxValue) / 255.0f; } private void DrawText(DrawContext context, MessageCommandModel command) @@ -215,8 +221,9 @@ private void DrawText(DrawContext context, MessageCommandModel command) private void DrawText(DrawContext context, byte[] data) { - foreach (var ch in data) + for (int i = 0; i < data.Length; i++) { + byte ch = data[i]; int spacing; if (ch >= 0x20) @@ -226,6 +233,13 @@ private void DrawText(DrawContext context, byte[] data) DrawChar(context, chIndex); spacing = _fontSpacing?[chIndex] ?? Context.FontWidth; } + else if (ch >= 0x19 && ch <= 0x1f) + { + int chIndex = data[++i] + (ch - 0x19) * 0x100 + 0xE0; + if (!context.IgnoreDraw) + DrawChar(context, chIndex); + spacing = _fontSpacing?[chIndex] ?? Context.FontWidth; + } else if (ch == 1) { spacing = 6; @@ -247,11 +261,31 @@ private void DrawIcon(DrawContext context, byte index) context.x += _iconSpacing?[index] ?? IconWidth; } - protected void DrawChar(DrawContext context, int index) => + protected void DrawChar(DrawContext context, int index) + { DrawChar(context, (index % _charPerRow) * Context.FontWidth, (index / _charPerRow) * Context.FontHeight); + } - protected void DrawChar(DrawContext context, int sourceX, int sourceY) => - DrawImageScale(context, _surfaceFont, sourceX, sourceY, Context.FontWidth, Context.FontHeight); + protected void DrawChar(DrawContext context, int sourceX, int sourceY) + { + ISurface surfaceFont; + + var tableIndex = sourceY / _charTableHeight; + sourceY %= _charTableHeight; + + if ((tableIndex & 1) != 0) + surfaceFont = _surfaceFont2; + else + surfaceFont = _surfaceFont; + + if ((tableIndex & 2) != 0) + sourceY += _tableHeight; + + if (surfaceFont == null) + return; + + DrawImageScale(context, surfaceFont, sourceX, sourceY, Context.FontWidth, Context.FontHeight); + } protected void DrawIcon(DrawContext context, int sourceX, int sourceY) => DrawImage(_surfaceIcon, context.x, context.y, sourceX, sourceY, IconWidth, IconHeight, 1.0, 1.0, new ColorF(1.0f, 1.0f, 1.0f, 1.0f)); @@ -274,14 +308,20 @@ private void SetContext(KingdomTextContext context) _fontSpacing = context.FontSpacing; _iconSpacing = context.IconSpacing; _imageFont = context.Font; + _imageFont2 = context.Font2; _imageIcon = context.Icon; _charPerRow = context.Font?.Size.Width / context.FontWidth ?? 1; _iconPerRow = context.Icon?.Size.Width / IconWidth ?? 1; - _encode = context.Encode; + _tableHeight = context.TableHeight; + _charTableHeight = context.TableHeight / context.FontHeight * context.FontHeight; + _encode = context.Encoder; if (_imageFont != null) InitializeSurface(ref _surfaceFont, _imageFont); + if (_imageFont2 != null) + InitializeSurface(ref _surfaceFont2, _imageFont2); + if (_imageIcon != null) InitializeSurface(ref _surfaceIcon, _imageIcon); diff --git a/OpenKh.Tools.Common/Extensions/KingdomTextContextExtensions.cs b/OpenKh.Tools.Common/Extensions/KingdomTextContextExtensions.cs index d914fb317..b5cee33e7 100644 --- a/OpenKh.Tools.Common/Extensions/KingdomTextContextExtensions.cs +++ b/OpenKh.Tools.Common/Extensions/KingdomTextContextExtensions.cs @@ -11,24 +11,56 @@ public static KingdomTextContext ToKh2EuSystemTextContext(this kh2.FontContext f new KingdomTextContext { Font = fontContext.ImageSystem, + Font2 = fontContext.ImageSystem2, Icon = fontContext.ImageIcon, FontSpacing = fontContext.SpacingSystem, IconSpacing = fontContext.SpacingIcon, - Encode = Encoders.InternationalSystem, + Encoder = Encoders.InternationalSystem, FontWidth = Constants.FontEuropeanSystemWidth, FontHeight = Constants.FontEuropeanSystemHeight, + TableHeight = Constants.FontTableSystemHeight, }; public static KingdomTextContext ToKh2EuEventTextContext(this kh2.FontContext fontContext) => new KingdomTextContext { Font = fontContext.ImageEvent, + Font2 = fontContext.ImageEvent2, Icon = fontContext.ImageIcon, FontSpacing = fontContext.SpacingEvent, IconSpacing = fontContext.SpacingIcon, - Encode = Encoders.InternationalSystem, + Encoder = Encoders.InternationalSystem, FontWidth = Constants.FontEuropeanEventWidth, FontHeight = Constants.FontEuropeanEventHeight, + TableHeight = Constants.FontTableEventHeight, + }; + + public static KingdomTextContext ToKh2JpSystemTextContext(this kh2.FontContext fontContext) => + new KingdomTextContext + { + Font = fontContext.ImageSystem, + Font2 = fontContext.ImageSystem2, + Icon = fontContext.ImageIcon, + FontSpacing = fontContext.SpacingSystem, + IconSpacing = fontContext.SpacingIcon, + Encoder = Encoders.JapaneseSystem, + FontWidth = Constants.FontJapaneseSystemWidth, + FontHeight = Constants.FontJapaneseSystemHeight, + TableHeight = Constants.FontTableSystemHeight, + }; + + public static KingdomTextContext ToKh2JpEventTextContext(this kh2.FontContext fontContext) => + new KingdomTextContext + { + Font = fontContext.ImageEvent, + Font2 = fontContext.ImageEvent2, + Icon = fontContext.ImageIcon, + FontSpacing = fontContext.SpacingEvent, + IconSpacing = fontContext.SpacingIcon, + Encoder = Encoders.JapaneseSystem, + FontWidth = Constants.FontJapaneseEventWidth, + FontHeight = Constants.FontJapaneseEventHeight, + TableHeight = Constants.FontTableEventHeight, }; } } diff --git a/OpenKh.Tools.Common/Models/Kh2WorldsList.cs b/OpenKh.Tools.Common/Models/Kh2WorldsList.cs new file mode 100644 index 000000000..31b2c487d --- /dev/null +++ b/OpenKh.Tools.Common/Models/Kh2WorldsList.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Xe.Tools.Models; +using OpenKh.Kh2; + +namespace OpenKh.Tools.Common.Models +{ + public class Kh2WorldsList : IEnumerable>, IEnumerable + { + private static List> _list = Enum.GetValues(typeof(World)) + .Cast() + .Select(e => new EnumItemModel() + { + Value = e, + Name = Constants.WorldNames[(int)e] + }) + .ToList(); + + public IEnumerator> GetEnumerator() => _list.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator(); + } +} diff --git a/OpenKh.Tools.Common/Models/KingdomTextContext.cs b/OpenKh.Tools.Common/Models/KingdomTextContext.cs index e34a7ed3d..ae2f48adc 100644 --- a/OpenKh.Tools.Common/Models/KingdomTextContext.cs +++ b/OpenKh.Tools.Common/Models/KingdomTextContext.cs @@ -6,12 +6,14 @@ namespace OpenKh.Tools.Common.Models public class KingdomTextContext { public IImageRead Font { get; set; } + public IImageRead Font2 { get; set; } public IImageRead Icon { get; set; } public byte[] FontSpacing { get; set; } public byte[] IconSpacing { get; set; } - public IMessageEncode Encode { get; set; } + public IMessageEncoder Encoder { get; set; } public int FontWidth { get; set; } public int FontHeight { get; set; } + public int TableHeight { get; set; } } } diff --git a/OpenKh.Tools.ImageViewer/App.xaml b/OpenKh.Tools.ImageViewer/App.xaml new file mode 100644 index 000000000..7a276d3a5 --- /dev/null +++ b/OpenKh.Tools.ImageViewer/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/OpenKh.Tools.ImgzViewer/App.xaml.cs b/OpenKh.Tools.ImageViewer/App.xaml.cs similarity index 51% rename from OpenKh.Tools.ImgzViewer/App.xaml.cs rename to OpenKh.Tools.ImageViewer/App.xaml.cs index 4daf16732..4acd4c8dd 100644 --- a/OpenKh.Tools.ImgzViewer/App.xaml.cs +++ b/OpenKh.Tools.ImageViewer/App.xaml.cs @@ -6,12 +6,12 @@ using System.Threading.Tasks; using System.Windows; -namespace OpenKh.Tools.ImgzViewer +namespace OpenKh.Tools.ImageViewer { - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application - { - } + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } } diff --git a/OpenKh.Tools.ImgdViewer/OpenKh.Tools.ImgdViewer.csproj b/OpenKh.Tools.ImageViewer/OpenKh.Tools.ImageViewer.csproj similarity index 60% rename from OpenKh.Tools.ImgdViewer/OpenKh.Tools.ImgdViewer.csproj rename to OpenKh.Tools.ImageViewer/OpenKh.Tools.ImageViewer.csproj index b78abf13a..5439e1b40 100644 --- a/OpenKh.Tools.ImgdViewer/OpenKh.Tools.ImgdViewer.csproj +++ b/OpenKh.Tools.ImageViewer/OpenKh.Tools.ImageViewer.csproj @@ -1,12 +1,14 @@  + WinExe net472 - IMGD editor - Luciano (Xeeynamo) Ciccariello - IMGD editor - Kingdom Hearts II - https://github.com/Xeeynamo/KingdomHearts - Copyright © 2018 + true + Image viewer + OpenKH + Image viewer - OpenKH + https://github.com/Xeeynamo/OpenKh + Copyright © 2019 true @@ -15,9 +17,10 @@ + - - + + diff --git a/OpenKh.Tools.ImageViewer/Services/IImageContainer.cs b/OpenKh.Tools.ImageViewer/Services/IImageContainer.cs new file mode 100644 index 000000000..cacf01e2c --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Services/IImageContainer.cs @@ -0,0 +1,14 @@ +using OpenKh.Imaging; +using System.Collections.Generic; + +namespace OpenKh.Tools.ImageViewer.Services +{ + public interface IImageContainer + { + int Count { get; } + + IEnumerable Images { get; } + + IImageRead GetImage(int index); + } +} diff --git a/OpenKh.Tools.ImageViewer/Services/IImageFormat.cs b/OpenKh.Tools.ImageViewer/Services/IImageFormat.cs new file mode 100644 index 000000000..7383a1450 --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Services/IImageFormat.cs @@ -0,0 +1,20 @@ +using OpenKh.Imaging; +using System.IO; + +namespace OpenKh.Tools.ImageViewer.Services +{ + public interface IImageFormat + { + string Name { get; } + + string Extension { get; } + + bool IsContainer { get; } + + bool IsCreationSupported { get; } + + bool IsValid(Stream stream); + + T As() where T : IImageFormat; + } +} diff --git a/OpenKh.Tools.ImageViewer/Services/IImageFormatService.cs b/OpenKh.Tools.ImageViewer/Services/IImageFormatService.cs new file mode 100644 index 000000000..9eff2d71d --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Services/IImageFormatService.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.IO; + +namespace OpenKh.Tools.ImageViewer.Services +{ + public interface IImageFormatService + { + IEnumerable Formats { get; } + + IImageFormat GetFormatByFileName(string fileName); + + IImageFormat GetFormatByContent(Stream stream); + } +} diff --git a/OpenKh.Tools.ImageViewer/Services/IImageMultiple.cs b/OpenKh.Tools.ImageViewer/Services/IImageMultiple.cs new file mode 100644 index 000000000..bed1eebeb --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Services/IImageMultiple.cs @@ -0,0 +1,11 @@ +using System.IO; + +namespace OpenKh.Tools.ImageViewer.Services +{ + public interface IImageMultiple : IImageFormat + { + IImageContainer Read(Stream stream); + + void Write(Stream stream, IImageContainer image); + } +} diff --git a/OpenKh.Tools.ImageViewer/Services/IImageSingle.cs b/OpenKh.Tools.ImageViewer/Services/IImageSingle.cs new file mode 100644 index 000000000..521ed7b95 --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Services/IImageSingle.cs @@ -0,0 +1,12 @@ +using OpenKh.Imaging; +using System.IO; + +namespace OpenKh.Tools.ImageViewer.Services +{ + public interface IImageSingle : IImageFormat + { + IImageRead Read(Stream stream); + + void Write(Stream stream, IImageRead image); + } +} diff --git a/OpenKh.Tools.ImageViewer/Services/ImageFormatService.GenericImageFormat.cs b/OpenKh.Tools.ImageViewer/Services/ImageFormatService.GenericImageFormat.cs new file mode 100644 index 000000000..8906e6459 --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Services/ImageFormatService.GenericImageFormat.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; + +namespace OpenKh.Tools.ImageViewer.Services +{ + public partial class ImageFormatService + { + private class GenericImageFormat : IImageFormat + { + private readonly Func isValid; + + public GenericImageFormat( + string name, + string ext, + bool isContainer, + bool isCreationSupported, + Func isValid) + { + Name = name; + Extension = ext; + IsContainer = isContainer; + IsCreationSupported = isCreationSupported; + this.isValid = isValid; + } + + public string Name { get; } + public string Extension { get; } + public bool IsContainer { get; } + public bool IsCreationSupported { get; } + public T As() where T : IImageFormat => (T)(object)this; + + public bool IsValid(Stream stream) => isValid(stream); + } + } +} diff --git a/OpenKh.Tools.ImageViewer/Services/ImageFormatService.ImageContainer.cs b/OpenKh.Tools.ImageViewer/Services/ImageFormatService.ImageContainer.cs new file mode 100644 index 000000000..55d32cf3b --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Services/ImageFormatService.ImageContainer.cs @@ -0,0 +1,23 @@ +using OpenKh.Imaging; +using System.Collections.Generic; +using System.Linq; + +namespace OpenKh.Tools.ImageViewer.Services +{ + public partial class ImageFormatService + { + private class ImageContainer : IImageContainer + { + private readonly IImageRead[] _images; + + public ImageContainer(IEnumerable images) + { + _images = images.ToArray(); + } + + public int Count => _images.Length; + public IEnumerable Images => _images; + public IImageRead GetImage(int index) => _images[index]; + } + } +} diff --git a/OpenKh.Tools.ImageViewer/Services/ImageFormatService.MultipleImageFormat.cs b/OpenKh.Tools.ImageViewer/Services/ImageFormatService.MultipleImageFormat.cs new file mode 100644 index 000000000..af5215280 --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Services/ImageFormatService.MultipleImageFormat.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; + +namespace OpenKh.Tools.ImageViewer.Services +{ + public partial class ImageFormatService + { + private class MultipleImageFormat : GenericImageFormat, IImageMultiple + { + private readonly Func read; + private readonly Action write; + + public MultipleImageFormat( + string name, + string ext, + bool isCreationSupported, + Func isValid, + Func read, + Action write) : + base(name, ext, true, isCreationSupported, isValid) + { + this.read = read; + this.write = write; + } + + public IImageContainer Read(Stream stream) => read(stream); + public void Write(Stream stream, IImageContainer image) => write(stream, image); + } + } +} diff --git a/OpenKh.Tools.ImageViewer/Services/ImageFormatService.SingleImageFormat.cs b/OpenKh.Tools.ImageViewer/Services/ImageFormatService.SingleImageFormat.cs new file mode 100644 index 000000000..0d6a3a6a9 --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Services/ImageFormatService.SingleImageFormat.cs @@ -0,0 +1,31 @@ +using OpenKh.Imaging; +using System; +using System.IO; + +namespace OpenKh.Tools.ImageViewer.Services +{ + public partial class ImageFormatService + { + private class SingleImageFormat : GenericImageFormat, IImageSingle + { + private readonly Func read; + private readonly Action write; + + public SingleImageFormat( + string name, + string ext, + bool isCreationSupported, + Func isValid, + Func read, + Action write) : + base(name, ext, false, isCreationSupported, isValid) + { + this.read = read; + this.write = write; + } + + public IImageRead Read(Stream stream) => read(stream); + public void Write(Stream stream, IImageRead image) => write(stream, image); + } + } +} diff --git a/OpenKh.Tools.ImageViewer/Services/ImageFormatService.cs b/OpenKh.Tools.ImageViewer/Services/ImageFormatService.cs new file mode 100644 index 000000000..8f6acaea5 --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Services/ImageFormatService.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using OpenKh.Common; +using OpenKh.Imaging; +using OpenKh.Kh2; +using OpenKh.Kh2.Extensions; + +namespace OpenKh.Tools.ImageViewer.Services +{ + public partial class ImageFormatService : IImageFormatService + { + private static readonly IImageFormat[] imageFormat; + + static ImageFormatService() + { + imageFormat = new IImageFormat[] + { + GetImageFormat("PNG", "png", true, Png.IsValid, Png.Read, (stream, image) => Png.Write(stream, image)), + GetImageFormat("BMP", "bmp", true, Bmp.IsValid, Bmp.Read, (stream, image) => Bmp.Write(stream, image)), + GetImageFormat("TIFF", "tiff", true, Tiff.IsValid, Tiff.Read, (stream, image) => Tiff.Write(stream, image)), + GetImageFormat("IMGD", "imd", true, Imgd.IsValid, Imgd.Read, (stream, image) => image.AsImgd().Write(stream)), + + GetImageFormat("IMGZ", "imz", true, Imgz.IsValid, s => Imgz.Read(s), (stream, images) => + Imgz.Write(stream, images.Select(x => x.AsImgd()))), + + GetImageFormat("TIM2", "tm2", false, Tm2.IsValid, s => Tm2.Read(s), (stream, images) => + throw new NotImplementedException()), + + GetImageFormat("KH2TIM", "tex", true, _ => true, + s => ModelTexture.Read(s).Images.Cast(), + (stream, images) => throw new NotImplementedException()), + }; + } + + public IEnumerable Formats => imageFormat; + + public IImageFormat GetFormatByContent(Stream stream) => + imageFormat.FirstOrDefault(x => x.IsValid(stream.SetPosition(0))); + + public IImageFormat GetFormatByFileName(string fileName) + { + var extension = Path.GetExtension(fileName); + var dotIndex = extension.IndexOf('.'); + if (dotIndex >= 0) + extension = extension.Substring(dotIndex + 1); + + return imageFormat.FirstOrDefault(x => string.Compare(x.Extension, extension, System.StringComparison.OrdinalIgnoreCase) == 0); + } + + private static IImageFormat GetImageFormat( + string name, + string extension, + bool isCreationSupported, + Func isValid, + Func read, + Action write) + { + return new SingleImageFormat(name, extension, isCreationSupported, isValid, read, write); + } + + private static IImageFormat GetImageFormat( + string name, + string extension, + bool isCreationSupported, + Func isValid, + Func> read, + Action> write) + { + return new MultipleImageFormat(name, extension, isCreationSupported, isValid, + stream => new ImageContainer(read(stream)), + (stream, container) => write(stream, container.Images)); + } + } +} diff --git a/OpenKh.Tools.ImageViewer/ViewModels/ImageContainerViewModel.cs b/OpenKh.Tools.ImageViewer/ViewModels/ImageContainerViewModel.cs new file mode 100644 index 000000000..610f61e9f --- /dev/null +++ b/OpenKh.Tools.ImageViewer/ViewModels/ImageContainerViewModel.cs @@ -0,0 +1,29 @@ +using OpenKh.Tools.ImageViewer.Services; +using System.Collections.Generic; +using System.Linq; +using Xe.Tools.Wpf.Models; + +namespace OpenKh.Tools.ImageViewer.ViewModels +{ + public class ImageContainerViewModel : GenericListModel + { + private readonly IImageContainer imageContainer; + + public ImageContainerViewModel(IImageContainer imageContainer) : + this(imageContainer.Images.Select((image, index) => new ImageViewModel(image, index))) + { + this.imageContainer = imageContainer; + } + + private ImageContainerViewModel(IEnumerable imageViewModels) : + base(imageViewModels) + { + + } + + protected override ImageViewModel OnNewItem() + { + throw new System.NotImplementedException(); + } + } +} diff --git a/OpenKh.Tools.ImageViewer/ViewModels/ImageViewModel.cs b/OpenKh.Tools.ImageViewer/ViewModels/ImageViewModel.cs new file mode 100644 index 000000000..ffc108720 --- /dev/null +++ b/OpenKh.Tools.ImageViewer/ViewModels/ImageViewModel.cs @@ -0,0 +1,34 @@ +using OpenKh.Imaging; +using OpenKh.Tools.Common; +using System.Windows.Media.Imaging; +using Xe.Tools; + +namespace OpenKh.Tools.ImageViewer.ViewModels +{ + public class ImageViewModel : BaseNotifyPropertyChanged + { + public ImageViewModel(IImageRead image, int index = -1) + { + Source = image; + Bitmap = Source.GetBimapSource(); + Index = index; + } + + public IImageRead Source { get; } + public BitmapSource Bitmap { get; } + public int Index { get; } + + public string Name => $"#{Index}"; + + public int Width => Source.Size.Width; + public int Height => Source.Size.Height; + + public string Size => $"{Width}x{Height}"; + public string Format => Source.PixelFormat.ToString(); + + public string ImageSize => Source != null ? $"{Source.Size.Width}x{Source.Size.Height}" : "-"; + public string ImageFormat => Source?.PixelFormat.ToString(); + + public override string ToString() => $"{Name} {Size} {Format}"; + } +} diff --git a/OpenKh.Tools.ImageViewer/ViewModels/ImageViewerViewModel.cs b/OpenKh.Tools.ImageViewer/ViewModels/ImageViewerViewModel.cs new file mode 100644 index 000000000..15fa2e92b --- /dev/null +++ b/OpenKh.Tools.ImageViewer/ViewModels/ImageViewerViewModel.cs @@ -0,0 +1,291 @@ +using OpenKh.Common; +using OpenKh.Tools.Common; +using OpenKh.Tools.ImageViewer.Services; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Windows; +using System.Windows.Media.Imaging; +using Xe.Tools; +using Xe.Tools.Wpf.Commands; +using Xe.Tools.Wpf.Dialogs; + +namespace OpenKh.Tools.ImageViewer.ViewModels +{ + public class ImageViewerViewModel : BaseNotifyPropertyChanged + { + private const int ZoomLevelFit = -1; + private static readonly IImageFormatService _imageFormatService = new ImageFormatService(); + private static readonly (string, string)[] _openFilter = + new (string, string)[] { ("All supported images", GetAllSupportedExtensions()) } + .Concat(_imageFormatService.Formats.Select(x => ($"{x.Name} image", x.Extension))) + .Concat(new[] { ("All files", "*") }) + .ToArray(); + private static readonly (string, string)[] _exportFilter = + new (string, string)[] { ("All supported images for export", GetAllSupportedExtensions()) } + .Concat(_imageFormatService.Formats.Where(x => x.IsCreationSupported).Select(x => ($"{x.Name} image", x.Extension))) + .Concat(new[] { ("All files", "*") }) + .ToArray(); + + private static string ApplicationName = Utilities.GetApplicationName(); + private Window Window => Application.Current.Windows.OfType().FirstOrDefault(x => x.IsActive); + + public string Title => $"{Path.GetFileName(FileName) ?? "untitled"} | {ApplicationName}"; + + public ImageViewerViewModel() + { + OpenCommand = new RelayCommand(x => + { + var fd = FileDialog.Factory(Window, FileDialog.Behavior.Open, _openFilter); + if (fd.ShowDialog() == true) + { + using (var stream = File.OpenRead(fd.FileName)) + { + LoadImage(stream); + FileName = fd.FileName; + } + } + }, x => true); + + SaveCommand = new RelayCommand(x => + { + if (!string.IsNullOrEmpty(FileName)) + { + using (var stream = File.Open(FileName, FileMode.Create)) + { + Save(stream); + } + } + else + { + SaveAsCommand.Execute(x); + } + }, x => ImageFormat != null); + + SaveAsCommand = new RelayCommand(x => + { + var fd = FileDialog.Factory(Window, FileDialog.Behavior.Save, ($"{ImageFormat.Name} format", ImageFormat.Extension)); + fd.DefaultFileName = FileName; + + if (fd.ShowDialog() == true) + { + using (var stream = File.Open(fd.FileName, FileMode.Create)) + { + Save(stream); + } + } + }, x => ImageFormat != null); + + ExitCommand = new RelayCommand(x => + { + Window.Close(); + }, x => true); + + AboutCommand = new RelayCommand(x => + { + new AboutDialog(Assembly.GetExecutingAssembly()).ShowDialog(); + }, x => true); + + ExportCommand = new RelayCommand(x => + { + var fd = FileDialog.Factory(Window, FileDialog.Behavior.Save, _exportFilter); + fd.DefaultFileName = Path.GetFileName(FileName); + + if (fd.ShowDialog() == true) + { + var imageFormat = _imageFormatService.GetFormatByFileName(fd.FileName); + if (imageFormat == null) + { + var extension = Path.GetExtension(fd.FileName); + MessageBox.Show($"The format with extension {extension} is not supported for export.", + "Error", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + File.OpenWrite(fd.FileName).Using(stream => Save(stream, imageFormat)); + } + }, x => true); + + ImportCommand = new RelayCommand(x => + { + var fd = FileDialog.Factory(Window, FileDialog.Behavior.Open, FileDialog.Type.ImagePng); + fd.DefaultFileName = $"{Path.GetFileNameWithoutExtension(FileName)}.png"; + + if (fd.ShowDialog() == true) + { + using (var fStream = File.OpenRead(fd.FileName)) + { + throw new NotImplementedException(); + } + } + }, x => false); + + ZoomLevel = ZoomLevelFit; + } + + public ImageViewerViewModel(Stream stream) : + this() + { + LoadImage(stream); + } + + private string _fileName; + private IImageFormat _imageFormat; + private IImageContainer _imageContainer; + private double _zoomLevel; + private bool _zoomFit; + private ImageViewModel _imageViewModel; + private ImageContainerViewModel _imageContainerItems; + + public string FileName + { + get => _fileName; + set + { + _fileName = value; + OnPropertyChanged(nameof(Title)); + } + } + + public RelayCommand OpenCommand { get; set; } + public RelayCommand SaveCommand { get; set; } + public RelayCommand SaveAsCommand { get; set; } + public RelayCommand ExitCommand { get; set; } + public RelayCommand AboutCommand { get; set; } + public RelayCommand ExportCommand { get; set; } + public RelayCommand ImportCommand { get; set; } + + public IEnumerable> ZoomLevels { get; } = + new double[] + { + 0.25, 0.33, 0.5, 0.75, 1, 1.25, 1.50, 1.75, 2, 2.5, 3, 4, 6, 8, 12, 16, 1 + } + .Select(x => new KeyValuePair($"{x * 100.0}%", x)) + .Concat(new KeyValuePair[] + { + new KeyValuePair("Fit", ZoomLevelFit) + }) + .ToArray(); + + private IImageFormat ImageFormat + { + get => _imageFormat; + set + { + _imageFormat = value; + OnPropertyChanged(nameof(ImageType)); + OnPropertyChanged(nameof(ImageMultiple)); + OnPropertyChanged(nameof(ImageSelectionVisibility)); + OnPropertyChanged(nameof(SaveCommand)); + OnPropertyChanged(nameof(SaveAsCommand)); + } + } + + private IImageContainer ImageContainer + { + get => _imageContainer; + set + { + _imageContainer = value; + ImageContainerItems = new ImageContainerViewModel(_imageContainer); + } + } + + public ImageViewModel Image + { + get => _imageViewModel; + set + { + _imageViewModel = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ImageZoomWidth)); + OnPropertyChanged(nameof(ImageZoomHeight)); + } + } + + public ImageContainerViewModel ImageContainerItems + { + get => _imageContainerItems; + set + { + _imageContainerItems = value; + OnPropertyChanged(); + } + } + + public string ImageType => _imageFormat?.Name ?? "Unknown"; + public string ImageMultiple => _imageFormat != null ? _imageFormat.IsContainer ? "Multiple" : "Single" : null; + public Visibility ImageSelectionVisibility => (_imageFormat?.IsContainer ?? false) ? Visibility.Visible : Visibility.Collapsed; + public double ZoomLevel + { + get => _zoomLevel; + set + { + _zoomLevel = value; + ZoomFit = _zoomLevel <= 0; + OnPropertyChanged(); + OnPropertyChanged(nameof(ImageZoomWidth)); + OnPropertyChanged(nameof(ImageZoomHeight)); + } + } + + public bool ZoomFit + { + get => _zoomFit; + set + { + _zoomFit = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ImageFitVisibility)); + OnPropertyChanged(nameof(ImageCustomZoomVisibility)); + } + } + + public double ImageZoomWidth => (Image?.Width * ZoomLevel) ?? 0; + public double ImageZoomHeight => (Image?.Height * ZoomLevel) ?? 0; + + public Visibility ImageFitVisibility => ZoomFit ? Visibility.Visible : Visibility.Collapsed; + public Visibility ImageCustomZoomVisibility => ZoomFit ? Visibility.Collapsed : Visibility.Visible; + + private void LoadImage(Stream stream) + { + var imageFormat = _imageFormatService.GetFormatByContent(stream); + if (imageFormat == null) + throw new Exception("Image format not found for the given stream."); + + ImageFormat = imageFormat; + + if (ImageFormat.IsContainer) + { + ImageContainer = _imageFormat.As().Read(stream); + Image = ImageContainerItems.First(); + } + else + { + Image = new ImageViewModel(_imageFormat.As().Read(stream)); + } + } + + public void Save(Stream stream) => Save(stream, ImageFormat); + + public void Save(Stream stream, IImageFormat imageFormat) + { + if (imageFormat.IsContainer) + { + imageFormat.As().Write(stream, ImageContainer); + } + else + { + imageFormat.As().Write(stream, Image.Source); + } + } + + private static string GetAllSupportedExtensions() + { + var extensions = _imageFormatService.Formats.Select(x => $"*.{x.Extension}"); + return string.Join(";", extensions).Substring(2); + } + } +} diff --git a/OpenKh.Tools.ImageViewer/Views/MainWindow.xaml b/OpenKh.Tools.ImageViewer/Views/MainWindow.xaml new file mode 100644 index 000000000..d791bf21e --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Views/MainWindow.xaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OpenKh.Tools.ImageViewer/Views/MainWindow.xaml.cs b/OpenKh.Tools.ImageViewer/Views/MainWindow.xaml.cs new file mode 100644 index 000000000..f4e4e5630 --- /dev/null +++ b/OpenKh.Tools.ImageViewer/Views/MainWindow.xaml.cs @@ -0,0 +1,17 @@ +using OpenKh.Tools.ImageViewer.ViewModels; +using System.Windows; + +namespace OpenKh.Tools.ImageViewer.Views +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + DataContext = new ImageViewerViewModel(); + } + } +} diff --git a/OpenKh.Tools.ImgdViewer/App.config b/OpenKh.Tools.ImgdViewer/App.config deleted file mode 100644 index 787dcbecc..000000000 --- a/OpenKh.Tools.ImgdViewer/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/OpenKh.Tools.ImgdViewer/ImgdModule.cs b/OpenKh.Tools.ImgdViewer/ImgdModule.cs deleted file mode 100644 index e035e6f4f..000000000 --- a/OpenKh.Tools.ImgdViewer/ImgdModule.cs +++ /dev/null @@ -1,13 +0,0 @@ -using OpenKh.Tools.ImgdViewer.Views; -using Xe.Tools; - -namespace OpenKh.Tools.ImgdViewer -{ - public class ImgdModule : IToolModule - { - public bool? ShowDialog(params object[] args) - { - return new ImgdView(args).ShowDialog(); - } - } -} diff --git a/OpenKh.Tools.ImgdViewer/Properties/AssemblyInfo.cs b/OpenKh.Tools.ImgdViewer/Properties/AssemblyInfo.cs deleted file mode 100644 index 992bb34d2..000000000 --- a/OpenKh.Tools.ImgdViewer/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Windows; - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -//In order to begin building localizable applications, set -//CultureYouAreCodingWith in your .csproj file -//inside a . For example, if you are using US english -//in your source files, set the to en-US. Then uncomment -//the NeutralResourceLanguage attribute below. Update the "en-US" in -//the line below to match the UICulture setting in the project file. - -//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] - - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] diff --git a/OpenKh.Tools.ImgdViewer/Properties/Resources.Designer.cs b/OpenKh.Tools.ImgdViewer/Properties/Resources.Designer.cs deleted file mode 100644 index d67137afd..000000000 --- a/OpenKh.Tools.ImgdViewer/Properties/Resources.Designer.cs +++ /dev/null @@ -1,71 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace OpenKh.Tools.ImgdViewer.Properties -{ - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources - { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() - { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if ((resourceMan == null)) - { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("OpenKh.Tools.ImgdViewer.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { - return resourceCulture; - } - set - { - resourceCulture = value; - } - } - } -} diff --git a/OpenKh.Tools.ImgdViewer/Properties/Settings.Designer.cs b/OpenKh.Tools.ImgdViewer/Properties/Settings.Designer.cs deleted file mode 100644 index 9670bdc17..000000000 --- a/OpenKh.Tools.ImgdViewer/Properties/Settings.Designer.cs +++ /dev/null @@ -1,30 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace OpenKh.Tools.ImgdViewer.Properties -{ - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { - return defaultInstance; - } - } - } -} diff --git a/OpenKh.Tools.ImgdViewer/ViewModels/ImageViewModel.cs b/OpenKh.Tools.ImgdViewer/ViewModels/ImageViewModel.cs deleted file mode 100644 index b6c710ae3..000000000 --- a/OpenKh.Tools.ImgdViewer/ViewModels/ImageViewModel.cs +++ /dev/null @@ -1,160 +0,0 @@ -using OpenKh.Imaging; -using OpenKh.Kh2; -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Windows; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using Xe.Tools; -using Xe.Tools.Wpf.Commands; -using Xe.Tools.Wpf.Dialogs; - -namespace OpenKh.Tools.ImgdViewer.ViewModels -{ - public class ImageViewModel : BaseNotifyPropertyChanged - { - public ImageViewModel() - { - OpenCommand = new RelayCommand(x => - { - var fd = FileDialog.Factory(Window, FileDialog.Behavior.Open, ("IMGD texture", "imd")); - if (fd.ShowDialog() == true) - { - using (var stream = File.OpenRead(fd.FileName)) - { - FileName = fd.FileName; - LoadImgd(Imgd = Imgd.Read(stream)); - - OnPropertyChanged(nameof(Image)); - OnPropertyChanged(nameof(SaveCommand)); - OnPropertyChanged(nameof(SaveAsCommand)); - } - } - }, x => true); - - SaveCommand = new RelayCommand(x => - { - if (!string.IsNullOrEmpty(FileName)) - { - using (var stream = File.Open(FileName, FileMode.Create)) - { - Imgd.Write(stream); - } - } - else - { - SaveAsCommand.Execute(x); - } - }, x => Imgd != null); - - SaveAsCommand = new RelayCommand(x => - { - var fd = FileDialog.Factory(Window, FileDialog.Behavior.Save, ("IMGD texture", "imd")); - fd.DefaultFileName = FileName; - - if (fd.ShowDialog() == true) - { - using (var stream = File.Open(fd.FileName, FileMode.Create)) - { - Imgd.Write(stream); - } - } - }, x => Imgd != null); - - ExitCommand = new RelayCommand(x => - { - Window.Close(); - }, x => true); - - AboutCommand = new RelayCommand(x => - { - new AboutDialog(Assembly.GetExecutingAssembly()).ShowDialog(); - }, x => true); - - ExportCommand = new RelayCommand(x => - { - var fd = FileDialog.Factory(Window, FileDialog.Behavior.Save, FileDialog.Type.ImagePng); - fd.DefaultFileName = $"{Path.GetFileNameWithoutExtension(FileName)}.png"; - - if (fd.ShowDialog() == true) - { - using (var fStream = File.OpenWrite(fd.FileName)) - { - BitmapEncoder encoder = new PngBitmapEncoder(); - encoder.Frames.Add(BitmapFrame.Create(Image)); - encoder.Save(fStream); - } - } - }, x => true); - - ImportCommand = new RelayCommand(x => - { - var fd = FileDialog.Factory(Window, FileDialog.Behavior.Open, FileDialog.Type.ImagePng); - fd.DefaultFileName = $"{Path.GetFileNameWithoutExtension(FileName)}.png"; - - if (fd.ShowDialog() == true) - { - using (var fStream = File.OpenRead(fd.FileName)) - { - throw new NotImplementedException(); - } - } - }, x => false); - } - - public ImageViewModel(Stream stream) : - this(Imgd.Read(stream)) - { - SaveCommand = new RelayCommand(x => - { - stream.Position = 0; - stream.SetLength(0); - Imgd.Write(stream); - }); - } - - public ImageViewModel(Imgd imgd) : - this() - { - LoadImgd(imgd); - - OpenCommand = new RelayCommand(x => { }, x => false); - } - - private Window Window => Application.Current.Windows.OfType().FirstOrDefault(x => x.IsActive); - - public BitmapSource Image { get; set; } - - public Imgd Imgd { get; set; } - - public string FileName { get; set; } - - public RelayCommand OpenCommand { get; set; } - - public RelayCommand SaveCommand { get; set; } - - public RelayCommand SaveAsCommand { get; set; } - - public RelayCommand ExitCommand { get; set; } - - public RelayCommand AboutCommand { get; set; } - - public RelayCommand ExportCommand { get; set; } - - public RelayCommand ImportCommand { get; set; } - - private void LoadImgd(Imgd imgd) - { - LoadImage(imgd); - } - - private void LoadImage(IImageRead imageRead) - { - var size = imageRead.Size; - var data = imageRead.ToBgra32(); - Image = BitmapSource.Create(size.Width, size.Height, 96.0, 96.0, PixelFormats.Bgra32, null, data, size.Width * 4); - } - } -} diff --git a/OpenKh.Tools.ImgdViewer/Views/ImgdView.xaml.cs b/OpenKh.Tools.ImgdViewer/Views/ImgdView.xaml.cs deleted file mode 100644 index 629372b2a..000000000 --- a/OpenKh.Tools.ImgdViewer/Views/ImgdView.xaml.cs +++ /dev/null @@ -1,53 +0,0 @@ -using OpenKh.Kh2; -using OpenKh.Tools.ImgdViewer.ViewModels; -using System.IO; -using System.Windows; - -namespace OpenKh.Tools.ImgdViewer.Views -{ - /// - /// Interaction logic for ImgdView.xaml - /// - public partial class ImgdView : Window - { - public ImgdView() - { - InitializeComponent(); - DataContext = new ImageViewModel(); - } - - public ImgdView(object[] args) : - this() - { - if (args.Length > 0) - { - if (args[0] is Imgd imgd) - Initialize(imgd); - else if (args[0] is Stream stream) - Initialize(stream); - } - } - - public ImgdView(Imgd imgd) : - this() - { - Initialize(imgd); - } - - public ImgdView(Stream stream) : - this() - { - Initialize(stream); - } - - private void Initialize(Stream stream) - { - DataContext = new ImageViewModel(stream); - } - - private void Initialize(Imgd image) - { - DataContext = new ImageViewModel(image); - } - } -} diff --git a/OpenKh.Tools.ImgzViewer/App.xaml b/OpenKh.Tools.ImgzViewer/App.xaml deleted file mode 100644 index 3c0970956..000000000 --- a/OpenKh.Tools.ImgzViewer/App.xaml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/OpenKh.Tools.ImgzViewer/ImgzModule.cs b/OpenKh.Tools.ImgzViewer/ImgzModule.cs deleted file mode 100644 index b715d0bc0..000000000 --- a/OpenKh.Tools.ImgzViewer/ImgzModule.cs +++ /dev/null @@ -1,13 +0,0 @@ -using OpenKh.Tools.ImgzViewer.Views; -using Xe.Tools; - -namespace OpenKh.Tools.ImgdViewer -{ - public class ImgzModule : IToolModule - { - public bool? ShowDialog(params object[] args) - { - return new ImgzView(args).ShowDialog(); - } - } -} diff --git a/OpenKh.Tools.ImgzViewer/Models/ImageModel.cs b/OpenKh.Tools.ImgzViewer/Models/ImageModel.cs deleted file mode 100644 index f0a071357..000000000 --- a/OpenKh.Tools.ImgzViewer/Models/ImageModel.cs +++ /dev/null @@ -1,42 +0,0 @@ -using OpenKh.Imaging; -using OpenKh.Kh2; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using Xe.Tools; -using Xe.Tools.Wpf; - -namespace OpenKh.Tools.ImgzViewer.Models -{ - public class ImageModel : BaseNotifyPropertyChanged - { - private Imgd imgd; - - public ImageModel(Imgd imgd) - { - Imgd = imgd; - } - - public Imgd Imgd - { - get => imgd; - set => LoadImgd(imgd = value); - } - - public BitmapSource Image { get; set; } - - public string DisplayName => $"{Image.PixelWidth}x{Image.PixelHeight}"; - - private void LoadImgd(Imgd imgd) - { - LoadImage(imgd); - } - - private void LoadImage(IImageRead imageRead) - { - var size = imageRead.Size; - var data = imageRead.ToBgra32(); - Image = BitmapSource.Create(size.Width, size.Height, 96.0, 96.0, PixelFormats.Bgra32, null, data, size.Width * 4); - OnPropertyChanged(nameof(Image)); - } - } -} diff --git a/OpenKh.Tools.ImgzViewer/Properties/AssemblyInfo.cs b/OpenKh.Tools.ImgzViewer/Properties/AssemblyInfo.cs deleted file mode 100644 index 992bb34d2..000000000 --- a/OpenKh.Tools.ImgzViewer/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Windows; - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -//In order to begin building localizable applications, set -//CultureYouAreCodingWith in your .csproj file -//inside a . For example, if you are using US english -//in your source files, set the to en-US. Then uncomment -//the NeutralResourceLanguage attribute below. Update the "en-US" in -//the line below to match the UICulture setting in the project file. - -//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] - - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] diff --git a/OpenKh.Tools.ImgzViewer/Properties/Resources.Designer.cs b/OpenKh.Tools.ImgzViewer/Properties/Resources.Designer.cs deleted file mode 100644 index 157479e13..000000000 --- a/OpenKh.Tools.ImgzViewer/Properties/Resources.Designer.cs +++ /dev/null @@ -1,71 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace OpenKh.Tools.ImgzViewer.Properties -{ - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources - { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() - { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if ((resourceMan == null)) - { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("OpenKh.Tools.ImgzViewer.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { - return resourceCulture; - } - set - { - resourceCulture = value; - } - } - } -} diff --git a/OpenKh.Tools.ImgzViewer/Properties/Resources.resx b/OpenKh.Tools.ImgzViewer/Properties/Resources.resx deleted file mode 100644 index af7dbebba..000000000 --- a/OpenKh.Tools.ImgzViewer/Properties/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/OpenKh.Tools.ImgzViewer/Properties/Settings.Designer.cs b/OpenKh.Tools.ImgzViewer/Properties/Settings.Designer.cs deleted file mode 100644 index 99c51baff..000000000 --- a/OpenKh.Tools.ImgzViewer/Properties/Settings.Designer.cs +++ /dev/null @@ -1,30 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace OpenKh.Tools.ImgzViewer.Properties -{ - - - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - - private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { - return defaultInstance; - } - } - } -} diff --git a/OpenKh.Tools.ImgzViewer/Properties/Settings.settings b/OpenKh.Tools.ImgzViewer/Properties/Settings.settings deleted file mode 100644 index 033d7a5e9..000000000 --- a/OpenKh.Tools.ImgzViewer/Properties/Settings.settings +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/OpenKh.Tools.ImgzViewer/ViewModels/ImagesViewModel.cs b/OpenKh.Tools.ImgzViewer/ViewModels/ImagesViewModel.cs deleted file mode 100644 index 58b1343e4..000000000 --- a/OpenKh.Tools.ImgzViewer/ViewModels/ImagesViewModel.cs +++ /dev/null @@ -1,179 +0,0 @@ -using OpenKh.Kh2; -using OpenKh.Tools.ImgzViewer.Models; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Windows; -using System.Windows.Media.Imaging; -using Xe.Tools.Wpf.Commands; -using Xe.Tools.Wpf.Dialogs; -using Xe.Tools.Wpf.Models; - -namespace OpenKh.Tools.ImgzViewer.ViewModels -{ - public class ImagesViewModel : GenericListModel - { - public ImagesViewModel() : - this((IEnumerable)null) - { } - - public ImagesViewModel(Stream stream) : - this(Imgz.Open(stream)) - { - OpenCommand = new RelayCommand(x => { }, x => false); - SaveCommand = new RelayCommand(x => - { - stream.Position = 0; - stream.SetLength(0); - Imgz.Save(stream, Items.Select(image => image.Imgd)); - }); - } - - public ImagesViewModel(IEnumerable images) : - this(images.Select(x => new ImageModel(x))) - { } - - public ImagesViewModel(IEnumerable images) : - base(images) - { - OpenCommand = new RelayCommand(x => - { - var fd = FileDialog.Factory(Window, FileDialog.Behavior.Open, ("IMGZ texture", "imz")); - if (fd.ShowDialog() == true) - { - using (var stream = File.OpenRead(fd.FileName)) - { - FileName = fd.FileName; - Items.Clear(); - foreach (var item in Imgz.Open(stream)) - { - Items.Add(new ImageModel(item)); - } - - OnPropertyChanged(nameof(SaveCommand)); - OnPropertyChanged(nameof(SaveAsCommand)); - } - } - }, x => true); - - SaveCommand = new RelayCommand(x => - { - if (!string.IsNullOrEmpty(FileName)) - { - using (var stream = File.Open(FileName, FileMode.Create)) - { - Imgz.Save(stream, Items.Select(model => model.Imgd)); - } - } - else - { - SaveAsCommand.Execute(x); - } - }, x => true); - - SaveAsCommand = new RelayCommand(x => - { - var fd = FileDialog.Factory(Window, FileDialog.Behavior.Save, ("IMGZ texture", "imz")); - if (fd.ShowDialog() == true) - { - using (var stream = File.Open(fd.FileName, FileMode.Create)) - { - Imgz.Save(stream, Items.Select(model => model.Imgd)); - } - } - }, x => true); - - ExitCommand = new RelayCommand(x => - { - Window.Close(); - }, x => true); - - AboutCommand = new RelayCommand(x => - { - new AboutDialog(Assembly.GetExecutingAssembly()).ShowDialog(); - }, x => true); - - ExportCommand = new RelayCommand(x => - { - var fd = FileDialog.Factory(Window, FileDialog.Behavior.Save, FileDialog.Type.ImagePng); - fd.DefaultFileName = $"{Path.GetFileNameWithoutExtension(FileName)}_{SelectedIndex}.png"; - - if (fd.ShowDialog() == true) - { - using (var fStream = File.OpenWrite(fd.FileName)) - { - BitmapEncoder encoder = new PngBitmapEncoder(); - encoder.Frames.Add(BitmapFrame.Create(SelectedItem.Image)); - encoder.Save(fStream); - } - } - }, x => IsItemSelected); - - ExportAllCommand = new RelayCommand(x => - { - var fd = FileDialog.Factory(Window, FileDialog.Behavior.Folder, FileDialog.Type.ImagePng); - - if (fd.ShowDialog() == true) - { - var index = 0; - foreach (var item in Items) - { - var defaultFileName = $"{Path.GetFileNameWithoutExtension(FileName)}_{index++}.png"; - var path = Path.Combine(fd.FileName, defaultFileName); - - using (var fStream = File.OpenWrite(path)) - { - BitmapEncoder encoder = new PngBitmapEncoder(); - encoder.Frames.Add(BitmapFrame.Create(item.Image)); - encoder.Save(fStream); - } - } - } - }, x => IsItemSelected); - - ImportCommand = new RelayCommand(x => - { - var fd = FileDialog.Factory(Window, FileDialog.Behavior.Open, FileDialog.Type.ImagePng); - fd.DefaultFileName = $"{Path.GetFileNameWithoutExtension(FileName)}_{SelectedIndex}.png"; - - if (fd.ShowDialog() == true) - { - using (var fStream = File.OpenRead(fd.FileName)) - { - throw new NotImplementedException(); - } - } - }, x => false); - } - - - private Window Window => Application.Current.Windows.OfType().FirstOrDefault(x => x.IsActive); - - public string FileName { get; set; } - - public RelayCommand OpenCommand { get; set; } - - public RelayCommand SaveCommand { get; set; } - - public RelayCommand SaveAsCommand { get; set; } - - public RelayCommand ExitCommand { get; set; } - - public RelayCommand AboutCommand { get; set; } - - public RelayCommand AddItemCommand { get; set; } - - public RelayCommand ExportCommand { get; set; } - - public RelayCommand ExportAllCommand { get; set; } - - public RelayCommand ImportCommand { get; set; } - - protected override ImageModel OnNewItem() - { - throw new NotImplementedException(); - } - } -} diff --git a/OpenKh.Tools.ImgzViewer/Views/ImgzView.xaml b/OpenKh.Tools.ImgzViewer/Views/ImgzView.xaml deleted file mode 100644 index 7412a584f..000000000 --- a/OpenKh.Tools.ImgzViewer/Views/ImgzView.xaml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/OpenKh.Tools.ImgzViewer/Views/ImgzView.xaml.cs b/OpenKh.Tools.ImgzViewer/Views/ImgzView.xaml.cs deleted file mode 100644 index 365d3a052..000000000 --- a/OpenKh.Tools.ImgzViewer/Views/ImgzView.xaml.cs +++ /dev/null @@ -1,52 +0,0 @@ -using OpenKh.Kh2; -using OpenKh.Tools.ImgzViewer.ViewModels; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; - -namespace OpenKh.Tools.ImgzViewer.Views -{ - /// - /// Interaction logic for ImgzView.xaml - /// - public partial class ImgzView : Window - { - public ImgzView() - { - InitializeComponent(); - DataContext = new ImagesViewModel(); - } - - public ImgzView(object[] args) : - this() - { - if (args.Length > 0) - { - if (args[0] is Stream stream) - Initialize(stream); - } - } - - public ImgzView(Stream stream) : - this() - { - Initialize(stream); - } - - private void Initialize(Stream stream) - { - DataContext = new ImagesViewModel(stream); - } - } -} diff --git a/OpenKh.Tools.Kh2SystemEditor/App.xaml b/OpenKh.Tools.Kh2SystemEditor/App.xaml new file mode 100644 index 000000000..99f8c0c8e --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/App.xaml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/OpenKh.Tools.Kh2SystemEditor/App.xaml.cs b/OpenKh.Tools.Kh2SystemEditor/App.xaml.cs new file mode 100644 index 000000000..2fecbb7a6 --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/App.xaml.cs @@ -0,0 +1,11 @@ +using System.Windows; + +namespace OpenKh.Tools.Kh2SystemEditor +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } +} diff --git a/OpenKh.Tools.Kh2SystemEditor/Extensions/BarExtensions.cs b/OpenKh.Tools.Kh2SystemEditor/Extensions/BarExtensions.cs new file mode 100644 index 000000000..3e194495f --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/Extensions/BarExtensions.cs @@ -0,0 +1,15 @@ +using OpenKh.Common; +using OpenKh.Kh2; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace OpenKh.Tools.Kh2SystemEditor.Extensions +{ + public static class BarExtensions + { + public static Stream GetBinaryStream(this IEnumerable entries, string name) => + // TODO throws exception if that entry is not found + entries.First(x => x.Name == name && x.Index == 0 && x.Type == Bar.EntryType.Binary).Stream.SetPosition(0); + } +} diff --git a/OpenKh.Tools.Kh2SystemEditor/Interfaces/IItemEntry.cs b/OpenKh.Tools.Kh2SystemEditor/Interfaces/IItemEntry.cs new file mode 100644 index 000000000..864690484 --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/Interfaces/IItemEntry.cs @@ -0,0 +1,8 @@ +namespace OpenKh.Tools.Kh2SystemEditor.Interfaces +{ + public interface IItemEntry + { + ushort Id { get; } + string Name { get; } + } +} diff --git a/OpenKh.Tools.Kh2SystemEditor/Interfaces/IItemProvider.cs b/OpenKh.Tools.Kh2SystemEditor/Interfaces/IItemProvider.cs new file mode 100644 index 000000000..198b5aef8 --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/Interfaces/IItemProvider.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace OpenKh.Tools.Kh2SystemEditor.Interfaces +{ + public interface IItemProvider + { + IEnumerable ItemEntries { get; } + + bool IsItemExists(int itemId); + string GetItemName(int itemId); + void InvalidateItemName(int itemId); + } +} diff --git a/OpenKh.Tools.Kh2SystemEditor/Interfaces/IMessageProvider.cs b/OpenKh.Tools.Kh2SystemEditor/Interfaces/IMessageProvider.cs new file mode 100644 index 000000000..8bda31f3c --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/Interfaces/IMessageProvider.cs @@ -0,0 +1,8 @@ +namespace OpenKh.Tools.Kh2SystemEditor.Interfaces +{ + public interface IMessageProvider + { + string GetMessage(ushort id); + void SetMessage(ushort id, string text); + } +} diff --git a/OpenKh.Tools.Kh2SystemEditor/Interfaces/ISystemGetChanges.cs b/OpenKh.Tools.Kh2SystemEditor/Interfaces/ISystemGetChanges.cs new file mode 100644 index 000000000..a006a2724 --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/Interfaces/ISystemGetChanges.cs @@ -0,0 +1,11 @@ +using System.IO; + +namespace OpenKh.Tools.Kh2SystemEditor.Interfaces +{ + public interface ISystemGetChanges + { + string EntryName { get; } + + Stream CreateStream(); + } +} diff --git a/OpenKh.Tools.ImgzViewer/OpenKh.Tools.ImgzViewer.csproj b/OpenKh.Tools.Kh2SystemEditor/OpenKh.Tools.Kh2SystemEditor.csproj similarity index 72% rename from OpenKh.Tools.ImgzViewer/OpenKh.Tools.ImgzViewer.csproj rename to OpenKh.Tools.Kh2SystemEditor/OpenKh.Tools.Kh2SystemEditor.csproj index 163278c40..177ae642f 100644 --- a/OpenKh.Tools.ImgzViewer/OpenKh.Tools.ImgzViewer.csproj +++ b/OpenKh.Tools.Kh2SystemEditor/OpenKh.Tools.Kh2SystemEditor.csproj @@ -1,23 +1,27 @@  + WinExe net472 - IMGZ editor - Luciano (Xeeynamo) Ciccariello - IMGZ editor - Kingdom Hearts II - https://github.com/Xeeynamo/KingdomHearts - Copyright © 2018 + true + ..\bin\$(Configuration)\ true + 7 + + + + + \ No newline at end of file diff --git a/OpenKh.Tools.Kh2SystemEditor/Services/BucketService.cs b/OpenKh.Tools.Kh2SystemEditor/Services/BucketService.cs new file mode 100644 index 000000000..2da879cf2 --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/Services/BucketService.cs @@ -0,0 +1,62 @@ +using OpenKh.Kh2; +using OpenKh.Kh2.Messages; +using OpenKh.Tools.Kh2SystemEditor.Interfaces; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace OpenKh.Tools.Kh2SystemEditor.Services +{ + public class BucketService : IMessageProvider + { + private List _messages; + + public string GetMessage(ushort id) + { + var message = _messages?.FirstOrDefault(x => x.Id == (id & 0x7fff)); + if (message == null) + { + if (id == Msg.FallbackMessage) + return null; + + return GetMessage(Msg.FallbackMessage); + } + + return MsgSerializer.SerializeText(Encoders.InternationalSystem.Decode(message.Data)); + } + + public void SetMessage(ushort id, string text) + { + var message = _messages?.FirstOrDefault(x => x.Id == (id & 0x7fff)); + if (message == null) + return; + + message.Data = Encoders.InternationalSystem.Encode(MsgSerializer.DeserializeText(text).ToList()); + } + + public bool LoadMessages(Stream stream) => + TryReadMessagesAsRaw(stream) || TryReadMessagesAsBar(stream); + + private bool TryReadMessagesAsBar(Stream stream) + { + if (!Bar.IsValid(stream)) + return false; + + var entries = Bar.Read(stream); + var entry = entries.FirstOrDefault(x => x.Type == Bar.EntryType.Binary && x.Name == "sys"); + if (entry == null) + return false; + + return TryReadMessagesAsRaw(entry.Stream); + } + + private bool TryReadMessagesAsRaw(Stream stream) + { + if (!Msg.IsValid(stream)) + return false; + + _messages = Msg.Read(stream); + return true; + } + } +} diff --git a/OpenKh.Tools.Kh2SystemEditor/ViewModels/FtstViewModel.cs b/OpenKh.Tools.Kh2SystemEditor/ViewModels/FtstViewModel.cs new file mode 100644 index 000000000..ec4d631f2 --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/ViewModels/FtstViewModel.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows.Input; +using System.Windows.Media; +using OpenKh.Kh2; +using OpenKh.Kh2.System; +using OpenKh.Tools.Common; +using OpenKh.Tools.Common.Models; +using OpenKh.Tools.Kh2SystemEditor.Extensions; +using OpenKh.Tools.Kh2SystemEditor.Interfaces; +using Xe.Tools; + +namespace OpenKh.Tools.Kh2SystemEditor.ViewModels +{ + public class FtstViewModel : MyGenericListModel, ISystemGetChanges + { + public class Entry : BaseNotifyPropertyChanged + { + internal Entry(World world, int[] palette) + { + World = world; + Palette = palette; + ChangeColor = new RelayCommand(strIndex => + { + var index = int.Parse(strIndex); + var color = ToColor(palette[index]); + // INSERT COLOR PICKER HERE + palette[index] = FromColor(color); + OnAllPropertiesChanged(); + }, strIndex => + { + var index = int.Parse(strIndex); + return index >= 0 && index < palette.Length; + }); + } + + public World World { get; } + public int[] Palette { get; } + public string Name => Constants.WorldNames[(int)World]; + + public Brush Color1 => ToBrush(Palette[0]); + public Brush Color2 => ToBrush(Palette[1]); + public Brush Color3 => ToBrush(Palette[2]); + public Brush Color4 => ToBrush(Palette[3]); + public Brush Color5 => ToBrush(Palette[4]); + public Brush Color6 => ToBrush(Palette[5]); + public Brush Color7 => ToBrush(Palette[6]); + public Brush Color8 => ToBrush(Palette[7]); + public Brush Color9 => ToBrush(Palette[8]); + + public ICommand ChangeColor { get; } + + private static int FromColor(Color color) + { + var newColor = ((color.A + 1) / 2) << 24; + newColor |= color.R << 16; + newColor |= color.G << 8; + newColor |= color.B; + + return newColor; + } + private static Color ToColor(int color) + { + var ch1 = (byte)((color >> 16) & 0xFF); + var ch2 = (byte)((color >> 8) & 0xFF); + var ch3 = (byte)((color >> 0) & 0xFF); + var ch4 = (byte)(((color >> 24) & 0xFF) * 2 - 1); + return Color.FromArgb(ch4, ch1, ch2, ch3); + } + private static Brush ToBrush(int color) => new SolidColorBrush(ToColor(color)); + } + + private const string _entryName = "ftst"; + + public string EntryName => _entryName; + + public IEnumerable Palette => Map(this.ToArray()); + + public FtstViewModel() : + this(Enumerable.Range(0, Constants.PaletteCount).Select(x => new Ftst.Entry + { + Id = x, + Colors = new int[Constants.WorldCount] + })) + { } + + public FtstViewModel(IEnumerable entries) : + this(Ftst.Read(entries.GetBinaryStream(_entryName))) + { } + + public FtstViewModel(IEnumerable ftsts) : + base(Map(ftsts.ToArray())) + { + + } + + public Stream CreateStream() + { + var stream = new MemoryStream(); + Ftst.Write(stream, Palette.ToList()); + return stream; + } + + private static IEnumerable Map(Ftst.Entry[] entries) => + Enumerable.Range(0, Constants.WorldCount) + .Select(x => new Entry((World)x, GetPalette(x, entries))); + + private static IEnumerable Map(Entry[] entries) => + Enumerable.Range(0, Constants.PaletteCount) + .Select(x => new Ftst.Entry + { + Id = x, + Colors = entries.Select(worldPalette => worldPalette.Palette[x]).ToArray() + }); + + private static int[] GetPalette(int index, Ftst.Entry[] entries) => + entries.Select(x => x.Colors[index]).ToArray(); + } +} diff --git a/OpenKh.Tools.Kh2SystemEditor/ViewModels/ItemViewModel.cs b/OpenKh.Tools.Kh2SystemEditor/ViewModels/ItemViewModel.cs new file mode 100644 index 000000000..361e95c16 --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/ViewModels/ItemViewModel.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows; +using OpenKh.Kh2; +using OpenKh.Kh2.System; +using OpenKh.Tools.Common.Models; +using OpenKh.Tools.Kh2SystemEditor.Extensions; +using OpenKh.Tools.Kh2SystemEditor.Interfaces; +using Xe.Tools; +using Xe.Tools.Models; + +namespace OpenKh.Tools.Kh2SystemEditor.ViewModels +{ + public class ItemViewModel : MyGenericListModel, ISystemGetChanges, IItemProvider + { + public class Entry : BaseNotifyPropertyChanged, IItemEntry + { + private readonly IMessageProvider _messageProvider; + + public Entry(IMessageProvider messageProvider, Item.Entry item) + { + _messageProvider = messageProvider; + Item = item; + Types = new EnumModel(); + Ranks = new EnumModel(); + } + + public Item.Entry Item { get; } + + public string Title => $"{Item.Id:X02} {_messageProvider.GetMessage(Item.Name)}"; + + public ushort Id { get => Item.Id; set => Item.Id = value; } + public Item.Type Type { get => Item.Type; set => Item.Type = value; } + public byte Flag0 { get => Item.Flag0; set => Item.Flag0 = value; } + public byte Flag1 { get => Item.Flag1; set => Item.Flag1 = value; } + public Item.Rank Rank { get => Item.Rank; set => Item.Rank = value; } + public ushort StatEntry { get => Item.StatEntry; set => Item.StatEntry = value; } + public ushort NameId + { + get => Item.Name; + set + { + Item.Name = value; + OnPropertyChanged(nameof(Name)); + } + } + public ushort DescriptionId + { + get => Item.Description; + set + { + Item.Description = value; + OnPropertyChanged(nameof(Description)); + } + } + public ushort ShopBuy { get => Item.ShopBuy; set => Item.ShopBuy = value; } + public ushort ShopSell { get => Item.ShopSell; set => Item.ShopSell = value; } + public ushort Command { get => Item.Command; set => Item.Command = value; } + public ushort Slot { get => Item.Slot; set => Item.Slot = value; } + public short Picture { get => Item.Picture; set => Item.Picture = value; } + public byte Icon1 { get => Item.Icon1; set => Item.Icon1 = value; } + public byte Icon2 { get => Item.Icon1; set => Item.Icon1 = value; } + + public string IdText => $"{Id} (0x{Id:X})"; + public string Name { get => _messageProvider.GetMessage(Item.Name); set => _messageProvider.SetMessage(Item.Name, value); } + public string Description { get => _messageProvider.GetMessage(Item.Description); set => _messageProvider.SetMessage(Item.Description, value); } + public EnumModel Types { get; } + public EnumModel Ranks { get; } + + public override string ToString() => Title; + } + + private const string entryName = "item"; + private string _searchTerm; + private IMessageProvider _messageProvider; + private List _item2; + + public ItemViewModel(IMessageProvider messageProvider, IEnumerable entries) : + this(messageProvider, Item.Read(entries.GetBinaryStream(entryName))) + { } + + public ItemViewModel(IMessageProvider messageProvider) : + this(messageProvider, new Item + { + Items1 = new List(), + Items2 = new List() + }) + { } + + private ItemViewModel(IMessageProvider messageProvider, Item item) : + this(messageProvider, item.Items1) + { + _messageProvider = messageProvider; + _item2 = item.Items2; + } + + private ItemViewModel(IMessageProvider messageProvider, IEnumerable items) : + base(items.Select(item => new Entry(messageProvider, item))) + { + } + + public string EntryName => entryName; + + public Visibility IsItemEditingVisible => IsItemSelected ? Visibility.Visible : Visibility.Collapsed; + public Visibility IsItemEditMessageVisible => !IsItemSelected ? Visibility.Visible : Visibility.Collapsed; + + + public string SearchTerm + { + get => _searchTerm; + set + { + _searchTerm = value; + PerformFiltering(); + } + } + + public Stream CreateStream() + { + var stream = new MemoryStream(); + new Item + { + Items1 = this.Select(x => x.Item).ToList(), + Items2 = _item2 + }.Write(stream); + + return stream; + } + + public IEnumerable ItemEntries => this; + public bool IsItemExists(int itemId) => this.Any(x => x.Id == itemId); + public string GetItemName(int itemId) => this.FirstOrDefault(x => x.Id == itemId)?.Name; + public void InvalidateItemName(int itemId) + { + OnPropertyChanged(nameof(ItemEntries)); + } + + protected override void OnSelectedItem(Entry item) + { + base.OnSelectedItem(item); + + OnPropertyChanged(nameof(IsItemEditingVisible)); + OnPropertyChanged(nameof(IsItemEditMessageVisible)); + } + + protected override Entry OnNewItem() + { + ushort smallestUnusedId = 0; + foreach (var item in UnfilteredItems.OrderBy(x => x.Id)) + { + if (smallestUnusedId++ + 1 != item.Id) + break; + } + + return SelectedItem = new Entry(_messageProvider, new Item.Entry + { + Id = smallestUnusedId + }); + } + + private void PerformFiltering() + { + if (string.IsNullOrWhiteSpace(SearchTerm)) + Filter(FilterNone); + else + Filter(FilterByName); + } + + private bool FilterNone(Entry arg) => true; + + private bool FilterByName(Entry arg) => + arg.Title.ToUpper().Contains(SearchTerm.ToUpper()); + } +} diff --git a/OpenKh.Tools.Kh2SystemEditor/ViewModels/SystemEditorViewModel.cs b/OpenKh.Tools.Kh2SystemEditor/ViewModels/SystemEditorViewModel.cs new file mode 100644 index 000000000..4d32b06ed --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/ViewModels/SystemEditorViewModel.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Windows; +using OpenKh.Common; +using OpenKh.Kh2; +using OpenKh.Kh2.Extensions; +using OpenKh.Tools.Common; +using Xe.Tools; +using Xe.Tools.Wpf.Commands; +using Xe.Tools.Wpf.Dialogs; +using OpenKh.Tools.Kh2SystemEditor.Interfaces; +using OpenKh.Tools.Kh2SystemEditor.Services; +using OpenKh.Common.Exceptions; + +namespace OpenKh.Tools.Kh2SystemEditor.ViewModels +{ + public class SystemEditorViewModel : BaseNotifyPropertyChanged + { + private static string ApplicationName = Utilities.GetApplicationName(); + private static readonly List SystemFilter = FileDialogFilterComposer.Compose() + .AddExtensions("03system", "bin", "bar").AddAllFiles(); + private static readonly List IdxFilter = FileDialogFilterComposer.Compose() + .AddExtensions("KH2.IDX", "idx").AddAllFiles(); + private static readonly List MsgFilter = FileDialogFilterComposer.Compose() + .AddExtensions("sys.bar", "bar", "msg", "bin").AddAllFiles(); + + private Window Window => Application.Current.Windows.OfType().FirstOrDefault(x => x.IsActive); + private string _fileName; + private IEnumerable _barItems; + private BucketService _bucketService; + private ItemViewModel _item; + private TrsrViewModel _trsr; + private FtstViewModel _ftst; + + public string Title => $"{FileName ?? "untitled"} | {ApplicationName}"; + + private string FileName + { + get => _fileName; + set + { + _fileName = value; + OnPropertyChanged(nameof(Title)); + } + } + + public RelayCommand OpenCommand { get; } + public RelayCommand SaveCommand { get; } + public RelayCommand SaveAsCommand { get; } + public RelayCommand ExitCommand { get; } + public RelayCommand AboutCommand { get; } + public RelayCommand LoadSupportIdxCommand { get; } + public RelayCommand LoadSupportMsgCommand { get; } + + public ItemViewModel Item + { + get => _item; + private set { _item = value; OnPropertyChanged(); } + } + + public TrsrViewModel Trsr + { + get => _trsr; + private set { _trsr = value; OnPropertyChanged(); } + } + + public FtstViewModel Ftst + { + get => _ftst; + private set { _ftst = value; OnPropertyChanged(); } + } + + public SystemEditorViewModel() + { + OpenCommand = new RelayCommand(_ => FileDialog.OnOpen(fileName => OpenFile(fileName), SystemFilter, parent: Window)); + + SaveCommand = new RelayCommand(x => + { + if (!string.IsNullOrEmpty(FileName)) + { + SaveFile(FileName, FileName); + } + else + { + SaveAsCommand.Execute(x); + } + }, x => true); + + SaveAsCommand = new RelayCommand(_ => FileDialog.OnSave(fileName => + { + SaveFile(FileName, fileName); + FileName = fileName; + }, SystemFilter, defaultFileName: FileName, parent: Window)); + + ExitCommand = new RelayCommand(x => Window.Close()); + + AboutCommand = new RelayCommand(x => new AboutDialog(Assembly.GetExecutingAssembly()).ShowDialog()); + + LoadSupportIdxCommand = new RelayCommand(_ => FileDialog.OnOpen(fileName => OpenIdx(fileName), IdxFilter, parent: Window)); + LoadSupportMsgCommand = new RelayCommand(_ => FileDialog.OnOpen(fileName => OpenMsg(fileName), MsgFilter, parent: Window)); + + _bucketService = new BucketService(); + CreateSystem(); + } + + public bool OpenFile(string fileName) => File.OpenRead(fileName).Using(stream => + { + if (!Bar.IsValid(stream)) + { + MessageBox.Show(Window, $"{Path.GetFileName(fileName)} is not a valid BAR file.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + return false; + } + + var items = Bar.Read(stream); + + if (!Is03System(items)) + { + MessageBox.Show(Window, $"{Path.GetFileName(fileName)} does not appear to be a valid 03system.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + return false; + } + + LoadSystem(items); + + FileName = fileName; + return true; + }); + + public void SaveFile(string previousFileName, string fileName) + { + File.Create(fileName).Using(stream => + { + SaveSystem(); + Bar.Write(stream, _barItems); + }); + } + + private bool Is03System(List items) => items.Any(x => new[] + { + "item", + "trsr", + "ftst", + }.Contains(x.Name)); + + private void CreateSystem() + { + _barItems = new Bar.Entry[0]; + Item = new ItemViewModel(_bucketService); + Trsr = new TrsrViewModel(Item); + Ftst = new FtstViewModel(); + } + + private void LoadSystem(IEnumerable entries) + { + _barItems = entries; + Item = new ItemViewModel(_bucketService, _barItems); + Trsr = new TrsrViewModel(Item, _barItems); + Ftst = new FtstViewModel(_barItems); + } + + private void SaveSystem() + { + _barItems = SaveSystemEntry(_barItems, Item); + _barItems = SaveSystemEntry(_barItems, Trsr); + _barItems = SaveSystemEntry(_barItems, Ftst); + } + + private void OpenMsg(string fileName) => + File.OpenRead(fileName).Using(stream => LoadMessage(stream)); + + private void OpenIdx(string fileName) => File.OpenRead(fileName).Using(stream => + { + if (!Idx.IsValid(stream)) + throw new InvalidFileException(); + + var imgFileName = $"{Path.GetFileNameWithoutExtension(fileName)}.img"; + var imgFilePath = Path.Combine(Path.GetDirectoryName(fileName), imgFileName); + + if (!File.Exists(imgFilePath)) + { + MessageBox.Show($"Unable to find {imgFileName} in the same directory of the IDX loaded.", + "Error", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } + + File.OpenRead(imgFilePath).Using(imgStream => + { + var img = new Img(imgStream, Idx.Read(stream), false); + foreach (var language in Constants.Languages) + { + if (img.FileOpen($"msg/{language}/sys.bar", LoadMessage)) + break; + } + }); + }); + + public void LoadMessage(string fileName) => + File.OpenRead(fileName).Using(stream => LoadMessage(stream)); + + public void LoadMessage(Stream stream) + { + if (!_bucketService.LoadMessages(stream)) + return; + + SaveSystem(); + LoadSystem(_barItems); + } + + private IEnumerable SaveSystemEntry(IEnumerable entries, ISystemGetChanges battleGetChanges) => + entries.ForEntry(Bar.EntryType.Binary, battleGetChanges.EntryName, 0, entry => entry.Stream = battleGetChanges.CreateStream()); + + + private T GetDefaultViewModelInstance() + where T : ISystemGetChanges => Activator.CreateInstance(); + } +} diff --git a/OpenKh.Tools.Kh2SystemEditor/ViewModels/TrsrViewModel.cs b/OpenKh.Tools.Kh2SystemEditor/ViewModels/TrsrViewModel.cs new file mode 100644 index 000000000..98d3c42fe --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/ViewModels/TrsrViewModel.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Windows; +using OpenKh.Kh2; +using OpenKh.Kh2.System; +using OpenKh.Tools.Common.Models; +using OpenKh.Tools.Kh2SystemEditor.Extensions; +using OpenKh.Tools.Kh2SystemEditor.Interfaces; +using Xe.Tools; +using Xe.Tools.Models; + +namespace OpenKh.Tools.Kh2SystemEditor.ViewModels +{ + public class TrsrViewModel : MyGenericListModel, ISystemGetChanges + { + public class Entry : BaseNotifyPropertyChanged + { + public Entry(IItemProvider itemProvider, Trsr treasure) + { + ItemProvider = itemProvider; + Treasure = treasure; + Worlds = new Kh2WorldsList(); + Types = new EnumModel(); + } + + public Trsr Treasure { get; } + public IItemProvider ItemProvider { get; } + + public string Title => $"{Treasure.Id:X03} {MapName} {ItemName}"; + public string Query => $"{Id} {Title} {Type} {World} {RoomChestIndex} {EventId}"; + + public ushort Id { get => Treasure.Id; set => Treasure.Id = value; } + + public ushort ItemId + { + get => Treasure.ItemId; + set + { + Treasure.ItemId = value; + OnPropertyChanged(nameof(Title)); + } + } + public Trsr.TrsrType Type { get => Treasure.Type; set => Treasure.Type = value; } + public World World + { + get => (World)Treasure.World; + set + { + Treasure.World = (byte)value; + OnPropertyChanged(nameof(Title)); + OnPropertyChanged(nameof(MapName)); + } + } + public byte Room + { + get => Treasure.Room; + set + { + Treasure.Room = value; + OnPropertyChanged(nameof(Title)); + OnPropertyChanged(nameof(MapName)); + } + } + public byte RoomChestIndex { get => Treasure.RoomChestIndex; set => Treasure.RoomChestIndex = value; } + public short EventId { get => Treasure.EventId; set => Treasure.EventId = value; } + public short OverallChestIndex { get => Treasure.OverallChestIndex; set => Treasure.OverallChestIndex = value; } + + public string IdText => $"{Id} (0x{Id:X})"; + public string MapName => $"{Constants.WorldIds[(int)World]}_{Room:D02}"; + public string ItemName => ItemProvider.GetItemName(ItemId); + + public Kh2WorldsList Worlds { get; } + public EnumModel Types { get; } + + public override string ToString() => Title; + } + + private const string entryName = "trsr"; + private readonly IItemProvider _itemProvider; + private string _searchTerm; + + public TrsrViewModel(IItemProvider itemProvider, IEnumerable entries) : + this(itemProvider, Trsr.Read(entries.GetBinaryStream(entryName))) + { } + + public TrsrViewModel(IItemProvider itemProvider) : + this(itemProvider, new Trsr[0]) + { } + + private TrsrViewModel(IItemProvider itemProvider, IEnumerable items) : + base(items.Select(item => new Entry(itemProvider, item))) + { + _itemProvider = itemProvider; + } + + public string EntryName => entryName; + + public Visibility IsItemEditingVisible => IsItemSelected ? Visibility.Visible : Visibility.Collapsed; + public Visibility IsItemEditMessageVisible => !IsItemSelected ? Visibility.Visible : Visibility.Collapsed; + + public string SearchTerm + { + get => _searchTerm; + set + { + _searchTerm = value; + PerformFiltering(); + } + } + + public Stream CreateStream() + { + var stream = new MemoryStream(); + Trsr.Write(stream, this.Select(x => x.Treasure)); + + return stream; + } + + protected override void OnSelectedItem(Entry item) + { + base.OnSelectedItem(item); + + OnPropertyChanged(nameof(IsItemEditingVisible)); + OnPropertyChanged(nameof(IsItemEditMessageVisible)); + } + + protected override Entry OnNewItem() + { + ushort smallestUnusedId = 0; + foreach (var item in UnfilteredItems.OrderBy(x => x.Id)) + { + if (smallestUnusedId++ + 1 != item.Id) + break; + } + + return SelectedItem = new Entry(_itemProvider, new Trsr + { + Id = smallestUnusedId + }); + } + + private void PerformFiltering() + { + if (string.IsNullOrWhiteSpace(SearchTerm)) + Filter(FilterNone); + else + Filter(FilterByName); + } + + private bool FilterNone(Entry arg) => true; + + private bool FilterByName(Entry arg) + { + var query = arg.Query.ToUpper(); + return SearchTerm.ToUpper().Split(new char[] { ',', ' ' }).All(term => query.Contains(term)); + } + } +} diff --git a/OpenKh.Tools.Kh2SystemEditor/Views/FtstView.xaml b/OpenKh.Tools.Kh2SystemEditor/Views/FtstView.xaml new file mode 100644 index 000000000..f9678816e --- /dev/null +++ b/OpenKh.Tools.Kh2SystemEditor/Views/FtstView.xaml @@ -0,0 +1,78 @@ + + + 30 + + + + + + + + + + + + +