diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 831d039b..20572a9e 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -7,6 +7,12 @@ "commands": [ "dotnet-cake" ] + }, + "docfx": { + "version": "2.70.3", + "commands": [ + "docfx" + ] } } } \ No newline at end of file diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 11e46ca3..08ac2044 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -37,7 +37,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: "Build, test and stage" - run: dotnet cake --target=Stage-Artifacts --configuration=Release --verbosity=diagnostic + run: dotnet cake --target=Stage-Artifacts-NewDocs --configuration=Release --verbosity=diagnostic - name: "Publish test results" uses: actions/upload-artifact@v2 diff --git a/.gitignore b/.gitignore index f4d82a9f..671ae458 100644 --- a/.gitignore +++ b/.gitignore @@ -11,9 +11,5 @@ artifacts/ # Cake tools/* -# Docs -docs/_site/ -docs/api/ - # Benchmarks BenchmarkDotNet.Artifacts/ diff --git a/.vscode/settings.json b/.vscode/settings.json index 092fb331..29456b5f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,10 +17,12 @@ "Conv", "cref", "Dependee", + "Ekona", "Diagnoser", "finalizer", "msgctxt", "Msgid", "typeparam" ], + "dotnet.defaultSolution": "src/Yarhl.sln", } \ No newline at end of file diff --git a/README.md b/README.md index 3e6debe4..64b9f275 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,117 @@ -# Yarhl: Yet Another ROM Hacking Library [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://choosealicense.com/licenses/mit/) +# Yarhl, A format ResearcH Library [![awesomeness](https://img.shields.io/badge/SceneGate-awesome%20%F0%9F%95%B6-blue?logo=csharp)](https://github.com/SceneGate) ![Yarhl Logo](https://raw.githubusercontent.com/SceneGate/Yarhl/develop/docs/images/logo.png) -**Yarhl** is a library for _ROM Hacking_ and fan-translation projects. It -provides a virtual file system, file format and format conversion features and -plugin support. It's built in C# / .NET and works in Windows, Linux and Mac OS -X. - - -| NuGet | [![Yarhl](https://img.shields.io/nuget/v/Yarhl?label=Yarhl)](https://www.nuget.org/packages/Yarhl) [![Yarhl.Media.Text](https://img.shields.io/nuget/v/Yarhl.Media.Text?label=Yarhl.Media.Text)](https://www.nuget.org/packages/Yarhl.Media.Text) | -| ------------------ | ------ | -| **Build & Test** | ![Build and release](https://github.com/SceneGate/Yarhl/workflows/Build%20and%20release/badge.svg?branch=develop) | -| **Quality report** | [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/2919/badge)](https://bestpractices.coreinfrastructure.org/projects/2919) | - -## Documentation + +

+ + Stable version + +   + + GitHub commits since latest release (by SemVer) + +   + + Build and release + +   + + CII Best Practices + +   + + MIT License + +   +

+ +**Yarhl** is a set of libraries that helps to **implement and convert file +formats**. It provides a virtual file system, format conversion APIs, full +featured binary IO and plugin support to support common formats. It's built in +**C# / .NET** and works in any OS that supports the .NET runtime. + +- :books: **Format implementation** architecture and guidelines +- :recycle: **Format conversion** API +- :open_file_folder: **Virtual file system** with format transformations +- :1234: **Enhanced binary IO API** + - Custom `Stream` with **sub-stream supports** (memory and disk efficient!) + - Full feature binary and text readers and writers + - Simple binary (de)serializer by attributes in the model. +- :page_with_curl: Standard text formats + - Industry-standard localization format: **GNU gettext PO** + - Table text replacements + - **Common encodings**: euc-jp, token-escaped encoding + - **API for simple encoding implementations** + +## Get started + +Check out the [documentation site](https://scenegate.github.io/Yarhl/index.html) +to start learning the power of _Yarhl_. Feel free to ask any question in the -[project Discussion site!](https://github.com/SceneGate/Yarhl/discussions). - -Check our on-line API overview: -[Yarhl in a nutshell](https://scenegate.github.io/Yarhl/guides/Yarhl-nutshell.html) -and the complete API documentation -[here](https://scenegate.github.io/Yarhl/api/Yarhl.html). +[project discussions](https://github.com/SceneGate/Yarhl/discussions). -## Install +## Usage -Stable releases are available from nuget.org: +This project provides the following libraries as NuGet packages (via nuget.org). +The libraries support the latest version of .NET and its LTS. -- [Yarhl](https://www.nuget.org/packages/Yarhl) -- [Yarhl.Media.Text](https://www.nuget.org/packages/Yarhl.Media.Text) +- [![Yarhl](https://img.shields.io/nuget/v/Yarhl?label=Yarhl&logo=nuget)](https://www.nuget.org/packages/Yarhl): + core, format conversion, file system and binary reading / writing (IO). +- [![Yarhl.Media.Text](https://img.shields.io/nuget/v/Yarhl.Media.Text?label=Yarhl.Media.Text&logo=nuget)](https://www.nuget.org/packages/Yarhl.Media.Text): + text formats (Po) and encodings. +- [![Yarhl.Plugins](https://img.shields.io/nuget/v/Yarhl.Plugins?label=Yarhl.Plugins&logo=nuget)](https://www.nuget.org/packages/Yarhl.Plugins): + discover formats and converters from .NET assemblies. -The libraries only support the latest version of .NET and its LTS (.NET 6). - -Preview releases can be found in this +**Preview releases** can be found in this [Azure DevOps package repository](https://dev.azure.com/SceneGate/SceneGate/_packaging?_a=feed&feed=SceneGate-Preview). To use a preview release, create a file `nuget.config` in the same directory of -your solution (.sln) file with the following content: +your solution file (.sln) with the following content: ```xml + + + + + + + + + + ``` -## Build +Then restore / install as usual via Visual Studio, Rider or command-line. You +may need to restart Visual Studio for the changes to apply. + +## Showcase + +Some cool projects built with _Yarhl_: -The project requires to build .NET 6.0 SDK and .NET Framework 4.8 or latest -Mono. If you open the project with VS Code and you did install the +- [**Texim**](https://github.com/SceneGate/Texim): experimental API for image + file formats. +- [**Ekona**](https://scenegate.github.io/Ekona/): support Nintendo DS file + formats. +- [**Lemon**](https://scenegate.github.io/Lemon/): support Nintendo 3DS file + formats. +- [**LayTea**](https://www.pleonex.dev/LayTea/): modding tools for _Professor + Layton_ games. +- [**Attack of Friday Monsters tools**](https://github.com/pleonex/AttackFridayMonsters): + modding tools for _Attack of the Friday Monsters_ game. +- [**Metatron**](https://github.com/TraduSquare/Metatron): translation framework + for _Shin Megami Tensei_ saga games. + +## Contributing + +The repository requires to build .NET 6.0 SDK and .NET Framework 4.8 or latest +Mono (for DocFX). If you open the project with VS Code and you did install the [VS Code Remote Containers](https://code.visualstudio.com/docs/remote/containers) extension, you can have an already pre-configured development environment with Docker or Podman. @@ -60,7 +122,7 @@ To build, test and generate artifacts run: # Only required the first time dotnet tool restore -# Default target is Stage-Artifacts +# Default target is "Default" that builds, runs tests, build doc and create the NuGets dotnet cake ``` @@ -69,3 +131,13 @@ To just build and test quickly, run: ```sh dotnet cake --target=BuildTest ``` + +Additionally you can use _Visual Studio_ or _JetBrains Rider_ as any other .NET +project. + +To contribute follow the [contributing guidelines](CONTRIBUTING.md). + +## License + +The software is licensed under the terms of the +[MIT license](https://choosealicense.com/licenses/mit/). diff --git a/build.cake b/build.cake index 08d0bba8..83401498 100644 --- a/build.cake +++ b/build.cake @@ -11,9 +11,31 @@ Task("Define-Project") info.AddTestProjects("Yarhl.UnitTests"); info.AddTestProjects("Yarhl.IntegrationTests"); + info.ChangelogFile = "docs/articles/Changelog.md"; + info.PreviewNuGetFeed = "https://pkgs.dev.azure.com/SceneGate/SceneGate/_packaging/SceneGate-Preview/nuget/v3/index.json"; }); +Task("DocFx-BuildDoc") + .Does(info => +{ + if (!FileExists(info.DocFxFile)) { + Warning("There isn't documentation."); + return; + } + + string args = $"-o {info.ArtifactsDirectory}/_site"; + if (info.WarningsAsErrors) { + args += " --warningsAsErrors"; + } + + DotNetTool($"docfx {info.DocFxFile} {args}"); + + Zip( + $"{info.ArtifactsDirectory}/_site", + $"{info.ArtifactsDirectory}/docs.zip"); +}); + Task("Prepare-IntegrationTests") .Description("Prepare the integration tests by copying an example of DLL") .IsDependeeOf("Test") @@ -53,7 +75,13 @@ public IEnumerable GetTargetFrameworks(string projectPath) } Task("Default") - .IsDependentOn("Stage-Artifacts"); + .IsDependentOn("Stage-Artifacts-NewDocs"); + +Task("Stage-Artifacts-NewDocs") + .IsDependentOn("BuildTest") + .IsDependentOn("DocFx-BuildDoc") + .IsDependentOn("Pack-Libs") + .IsDependentOn("Pack-Apps"); string target = Argument("target", "Default"); RunTarget(target); diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..83cc63a9 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,12 @@ +############### +# folder # +############### +/**/DROP/ +/**/TEMP/ +/**/packages/ +/**/bin/ +/**/obj/ +_site + +# DrawIO +.$*.drawio* diff --git a/docs/api/.gitignore b/docs/api/.gitignore new file mode 100644 index 00000000..e8079a3b --- /dev/null +++ b/docs/api/.gitignore @@ -0,0 +1,5 @@ +############### +# temp file # +############### +*.yml +.manifest diff --git a/docs/dev/Changelog.md b/docs/articles/Changelog.md similarity index 100% rename from docs/dev/Changelog.md rename to docs/articles/Changelog.md diff --git a/docs/articles/core/binary/attr-serialization.md b/docs/articles/core/binary/attr-serialization.md new file mode 100644 index 00000000..0914beaa --- /dev/null +++ b/docs/articles/core/binary/attr-serialization.md @@ -0,0 +1,3 @@ +# (De)serialization via attributes + +TODO diff --git a/docs/articles/core/binary/binaryformat.md b/docs/articles/core/binary/binaryformat.md new file mode 100644 index 00000000..e39fbfd8 --- /dev/null +++ b/docs/articles/core/binary/binaryformat.md @@ -0,0 +1,3 @@ +# Binary format + +TODO diff --git a/docs/articles/core/binary/binaryreader-writer.md b/docs/articles/core/binary/binaryreader-writer.md new file mode 100644 index 00000000..67a5bf70 --- /dev/null +++ b/docs/articles/core/binary/binaryreader-writer.md @@ -0,0 +1,3 @@ +# Binary reader and writer + +TODO diff --git a/docs/articles/core/binary/custom-streams.md b/docs/articles/core/binary/custom-streams.md new file mode 100644 index 00000000..fb37f0f0 --- /dev/null +++ b/docs/articles/core/binary/custom-streams.md @@ -0,0 +1,3 @@ +# Custom streams + +TODO diff --git a/docs/articles/core/binary/datastream.md b/docs/articles/core/binary/datastream.md new file mode 100644 index 00000000..55525a66 --- /dev/null +++ b/docs/articles/core/binary/datastream.md @@ -0,0 +1,276 @@ +# DataStream + +[`DataStream`](xref:Yarhl.IO.DataStream) is an enhanced version of .NET +[`Stream`](xref:System.IO.Stream) class. It inherits `Stream`, being compatible +with any existing API that uses `Stream` as input. + +Its main feature is the ability to create thread-safe **_sub-streams_**. It +allows to work with a segment of a `Stream` without copying or allocating any +memory. + +## Sub-streams + +A _sub-stream_ is _view_ of a part of a regular **Stream**. Just like in .NET +[`Span`](xref:System.Span`1) allows to work with a segment of an array, +`DataStream` allows to do it with a `Stream` but keeping the same API as any +other regular `Stream`. + +When you create a `DataStream` from another `Stream` you can specify its +_offset_ and _length_ to perform IO operations (reading / writing) only in that +segment. + +Let's imagine we have a binary format for a container (like a _zip_ or _tar_ +file without compression). In this format the content for the first file is in +the position `0x100`. The content for the second file is at `0x3C0`. + +![Container format with a header and 3 files inside](./images/datastream-container.drawio.png) + +If we want to only work with the data of the second file, we could create a +_sub-stream_ that starts at `0x3C0` and has the file length. It will allow to +have a `Stream` object based on the parent `Stream`. We can now pass this new +stream to any other API, for instance to a JSON deserializer to read its +content, without having to _export_ or do any prior copy. + +![Container format with assigned substreams to each file](./images/datastream-container-substreams.drawio.png) + +Another use case is reading a binary format with sections. By creating a +`DataStream` for each individual section, the application can have a design more +modular and safe. It would prevent reading data outside the range of the +section. + +## Factory + +The constructors of `DataStream` takes a `Stream` with optional offset and +lengths for _sub-streams_. You can also create a `DataStream` from other source +via the [`DataStreamFactory`](xref:Yarhl.IO.DataStreamFactory). + +It can quickly initialize a new `DataStream` from memory: + +```csharp +using var stream = DataStreamFactory.FromMemory(); +``` + +> [!TIP] +> Internally it uses the memory-efficient API from Microsoft: +> [`Microsoft.IO.RecyclableMemoryStream`](https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream). +> It keeps a range of buffers on memory allowing to re-use them without having +> to allocate memory twice. It may however consume more memory even when it's +> not used. + +or from an array of bytes: + +```csharp +byte[] dataBuffer = ... + +using var stream = DataStreamFactory.FromArray(dataBuffer); +``` + +or from an existing or new file: + +```csharp +using var stream = DataStreamFactory.FromFile("input/file.bin", FileOpenMode.Read); + +// or even a part of a file: only 0x40 bytes from 0x100. +using var stream = DataStreamFactory.FromFile("input/file.bin", FileOpenMode.Read, 0x100, 0x40); +``` + +where [`FileOpenMode`](xref:Yarhl.IO.FileOpenMode) is an enumeration that +internally maps to the .NET enumerations `FileMode` and `FileAccess` as follow: + +- `FileOpenMode.Read`: read a file, throwing an exception if it doesn't exists. + Maps to `FileMode.Open` and `FileAccess.Read`. +- `FileOpenMode.Write`: write a file, creating a new if it doesn't exist or + truncating it it does. Maps to `FileMode.Create` and `FileAccess.Write`. +- `FileOpenMode.ReadWrite`: allow to read and write to a file. If it doesn't + exist it will create a new one but it does **not truncate if the file exists** + (to allow reading). Maps to `FileMode.OpenOrCreate` and + `FileAccess.ReadWrite`. +- `FileOpenMode.Append`: append data to the end, if it doesn't exist it will + throw an exception. Maps to `FileMode.Append` and `FileAccess.Write` + +> [!NOTE] +> The file is _lazily open_. This means it will not actually open the file until +> the first read or write operation happens on the `Stream`. It allows having +> hundreds of `DataStream` on different files ready without running out of +> resources in the operative system. + +> [!TIP] +> `FileOpenMode` is handy enumeration that covers most use cases. If you require +> any other combination of `FileMode` and `FileAccess` you can create the +> `FileStream` by hand and pass it to the `DataStream` constructor or +> `FromStream` methods. + +Finally the factory also provides the methods +[`FromStream`]() +similar to the parameters accepted by the constructor of +[`DataStream`](xref:Yarhl.IO.DataStream). + +## IO operations + +`DataStream` supports the same read and write operations as any other standard +.NET `Stream`. It also provides some additional methods. + +### Writing to a file + +The [`WriteTo`]() API allows to +write the entire content of the stream into a file on the given path. This +applies to the content that targets this `DataStream`, not the entire parent +`Stream`. + +> [!NOTE] +> The method ignores the current position, it will always start writing from the +> start to the end. It will **restore** the current position after comparing. + +The path should point to the output file. If there is any directory that doesn't +exist, it will create them first. + +```csharp +using var stream = new DataStream(...); +// ... +stream.Position = 80; +// ... + +// Write the entire Stream from 0 to the end. +// It creates the folder 'output' if it doesn't exist. +stream.WriteTo("output/file.bin"); +``` + +### Comparing data + +The method [`Compare`]() +allows to compare byte-to-byte the content of the current stream against +another. If will return `false` if any byte between the two streams are +different. It will also return `false` if the length does not match. + +> [!NOTE] +> The method ignores the current position. It will always start reading **both +> Streams** from the start to the end. It will **restore** the current position +> after comparing. + +```csharp +using var myStream = new DataStream(...); +myStream.Position = 80; + +using var otherFile = new FileStream(...); + +// Compare both streams from 0 to end +bool result = myStream.Compare(otherFile); +``` + +### Positions stack + +Inspired by the _Unix commands `pushd` and `popd`_, `DataStream` provides a set +of APIs that simplifies the use cases of changing position to read / write some +data and go back. + +For instance, imagine the format has a table with file names in a separate +section. We could quickly read the string without having to store the current +position: + +```csharp +stream.Position = 0x100; + +// Save current position and move to 0x800 +stream.PushToPosition(0x800); +string name = ReadFilename(stream); +stream.PopPosition(); // return to 0x100, keep reading from there +``` + +[`PushToPosition`]() +will move to the given position, saving the current position in an internal +stack. Calling [`PopPosition`](xref:Yarhl.IO.DataStream.PopPosition) will return +to our original position. + +The class also provides +[`PushCurrentPosition`](xref:Yarhl.IO.DataStream.PushCurrentPosition). It saves +the current position into the stack. For instance, before calling an external +method that may modify our position. + +Finally, the method +[`RunInPosition`]() +allows to run an action such as a lambda expression or a method saving and +restoring our current position. We could re-write the previous example as: + +```csharp +stream.Position = 0x100; + +string name; +stream.RunInPosition(() => name = ReadFilename(stream), 0x800); +``` + +### Writing segments + +The +[`WriteSegmentTo`]() +APIs allows to write a part of our `DataStream` into `Stream` or file on disk. +This is a shortcut to create a temporary `DataStream` and run the regular +`WriteTo` method. + +```csharp +var stream = new DataStream(...); + +// Write starting at 0x100 to the end +stream.WriteSegmentTo(0x100, otherStream); +stream.WriteSegmentTo(0x100, "outputs/data.bin"); + +// Write 0x40 bytes starting at 0x800 +stream.WriteSegmentTo(0x800, 0x40, otherStream); +stream.WriteSegmentTo(0x800, 0x40, "outputs/data.bin"); +``` + +### Closing / Disposing + +By default a `DataStream` takes the _ownership_ of the base `Stream` object +passed. By disposing the `DataStream` it will dispose as well its parent +`Stream`. + +If we are working with _sub-streams_ the behavior is different. The API will +keep a **count of `DataStream` using a `Stream` object.** Disposing a +`DataStream` will decrease the count. Creating a new `DataStream` (via +constructor or factory) will increase the count. When the count reaches 0 in a +`Dispose` object, it will also dispose its parent. + +In the above format example, disposing the `DataStream` for file 1 and 2 will +not still dispose its parent `FileStream`, as we have still one for file 80. +Calling `Dispose` for the `DataStream` of file 80, will dispose the `FileStream` +as well. + +```csharp +var parentStream = new FileStream(...); + +var file1Stream = new DataStream(parentStream, 0x100, 0x2C0); +var file2Stream = new DataStream(parentStream, 0x3C0, 0x80); +var file80Stream = new DataStream(parentStream, 0x8400, 0x100); + +file1Stream.Dispose(); +file2Stream.Dispose(); + +// parentStream is still open. We can do read and write from it or file80Stream + +file80Stream.Dispose(); // <-- It will also dispose parentStream +``` + +You can control this behavior by using the +[constructor that takes the argument `bool transferOwnership`](). +Pass a `false` value to never dispose the parent `Stream`. + +> [!TIP] +> You can get the total number of `DataStream` created via the static property +> [`ActiveStreams`](xref:Yarhl.IO.DataStream.ActiveStreams). + +## Thread-safety + +The `DataStream` is thread-safe at the level of the sub-stream. + +For instance, it is safe to use several `DataStreams` over the same base +`Stream` in parallel. `DataStream` will wait for their turn to use the parent +`Stream` for each IO operation. Ensure they are at the required position always. + +The type **is not thread-safe for its methods**. For instance, it is **NOT safe +to use the same `DataStream`** in different threads at the same time. It's not +safe to change the position of the `DataStream` in one thread while another is +reading. **Please create a new `DataStream` for each thread.** + +The thread-safety is based in that there isn't any other part of the application +accessing to the parent `Stream` directly. Every IO operation should happen over +a `DataStream`. diff --git a/docs/articles/core/binary/images/datastream-container-substreams.drawio.png b/docs/articles/core/binary/images/datastream-container-substreams.drawio.png new file mode 100644 index 00000000..e9472de9 Binary files /dev/null and b/docs/articles/core/binary/images/datastream-container-substreams.drawio.png differ diff --git a/docs/articles/core/binary/images/datastream-container.drawio.png b/docs/articles/core/binary/images/datastream-container.drawio.png new file mode 100644 index 00000000..fd84acbb Binary files /dev/null and b/docs/articles/core/binary/images/datastream-container.drawio.png differ diff --git a/docs/articles/core/binary/textreader-writer.md b/docs/articles/core/binary/textreader-writer.md new file mode 100644 index 00000000..c7c3ecab --- /dev/null +++ b/docs/articles/core/binary/textreader-writer.md @@ -0,0 +1,3 @@ +# Text reader and writer + +TODO diff --git a/docs/articles/core/formats/cloneable-format.md b/docs/articles/core/formats/cloneable-format.md new file mode 100644 index 00000000..840ba5e0 --- /dev/null +++ b/docs/articles/core/formats/cloneable-format.md @@ -0,0 +1,26 @@ +# Cloneable format + +.NET does not provide an interface to guarantee a +[deep clone](https://learn.microsoft.com/en-us/dotnet/api/system.icloneable?view=net-7.0#remarks) +implementation. + +The [`ICloneableFormat`](xref:Yarhl.FileFormat.ICloneableFormat) gives the +possibility to a format implementation to specify how it should _deep_ clone its +data into a new format. This could be as simple as copying its properties into a +new object or in the case of binary data, copying all its bytes into a new +stream. + +[!code-csharp[cloneable](./../../../../src/Yarhl.Examples/Formats/Formats.cs?name=CloneableFormat)] + +The interface already implements `IFormat` so it's not needed to add both. + +> [!IMPORTANT] +> This interface is not required to be implemented by every format but some APIs +> of the library relies on it. For instance it's only possible to clone a +> [node via its constructor]() +> if it has a format that implements +> [`ICloneableFormat`](xref:Yarhl.FileFormat.ICloneableFormat). + +> [!TIP] +> The built-in formats from _Yarhl_ implements +> [`ICloneableFormat`](xref:Yarhl.FileFormat.ICloneableFormat). diff --git a/docs/articles/core/formats/converters-usecases.md b/docs/articles/core/formats/converters-usecases.md new file mode 100644 index 00000000..cafee377 --- /dev/null +++ b/docs/articles/core/formats/converters-usecases.md @@ -0,0 +1,156 @@ +# Advanced uses cases for converters + +The [converters](./converters.md) topic covers the standard use case: _convert +one format into another_. Often you may run into scenarios a bit more advanced. +The following sections tries to provide some architecture guidance. + +## Convert multiple formats into one + +**Requirement**: generate a format from more than one input formats. + +Depending on the use case the following patterns could help: + +- Implement a converter that converts one format (main) and **use parameters** + to pass the additional formats. +- Use the [import data](#updating--importing-data-in-a-format) pattern. + +Let's try the first pattern with a couple of examples: + +### Serialize a font file from an image and JSON + +We have exported a font information into multiple files: one file containing an +image with all the font glyphs and a JSON file with the metadata and charset +map. + +In this case we can identify the _main_ format as the JSON structure, as it +contains most of the information required to create the font. We will pass the +image as a parameter to be used for the glyphs of the font. + +[!code-csharp[Font2Binary example](../../../../src/Yarhl.Examples/Formats/AdvancedConverters.cs?name=ManyToOneFont&highlight=5)] + +> [!NOTE] +> Instead of passing the JSON as binary data, pre-convert it already into its +> structure / class. It will simplify the implementation of the converter and it +> could be it can be re-used for more cases (e.g. in the future you decide to +> support YAML). + +### Convert an indexed image with a palette into a RGB image + +The _main_ format would be the indexed image as contains more information +representing the target format. A palette is required to transform the pixel +indexes into a final RGB color. + +[!code-csharp[IndexedImage2RgbImage example](../../../../src/Yarhl.Examples/Formats/AdvancedConverters.cs?name=ManyToOneIndexedImage&highlight=5)] + +### Additional patterns for many to one + +Below other implementations that may fit some use cases. In our experience they +don't work as good as the previous mentioned _parameter_ approach. + +#### Intermediary types + +Create an intermediary type that groups all the required formats to convert. For +instance you could create a class `IndexedImageWithPalette` to gather the +`IndexedImage` and `Palette`. Then create a converter for +`IConverter`. + +This may simplify your converter but it can create more complex APIs. Now users +will need to _convert_ their two formats into this intermediary representation +before they can use the converter. + +It may prevent a _fluent-like_ usage of the converters when used with the +[node](../virtual-file-system/nodes.md#format-conversion) APIs. It won't allow +to convert one _node_ passing other _node_ as parameters. + +#### Using tuples as input type + +This similar to the above case. It has the further limitation that it can't +evolve over the time. If you need an additional format or parameter in the +future you will breaking the API for the users making it a bit more messy. + +## Convert one format into many + +**Requirement**: convert the format into more than one output formats. + +Depending on the format you may want to: + +- **Convert the format into a container type `NodeContainerFormat`** that + contains a child per output format. +- Create a **separate** converter for each target format. + +> [!TIP] +> Check-out the [node](../virtual-file-system/nodes.md) topic to learn more +> about containers. + +### Convert an RGB image into indexed image and palette + +Reverse operation from +[convert an indexed image with palette into RGB image](#convert-an-indexed-image-with-a-palette-into-a-rgb-image). +The converter generates a palette and an image with _indexed pixels_. It needs +to return both formats as they are generated at the same time. + +The approach is to return a container that has two nodes: `image` and `palette`. + +[!code-csharp[RgbImage2IndexedImage example](../../../../src/Yarhl.Examples/Formats/AdvancedConverters.cs?name=OneToManyIndexedImage)] + +We can extract the formats from this container as follow: + +[!code-csharp[Using previous converter](../../../../src/Yarhl.Examples/Formats/AdvancedConverters.cs?name=OneToManyIndexedImageProgram)] + +### Export a font into information and image + +In this case it could be a better approach to separate the converters: + +1. A `Font2BinaryInfo` converter that serializes the charset map and other + information into JSON / YAML `Stream`. +2. A `Font2Image` converter that exports the glyphs into an image. + +Each converter runs a different process to generate the output. These two output +formats are not generated at the same time (as it was the case above). + +By splitting it allows users to run the one they need when they need it. It may +not be required to generate an image all the time or vice-versa. + +## Convert multiple formats into many + +This use case would be covered by the two previous cases: combining converting +[multiple formats into one](#convert-multiple-formats-into-one) and +[one format into many](#convert-one-format-into-many). + +## Updating / Importing data in a format + +Sometimes you may run a process that modifies existing data of a format +**without creating a new format**. + +In these cases we can create a converter that **returns the same input +instance** after processing. We can pass the data to import as a **parameter**. + +### Importing a font file + +One example would be importing data from multiple formats over the same object. +For instance, if we need to _import_ / create a `Font` object from a JSON file +and an `Image` with the glyphs. We could do this scenario in two steps: + +1. One converter that creates the `Font` object from the JSON file: _binary -> + Font_ with `IConverter`. +2. Then, one converter that imports the glyphs images over the same `Font` + object: _Font -> Font_ with `IConverter` + +The structure of the second converter could look as follow: + +[!code-csharp[FontImporter example](../../../../src/Yarhl.Examples/Formats/AdvancedConverters.cs?name=FontImporter)] + +### Updating texts of an executable file + +Another scenario is changing the text of an unknown or complex binary format +like an executable. In that case we want to maintain all the existing bytes and +overwrite the ones containing text with new data. + +[!code-csharp[ExecutableTextImporter example](../../../../src/Yarhl.Examples/Formats/AdvancedConverters.cs?name=ExecutableTextImporter)] + +> [!TIP] +> It could be a good idea to create a **new `BinaryFormat` and copy the input +> before overwriting data**. In that case you would be returning a new binary +> format but with the existing content. In this way you don't modify the +> existing file on disk but create a new one in case something wrong happens and +> you want to run it again. diff --git a/docs/articles/core/formats/converters.md b/docs/articles/core/formats/converters.md new file mode 100644 index 00000000..15269146 --- /dev/null +++ b/docs/articles/core/formats/converters.md @@ -0,0 +1,187 @@ +# Converters + +You can convert a [formats](./formats.md) (model) into another format by using a +_converter_ class. A _Yarhl converter_ implements the interface +[`IConverter`](xref:Yarhl.FileFormat.IConverter`2) and provides the +method [`TDst Convert(TSrc)`](). +This method creates a new object in the target type _converting_ the data from +the input. + +For instance the converter [`Po2Binary`](xref:Yarhl.Media.Text.Po2Binary) +implements `IConverter`. It allows to convert a +[`Po`](xref:Yarhl.Media.Text.Po) model format into a +[_binary_ format](xref:Yarhl.IO.BinaryFormat). This is also known as +**serialization**. You can later write this binary data into a file on disk. + +In a similar way, the converter [`Binary2Po`](xref:Yarhl.Media.Text.Binary2Po) +implements `IConverter` to convert binary data into a +[`Po`](xref:Yarhl.Media.Text.Po) model (also known as _reading_ or +_deserializing_). + +We could have more conversions between formats. For instance +`IConverter` or `IConverter`. This is sometimes referred +as _exporting_ and _importing_ formats. _Converters_ simplify all these +operations by their common denominator: **converting models.** + +Let's see how to _serialize_ / convert a _Po_ model into binary data to write on +disk: + +[!code-csharp[serialize PO](./../../../../src/Yarhl.Examples/Formats/Converters.cs?name=SerializePo)] + +## Implementing a new converter + +To create a new converter, create a new class and implement the interface +[``](xref:Yarhl.FileFormat.IConverter`2). `TSrc` is the +type (or base type / interface) you are going to convert into a new object of +`TDst` type. + +> [!NOTE] +> It is possible to have a class implementing more than one converter at a type. +> However this can be confusing for the user. Our recommendation is that each +> class implements only one +> [`IConverter`](xref:Yarhl.FileFormat.IConverter`2) interface. For +> instance, create `Po2Binary` and `Binary2Po` instead of just `Binary2Po` +> having the two implementations. + +As an example, let's implement a new converter that reads binary data and +creates a [container type](../virtual-file-system/nodes.md) (like a file +system). + +First we create a new class for our converter: `BinaryArchive2Container` to do +the operation _binary data_ -> _container class_ (deserializing). + +```csharp +public class BinaryArchive2Container : IConverter +{ + // TODO: Implement interface. +} +``` + +Now let's add the required method `Convert` for the interface. + +```csharp +public NodeContainerFormat Convert(IBinary source) +{ + var container = new NodeContainerFormat(); + // TODO: do something with the source data. + return container; +} +``` + +Finally let's read some data to fill the container. This example binary format +contains a set of binary files inside. + +```csharp +public class BinaryArchive2Container : IConverter +{ + public NodeContainerFormat Convert(IBinary source) + { + // Format: number of files + table with "name + offset + size", then file data. + var reader = new DataReader(source.Stream); + var container = new NodeContainerFormat(); + + int numFiles = reader.ReadInt32(); + for (int i = 0; i < numFiles; i++) + { + string name = reader.ReadString(bytesCount: 0x10, encoding: Encoding.UTF8); + uint offset = reader.ReadUInt32(); + uint size = reader.ReadUInt32(); + + // Create a sub-stream for the child, a stream from a region + // of the parent stream without making any read/write or copies. + Node child = NodeFactory.FromSubstream(name, source.Stream, offset, size); + container.Root.Add(child); + } + + return container; + } +} +``` + +And voilร . To use our new converter we just need to create a new instance and +pass some binary data. + +```csharp +// Convert the binary file into a virtual folder (no disk writing). +var fileStream = DataStreamFactory.FromFile("myArchive.bin", FileOpenMode.Read); +using var binaryFormat = new BinaryFormat(); + +var binary2Container = new BinaryArchive2Container(); +using var container = binary2Container.Convert(binaryFormat); + +// Now we can inspect or extract the content of the container +Node child = container.Children["text.json"] +child.Stream.WriteTo(child.Name); +``` + +## Parameters + +Frequently your converter may require additional parameters than just the input +object to do the conversion. For instance in a compressor you may need to ask +your users to provide the level of compression to do. Or you may need to know +the line ending for a text format. You may need to know if the target CPU is big +or little endian or the text encoding. + +In any of these cases, you can ask the user to provide this required or optional +information in the constructor of the converter class. + +> [!TIP] +> If your converter can run with some _default_ parameters, provide a +> parameter-less constructor to simplify its usage for common use cases. + +```csharp +public class RgbImage2IndexedImage : IConverter +{ + private readonly IColorQuantization quantization; + + // Parameter-less constructors for a default value that can be used in most cases. + public RgbImage2IndexedImage() + { + quantization = new ColorQuantization(); + } + + // Allow the user to customize the converter to their needs. + public RgbImage2IndexedImage(IColorQuantization customQuantization) + { + quantization = customQuantization; + } + + public IndexedImage Converter(RgbImage source) + { + // Use the quantization instance to convert RGB colors into an indexed image + // ... + } +} +``` + +## `IConverter` interface + +> [!IMPORTANT] +> Normally the [`IConverter`](xref:Yarhl.FileFormat.IConverter) (no generics +> version) is for internal use only. Unless writing a new framework or generic +> tools, use always +> [`IConverter`](xref:Yarhl.FileFormat.IConverter`2). + +You may notice that there is also an +[`IConverter`](xref:Yarhl.FileFormat.IConverter) interface that takes no +generics. The [`IConverter`](xref:Yarhl.FileFormat.IConverter`2) +_implements_ this base interface. + +This is an empty interface used only internally to enforce some basic +type-safety when due to technical reason we can't know the types of the +converter, so we can't use +[`IConverter`](xref:Yarhl.FileFormat.IConverter`2). + +For instance `Node.TransformWith(IConverter converter)` uses the base interface +to provide a simple API. Requiring the fully typed interface would make users to +specify to repeat the types: +`node.TransformWith(myConverter)` as the compiler +cannot guess these types at compile-type. By having the simple interface we can +just use `node.TransformWith(myConverter)`. + +Note that when the API uses [`IConverter`](xref:Yarhl.FileFormat.IConverter) it +will run reflection run-time checks to ensure the argument is valid. It will +check that the variable or type implements +[`IConverter`](xref:Yarhl.FileFormat.IConverter`2) and that the +input object is valid for this type. Although it may hit some nanoseconds of +performance, it provides better error messages. diff --git a/docs/articles/core/formats/formats.md b/docs/articles/core/formats/formats.md new file mode 100644 index 00000000..efd17180 --- /dev/null +++ b/docs/articles/core/formats/formats.md @@ -0,0 +1,91 @@ +# Formats + +In Yarhl, _formats_ are _.NET classes_ that represents a model that can be +converted and/or assigned into virtual files (_nodes_). + +## Implement a new format + +A format is just a regular programming model, a _.NET class_ (or _record_), +usually with properties and methods. The only requirement to have a _Yarhl +format-compatible_ is to implement the empty interface +[`IFormat`](xref:Yarhl.FileFormat.IFormat). + +[!code-csharp[format implementation](./../../../../src/Yarhl.Examples/Formats/Formats.cs?name=FormatImpl)] + +## Converting formats + +> [!NOTE] +> Check-out the [converters](./converters.md) topic to learn more about them. + +The _converters_ classes are responsible to convert one format into a new one. +To use it, create a new instance and call its +[`Convert(source)`]() method. + +[!code-csharp[serialize PO](./../../../../src/Yarhl.Examples/Formats/Converters.cs?name=SerializePo)] + +### Fluent API + +An easier way, it's to use the extension method on formats +`ConvertWith(converter)`. As it returns the new format, it allows to _chain +conversions_: + +```csharp +FullImage fontImage = binaryFont + .ConvertWith(new Binary2Font(FontKind.Debug)) // binary -> font model + .ConvertWith(new Font2Image()); // font -> image +``` + +### Converting without knowing converter at compile-time + +Sometimes the application doesn't know the converter type at compile-time. This +could be the case of generic tools that loads assemblies in a plugin-style and +select the converter type via configuration file or user interface. + +The static class [`ConvertFormat`](xref:Yarhl.FileFormat.ConvertFormat) provides +the APIs to convert formats by passing its type object. The API uses reflection +to validate the converter type and its arguments so that it throws an exception +when: + +- the type does not implement `IConverter` +- the converter cannot convert the type of the input. +- the parameters does not match any constructor signature. + +```csharp +object inputFormat; + +// UI / config file loaded from same or external assembly +Type converterType; +object[] converterArgs; + +object outputFormat = ConvertFormat.With(converterType, inputFormat, converterArgs); +``` + +> [!IMPORTANT] +> Note that as it uses reflection it's not as performant as other APIs. It also +> lose the ability to have type-safe code. If one of the converter change its +> interfaces or parameters between versions it may throw unexpected exceptions. + +## `IFormat` interface + +The converter interface does not have any requirements for the types it could +convert. You can theoretically implement `IConverter`. However, in +order to provide some features the library expects that every format implements +the _empty_ interface [`IFormat`](xref:Yarhl.FileFormat.IFormat). + +By using the [`IFormat`](xref:Yarhl.FileFormat.IFormat) interface it allows the +APIs to: + +- Provide extension methods that applies to formats only (like `ConvertWith`). +- Provide type discovery for _formats_ via _Yarhl.Plugins_. +- Prevent unboxing performance issues. + +## Working with existing models + +Models should implement the [`IFormat`](xref:Yarhl.FileFormat.IFormat) +interface. If you have a model and cannot be modified to inherit from the +interface, then it's possible to create a _format wrapper_. + +For instance, let's see how to provide a format-compatible class for a +third-party sound format `ThirdPartyWave`: + +[!code-csharp[format wrapper](./../../../../src/Yarhl.Examples/Formats/Formats.cs?name=FormatWrapper)] diff --git a/docs/articles/core/getting-started/architecture.md b/docs/articles/core/getting-started/architecture.md new file mode 100644 index 00000000..b2a4c305 --- /dev/null +++ b/docs/articles/core/getting-started/architecture.md @@ -0,0 +1,19 @@ +# Framework architecture + +TODO: base goal and requirements + +## Reading, deserializing, exporting + +TODO + +## Core library and plugins + +TODO + +## Related tools + +TODO + +## End game + +TODO diff --git a/docs/articles/core/getting-started/images/deserializer_output.png b/docs/articles/core/getting-started/images/deserializer_output.png new file mode 100644 index 00000000..d293ec2f Binary files /dev/null and b/docs/articles/core/getting-started/images/deserializer_output.png differ diff --git a/docs/articles/core/getting-started/images/goal_overview.drawio.png b/docs/articles/core/getting-started/images/goal_overview.drawio.png new file mode 100644 index 00000000..3b94320d Binary files /dev/null and b/docs/articles/core/getting-started/images/goal_overview.drawio.png differ diff --git a/docs/articles/core/getting-started/images/hex_view.png b/docs/articles/core/getting-started/images/hex_view.png new file mode 100644 index 00000000..39c52ab1 Binary files /dev/null and b/docs/articles/core/getting-started/images/hex_view.png differ diff --git a/docs/articles/core/getting-started/images/po_poedit.png b/docs/articles/core/getting-started/images/po_poedit.png new file mode 100644 index 00000000..30486751 Binary files /dev/null and b/docs/articles/core/getting-started/images/po_poedit.png differ diff --git a/docs/articles/core/getting-started/images/po_vscode.png b/docs/articles/core/getting-started/images/po_vscode.png new file mode 100644 index 00000000..c98fcc5f Binary files /dev/null and b/docs/articles/core/getting-started/images/po_vscode.png differ diff --git a/docs/articles/core/getting-started/net-languages.md b/docs/articles/core/getting-started/net-languages.md new file mode 100644 index 00000000..03d7e80c --- /dev/null +++ b/docs/articles/core/getting-started/net-languages.md @@ -0,0 +1,3 @@ +# Yarhl in other .NET languages + +TODO diff --git a/docs/articles/core/getting-started/resources/texts.bin b/docs/articles/core/getting-started/resources/texts.bin new file mode 100644 index 00000000..353ee15b Binary files /dev/null and b/docs/articles/core/getting-started/resources/texts.bin differ diff --git a/docs/articles/core/getting-started/tutorial.md b/docs/articles/core/getting-started/tutorial.md new file mode 100644 index 00000000..204ea85b --- /dev/null +++ b/docs/articles/core/getting-started/tutorial.md @@ -0,0 +1,285 @@ +# Getting started guide + +**_Welcome to Yarhl!_** ๐ŸŽ‰ +This step-by-step guide covers how to create your first program that works with +custom file formats. + +You will learn how to: + +- ๐Ÿ”ง setup a C# project with the _Yarhl_ dependencies. +- ๐Ÿ“ƒ analyze a specification and **implement a file format**. +- ๐Ÿงฎ **read binary data** and convert it into a format. +- โ™ป๏ธ **convert a text format** into a standard like GNU Gettext PO. + +By the end of this guide you will have a program that converts a binary file +into an editable text file! + +![Diagram showing hexadecimal viewer with some bytes and an arrow pointing to a PO text file](images/goal_overview.drawio.png) + +Prepare your self a good cup of ๐Ÿต or โ˜• and let's dive into it! ๐Ÿคฟ +And if you have any question along the way don't hesitate to +[ask for help](https://github.com/SceneGate/Yarhl/discussions). + +> [!NOTE] +> This guide covers how to create the project in the **.NET language C#**, but +> it should be fairly similar in any other .NET languages like F# or Visual +> Basic .NET. +> Unfortunately Yarhl is not supported outside .NET languages. + +> [!TIP] +> If you aer new into .NET development, start by learning with the free course +> of C# from [Microsoft](https://aka.ms/selfguidedcsharp). + +## Pre-requisites + +Before starting you need to setup your computer to create .NET projects. You +will need: + +- Latest version of the [.NET SDK](https://dotnet.microsoft.com/en-us/download) +- A programming IDE for C# development. + - On Windows you can use + [Visual Studio Community](https://visualstudio.microsoft.com/vs/community/) + - [Visual Studio Code](https://code.visualstudio.com/) with the C# extension + is also great and works in every major platform. + - [JetBrains Rider](https://www.jetbrains.com/rider/) is a cool alternative to + Visual Studio and works on every platform, but it's a paid product. +- (Optional) Hexadecimal viewer. Some options are: + - [ImHex](https://github.com/WerWolv/ImHex): cross-platform. + - [HxD](https://mh-nexus.de/en/downloads.php): Windows only. + - [Okteta](https://apps.kde.org/en-gb/okteta/): Linux with KDE. + +Also download the following binary file that our program will take as input: +[texts.bin](./resources/texts.bin). + +## Project setup + +Let's start by creating a new C# console project. It depends with the type of +IDE you are using. + +### [Visual Studio](#tab/vs) + +[Microsoft docs](https://learn.microsoft.com/en-us/visualstudio/get-started/csharp/tutorial-console) +describe how to setup a new project with the latest version of their IDE. + +### [Visual Studio Code](#tab/vscode) + +[Visual Studio Code docs](https://code.visualstudio.com/docs/csharp/get-started#_create-a-hello-world-app) +describe how to setup and create a new C# project within the IDE. + +### [Terminal](#tab/terminal) + +In general, one of the easiest way to create a C# projects is to use the +terminal. Open a new window and move to the folder where you want to create the +project. Then type: `dotnet new console --name MyFirstConverterTool` + +--- + +Now we have an empty project let's add the Yarhl dependency. Open your project +file (file with extension `.csproj`) and add a new _package reference_. It will +download the dependencies from +[nuget.org](https://www.nuget.org/packages?q=Yarhl). + +```xml + + + + +``` + +Nice, we have an application ready to add some code. + +> [!TIP] +> Check-out [nuget.org](https://www.nuget.org/packages?q=Yarhl) for the latest +> version available of Yarhl. + +## Format specification + +Our goal is to make a program that converts the downloaded file `texts.bin` into +a new file in a format that we can easily open, like a text file `.txt`. We can +inspect the content in its pure format, bytes, by using programs such as +_hexadecimal viewers_. + +![File opened with an hex viewer where there is some text along _random_ bytes](./images/hex_view.png) + +We can see there isn't only text but bytes with other meanings. This file +matches the following specification: + +| Offset | Type | Description | +| ------ | ----------- | ------------------------ | +| 0x0000 | `char[4]` | Format identifier `TXTI` | +| 0x0004 | Int32 | Number of texts | +| 0x0008 | TextEntry[] | List of text entries | + +where `TextEntry` is: + +| Offset | Type | Description | +| ------ | ------ | ------------------------------------ | +| 0x0000 | Int16 | Entry ID | +| 0x0002 | String | Text null-ended with UTF-16 encoding | + +## Format implementation + +From the above specification we are interested in keeping a **list of _entries_ +with their identifier number and text content**. Next step is to create a new +class to represent this format. + +Let's create a new class named `TxtiFormat`. It will have a property with the +collection of entries (initialized for convenience). + +[!code-csharp[TxtiFormat](../../../../src/Yarhl.Examples/Tutorial/TxtiFormat.cs?name=Class&highlight=4)] + +Did you notice the **inheritance with `IFormat`**? +Format models should implement the interface `IFormat`. In this way Yarhl knows +that this type implements a format. The interface is empty, it doesn't need to +implement any specific method or property, it acts as a marker. + +Now let's create a second class to represent the `TextEntry`: + +[!code-csharp[TextEntry](../../../../src/Yarhl.Examples/Tutorial/TextEntry.cs?name=Class&highlight=1)] + +This time we don't need to inherit from `IFormat` as this class does not +represent a file format itself. It's part of one. + +๐Ÿ˜Ž Cool, we have our first **file format implemented!** It's time to add some +code to fill these classes from the file `texts.bin`. + +## Converting binary data into the format + +First we will do what it's named **deserialization**: reading binary data from a +file to fill a model. + +To do so we will create a **converter** class. It's goal is to _convert a binary +format, the data from the file, into the `TxtiFormat` model_. + +```mermaid +graph LR + A["Binary"] --> |Binary2Txti| B["Txti"] +``` + +Create a new class `Binary2Txti` and implement the interface +`IConverter`. + +[!code-csharp[Binary2Txti class definition](../../../../src/Yarhl.Examples/Tutorial/Binary2Txti.cs?name=Class&highlight=5)] + +The interface asks to have a method that performs the conversion: +`TxtiFormat Convert(IBinary source)`. It takes as an input any binary format and +produces as an output the model `TxtiFormat`. Add the method to our class. + +It's time to start adding code to read data from the file. First we need a +_binary reader_ class that will help us to read integers and strings from binary +content. Yarhl provides an enhanced version of .NET `BinaryReader` with the +class `DataReader`. + +[!code-csharp[Binary2Txti reading first 4 chars](../../../../src/Yarhl.Examples/Tutorial/Binary2Txti.cs?name=ValidateHeader&highlight=1,5)] + +As a start, we read the first 4 characters of the file that according to our +[specification](#format-specification) they must be `TXTI`. This guarantees we +are reading the format we expect. + +Now it's time to get the number of entries and iterate for each of them. As we +know, each entry has an ID followed by the text. We will add them to a new +instance of our target format `TxtiFormat`. Finally we return it from the +`Convert` method. + +[!code-csharp[Binary2Txti reading text entries](../../../../src/Yarhl.Examples/Tutorial/Binary2Txti.cs?name=ReadEntries)] + +And voilร  ๐Ÿš€. You have your first **format converter**. + +## Use the converter + +Let's try our cool format and converter. Going back to `Program.cs` let's add +some code to the `Main` method, our application entrypoint, so it opens the file +and convert it. + +First we need to open the file on disk and create our source format: a binary +format. We will take the file path from the program command-line arguments. + +[!code-csharp[Creating a stream from a file](../../../../src/Yarhl.Examples/Tutorial/Program.cs?name=OpenFile)] + +Next, let's create an instance of our converter and convert/read that file! + +[!code-csharp[Converting binary data](../../../../src/Yarhl.Examples/Tutorial/Program.cs?name=Deserialize)] + +If we run our program now we should see the following output: + +![deserializer output showing 3 entries and text 'Hello World!'](images/deserializer_output.png) + +## Converting Txti into a standard PO format + +So far we have been able to read the binary file into a model. If our intention +is to be able to modify it or use it outside the program, we will need to +convert it into another format. Hopefully this time more standard that other +programs can open. + +To deal with _translatable content_ one of the industry standards is +[GNU Gettext PO](https://en.wikipedia.org/wiki/Gettext) + +Our goal now is to do something we may call **exporting**: convert `TxtiFormat` +into a standard format like `Po`, so we can write it on a file later. + +```mermaid +graph LR + A["Txti"] --> |Txti2Po| B + B["Po"] -->|Po2Binary| C["Binary"] +``` + +Repeating the process from before, we create a class `Txti2Po` to implement this +time `IConverter`. Let's add some basic implementation: + +[!code-csharp[Converter Txti to PO](../../../../src/Yarhl.Examples/Tutorial/Txti2Po.cs?name=Converter)] + +Our converter returns the data into another format model: `Po`. We will need one +additional step before we can save the data to disk: converting the `Po` model +to binary (serializing). To do this task, `Yarhl.Media.Text` provides the +converter `Po2Binary` that implements `IConverter`. + +## Use a Node to chain converters + +There is a second way to use the _converters_ that could be easier to read when +we need to **chain conversions**. + +We can use `Node`s. They represent a _file_ on a virtual, non-existing, file +system that it's in our program. A `Node` has a format. If you create one from a +file, it will have `BinaryFormat` (`IBinary`) to start with. We can then +_convert_ the binary format of our node using the API `TransformWith`: + +[!code-csharp[Chaining conversions](../../../../src/Yarhl.Examples/Tutorial/Program.cs?name=ExportNodes)] + +Congrats! You just finished the program. ๐Ÿ‘๐Ÿ‘ +Feel free to open your output file with any text editor or PO-specific software +like [PO Edit](https://poedit.net/) or [Weblate](https://weblate.org). + +![PO opened in VSCode](images/po_vscode.png) + +![PO opened in PoEdit](images/po_poedit.png) + +## Wrap up + +In this guide we saw how to implement a file format and two converters. We were +able to read a binary file and generate a standard format that allows us to +inspect the content and edit it. + +```mermaid +graph LR + A["Binary TXTI"] --> |Binary2Txti| B + B["Txti"] --> |Txti2Po| C + C["Po"] -->|Po2Binary| D["Binary PO"] +``` + +Check-out the docs to learn more details about Yarhl and its features + +- [Format](../formats/formats.md) +- [Converters](../formats/converters.md) +- [`DataStream`](../binary/datastream.md) +- [Nodes](../virtual-file-system/nodes.md) + +> [!NOTE] +> We could have taken a shortcut and create a converter to do _binary TXTI_ to +> _PO_. Creating the intermediate conversion into the model `TxtiFormat` makes +> our application more extensible. In the future we may need to export into a +> _XLIFF_ file instead of _PO_. + +> [!TIP] +> Do you want to keep playing? +> Feel free to try to implement now the reverse operation: from a (modified) PO +> file generate the binary TXTI file. diff --git a/docs/articles/core/toc.yml b/docs/articles/core/toc.yml new file mode 100644 index 00000000..d9910d28 --- /dev/null +++ b/docs/articles/core/toc.yml @@ -0,0 +1,49 @@ +- name: โœจ Getting started +- name: Introduction + href: ../../index.md +- name: Getting started guide + href: ./getting-started/tutorial.md +- name: ๐Ÿšง Framework architecture + href: ./getting-started/architecture.md +- name: ๐Ÿšง Yarhl in other .NET languages + href: ./getting-started/net-languages.md + +- name: โ™ป Formats and converters +- name: Formats + href: ./formats/formats.md +- name: Converters + href: ./formats/converters.md +- name: Advanced + items: + - name: Cloneable format + href: ./formats/cloneable-format.md + - name: Use cases for converters + href: ./formats/converters-usecases.md + +- name: ๐Ÿ“ Virtual file system +- name: Nodes + href: ./virtual-file-system/nodes.md +- name: Node factory + href: ./virtual-file-system/node-factory.md +- name: ๐Ÿšง Node navigation + href: ./virtual-file-system/navigate-nodes.md +- name: ๐Ÿšง Container format + href: ./virtual-file-system/container-format.md + +- name: ๐Ÿงฎ Binary formats +- name: DataStream + href: ./binary/datastream.md +- name: ๐Ÿšง Binary format + href: ./binary/binaryformat.md +- name: ๐Ÿšง Binary reader and writer + href: ./binary/binaryreader-writer.md +- name: ๐Ÿšง Text reader and writer + href: ./binary/textreader-writer.md +- name: ๐Ÿšง (De)serialization via attributes + href: ./binary/attr-serialization.md +- name: ๐Ÿšง Custom streams + href: ./binary/custom-streams.md + +- name: ๐Ÿ”Œ Plugins +- name: ๐Ÿšง Overview + href: ../plugins/overview.md diff --git a/docs/articles/core/virtual-file-system/container-format.md b/docs/articles/core/virtual-file-system/container-format.md new file mode 100644 index 00000000..3e9d0a43 --- /dev/null +++ b/docs/articles/core/virtual-file-system/container-format.md @@ -0,0 +1,3 @@ +# Container format + +TODO diff --git a/docs/articles/core/virtual-file-system/images/node-children.drawio.png b/docs/articles/core/virtual-file-system/images/node-children.drawio.png new file mode 100644 index 00000000..180ff1b2 Binary files /dev/null and b/docs/articles/core/virtual-file-system/images/node-children.drawio.png differ diff --git a/docs/articles/core/virtual-file-system/images/node-simple.drawio.png b/docs/articles/core/virtual-file-system/images/node-simple.drawio.png new file mode 100644 index 00000000..2354d4e0 Binary files /dev/null and b/docs/articles/core/virtual-file-system/images/node-simple.drawio.png differ diff --git a/docs/articles/core/virtual-file-system/navigate-nodes.md b/docs/articles/core/virtual-file-system/navigate-nodes.md new file mode 100644 index 00000000..4a49dc18 --- /dev/null +++ b/docs/articles/core/virtual-file-system/navigate-nodes.md @@ -0,0 +1,3 @@ +# Navigate the nodes + +TODO diff --git a/docs/articles/core/virtual-file-system/node-factory.md b/docs/articles/core/virtual-file-system/node-factory.md new file mode 100644 index 00000000..7a267c3c --- /dev/null +++ b/docs/articles/core/virtual-file-system/node-factory.md @@ -0,0 +1,129 @@ +# Node factory + +The [`Node`](xref:Yarhl.FileSystem.Node) constructor requires at least a +**non-null name**. It's not required to provide an _extension_ in the name. Its +format can be initially `null`, you can [change it later](nodes.md#format). + +[!code-csharp[node constructors](./../../../../src/Yarhl.Examples/FileSystem/NodeFactoryExamples.cs?name=Constructor)] + +> [!NOTE] +> There is another overload of the `Node` constructor with a `Node` parameter. +> It's covered in the [node clone](nodes.md#cloning-a-node) topic. + +To cover common use cases, the +[`NodeFactory`](xref:Yarhl.FileSystem.NodeFactory) provides APIs to create nodes +with a format quickly. + +## Create node with binary data + +Similar to the [`DataStreamFactory`](xref:Yarhl.IO.DataStreamFactory), we can +create a node with binary data from different sources. In all these cases the +node will have the format [`BinaryFormat`](xref:Yarhl.IO.BinaryFormat). + +- New memory buffer: + [`FromMemory(name)`]() +- Byte array: + [`FromArray(name, data)`]() +- Segment of a byte array: + [`FromArray(name, data, offset, length)`]() +- .NET `Stream`: + [`FromStream(name, stream)`]() +- Segment of a .NET `Stream`: + [`FromSubstream(name, stream, offset, lengt)`]() + +[!code-csharp[from binary](./../../../../src/Yarhl.Examples/FileSystem/NodeFactoryExamples.cs?name=BinaryData)] + +> [!IMPORTANT] +> The APIs `FromStream` and `FromSubstream` will take ownership of the stream +> argument. Do not dispose the stream variable directly. If you don't want that +> the format takes the ownership, create a +> [BinaryFormat](xref:Yarhl.IO.BinaryFormat) with +> [DataStreamFactory.FromStreamKeepingOwnership](). + +## Create nodes from files + +The `FromFile` overloads allows to create a new node to access the data from a +file on disk. The node will have the format +[`BinaryFormat`](xref:Yarhl.IO.BinaryFormat). + +The +[`FromFile(path, mode)`]() +creates the node with the name from file. This includes the file extension as +well. + +The overload +[`FromFile(path, name, mode)`]() +allows to set a specific name for the node that differs from the name of the +file on disk. + +If the path points to a symbolic link from _Windows_ or _Unix_, it will resolve +to the target. + +[!code-csharp[from files](./../../../../src/Yarhl.Examples/FileSystem/NodeFactoryExamples.cs?name=Files)] + +> [!TIP] +> The new node will have a [tag](nodes.md#tags) named `FileInfo` containing an +> instance of .NET +> [`FileInfo`](https://learn.microsoft.com/en-us/dotnet/api/system.io.fileinfo) +> for the given path. If the path was pointing to a symbolic link, the file info +> will contain information of the link, not the actual target. + +> [!TIP] +> Check-out the [DataStream](../binary/datastream.md#factory) topic for +> information about the `FileOpenMode` arguments. + +## Create a container node + +The +[`CreateContainer(name)`]() +method allows to create a node with the given node for _container_ usage. It +will have the format `NodeContainerFormat`. + +[!code-csharp[container](./../../../../src/Yarhl.Examples/FileSystem/NodeFactoryExamples.cs?name=Container)] + +## Create nodes from directories + +The factory contains a set of APIs to create a node hierarchy that replicates +the files and folders from a given path on disk. + +The first method is +[`FromDirectory(path, filter, mode)`](). +By default the `filter` is `*`. It creates a new node with children **for each +file** on the given path. **It does not iterate recursively and it will ignore +folders on the path**. It opens the files with the provided `mode`. Optionally +it's possible to pass a `string` with a simple _filter_ or search pattern. More +information about the filter from the .NET API +[`Directory.GetFiles`](). +The node will have the name of the root directory from the given path. + +[!code-csharp[directory](./../../../../src/Yarhl.Examples/FileSystem/NodeFactoryExamples.cs?name=Directory1)] + +The method +[`FromDirectory(path, filter, name, iterateDirectories, mode)`]() +behaves similar but allows to specify the name of the node. It also has a new +argument to specify if it should **iterate recursively through any directory** +and create the full hierarchy. + +[!code-csharp[directory](./../../../../src/Yarhl.Examples/FileSystem/NodeFactoryExamples.cs?name=Directory2)] + +To provide advanced filtering capabilities, there are two more methods that +behaves similar to above. In these cases instead of a `string` to filter, you +can specify a function (lambda or method) that takes as an argument a file path +and returns a boolean to accept or not the file. + +[!code-csharp[directory](./../../../../src/Yarhl.Examples/FileSystem/NodeFactoryExamples.cs?name=Directory3)] + +## Create node hierarchy + +Given a _root_ node and a child with an associated path, you may want to +_insert_ it in the hierarchy. The API +[`CreateContainersForChild(root, path, child)`]() +adds the child in the given path from the _root_ node, creating any necessary +intermediary container node. + +As an example, we have a scenario were we just created our _root_ node and we +have a child that we want to add in `data/gfx/scene1/`. The method will create +the node containers `data`, `gfx` and `scene1`. It will add them to the _root_ +node and then add our child to `scene1`. + +[!code-csharp[hierarchy](./../../../../src/Yarhl.Examples/FileSystem/NodeFactoryExamples.cs?name=CreateHierarchy)] diff --git a/docs/articles/core/virtual-file-system/nodes.md b/docs/articles/core/virtual-file-system/nodes.md new file mode 100644 index 00000000..39dd614f --- /dev/null +++ b/docs/articles/core/virtual-file-system/nodes.md @@ -0,0 +1,172 @@ +# Node + +In Yarhl, a **node** is an entity that has a **name** and a +[**format**](../formats/formats.md). The concept is similar to a file or folder +on a computer hard-drive, but in this case is virtual, it's not a physical file +in the disk. Its format can be some bytes in a `Stream` like a disk file, but it +could also be any class that implements the +[`IFormat`](xref:Yarhl.FileFormat.IFormat) interface. + +![node with name, format and path properties](images/node-simple.drawio.png) + +If the _format_ of the node is `NodeContainerFormat`, then we say that the node +is a **container**. This is the equivalent of a folder in our file system. These +nodes may have one or more node **children**. + +By having nodes with children we can create a hierarchy similar to the file +system of our drives. We could navigate identify and navigate by their **path**. + +![previous node with three children nodes each with different set of properties](images/node-children.drawio.png) + +Nodes is a core feature of Yarhl. It allows to represent with a hierarchy a set +of formats. Running [converters](../formats/converters.md) we can transform the +content of the nodes, for instance by reading, writing, unpacking, etc. + +Combining nodes, [converters](../formats/converters.md) and the +[_sub-stream_ concept of `DataStream`](../binary/datastream.md#sub-streams), we +can represent a complex file system even when all the file data points to a +single `Stream` from a disk file. + +Let's see it with an example from the +[Ekona](https://scenegate.github.io/Ekona/index.html) library that provides +implementation for _Nintendo DS_ formats. + +[!code-csharp[overview](./../../../../src/Yarhl.Examples/FileSystem/NodeExamples.cs?name=Overview)] + +## Children + +A node may have children if it has a container type. Children are stored as +references in a collection and can be accessed via the property `Children`. + +It's possible to iterate `Children` with a `foreach` or a regular `for` and get +the number of children with `node.Children.Count`. + +To access to a child use its index, `Children[3]`, or its name, +`Children["image.png"]`. You can chain this operation to navigate the hierarchy: +`node.Children[1].Children["map1.scr"]`. + +[!code-csharp[children](./../../../../src/Yarhl.Examples/FileSystem/NodeExamples.cs?name=AccessChildren)] + +> [!TIP] +> You can find more ways to iterate or navigate nodes across a hierarchy in the +> [`Navigator`](xref:Yarhl.FileSystem.Navigator) class. + +### Add or remove + +It's possible to add or remove children from its parent node. Use the method +`Add` to add one or more nodes as its children. + +Use the `Remove` method to remove a child from its parent by instance reference +or node name. The method will **not throw an exception** if the node to remove +is not found. The method will return `false` in those cases. + +The method `RemoveChildren` removes all the children. Additionally its parameter +allow to dispose them. + +[!code-csharp[add remove](./../../../../src/Yarhl.Examples/FileSystem/NodeExamples.cs?name=AddRemove)] + +> [!NOTE] +> A removed child **is not disposed**. Consider freeing its formats, especially +> if they have binary type or are containers. `RemoveChildren` does offer the +> possibility to dispose the children. + +> [!IMPORTANT] +> A node cannot have two children with the same name. Adding a node with the +> same name will replace the node. + +> [!IMPORTANT] +> You should not add or remove while iterating the `Children` property. + +## Format + +A node can have any [format](../formats/formats.md) type that implements the +interface `IFormat`. It can also have a `null` format. + +The `Format` property gets access to the format by returning an instance of type +`IFormat`. For convenience, there is also the method `GetFormatAs()` that +tries to cast the node format to the desired type. It will **return `null`** if +the casting is not possible. + +The property `IsContainer` returns `true` when the node have a type that allows +having children. This would be `NodeContainerFormat` or `null`. + +The property `Stream` is a shortcut to `GetFormatAs().Stream` and it +will also return `null` if the format is not an implementation of `IBinary`. + +### Changing format + +The node can change its format via the method `ChangeFormat(format)`. + +If the current format is a container, first it will remove any children from +this node. If the future format is a container, it will move the children from +the format to the node. + +Additionally there is an optional argument to indicate if the method should +dispose the current format before changing. By default is `true`, meaning it +will call the method `Dispose` from the current format if it implements +`IDisposable`. + +## Format conversion + +Apart from the changing the format API, a _node_ also provides methods to +_transform_ the format by using a [converter](../formats/converters.md). + +If the converter does not need any +[parameter](../formats/converters.md#parameters) and it has a public +parameterless constructor (default case) you can use the short API +[`TransformWith()`](xref:Yarhl.FileSystem.Node.TransformWith``1) + +If it takes parameters or the instance needs to be created in a different way +(e.g. factory), pass the converter object via +[`TransformWith(converter)`](). + +[!code-csharp[transform](./../../../../src/Yarhl.Examples/FileSystem/NodeExamples.cs?name=Transform)] + +The `TransformWith()` method are a shortcut method to run a converter with the +current node's format and then call `ChangeFormat()` with the result. Note the +considerations of [changing format](#changing-format) like what it would happen +for container formats. + +The method returns the same instance of the node. This allows a syntax +fluent-like for chaining conversions. + +[!code-csharp[transform chaining](./../../../../src/Yarhl.Examples/FileSystem/NodeExamples.cs?name=TransformChain)] + +## Tags + +Nodes can store additional metadata via the generic dictionary `Tags`. Each tag +has a `string` as a key and it can have any type as value. Use it to store +metadata of the node, outside of its regular format. + +Converters may use the `Tags` to provide additional information about the nodes. +For instance, the _Ekona_ library adds to every node the tag +`scenegate.ekona.id` with the internal ID of the file in the game file. + +> [!NOTE] +> **Avoid depending on _tags_ in a converter.** +> It could be that the node has a given tag just after running a converter. But +> if it was created from a file on disk it may not have it again. + +## Cloning a node + +The constructor +[`Node(node)`]() +allows to do a _deep_ clone of a node. This includes name, format and tags. + +If the format of the source node is not null, **it must implement +[`ICloneableFormat`](../formats/cloneable-format.md)**. As children are also +_deep_ cloned, all of them must have a format that implements +`ICloneableFormat`. + +As `NodeContainerFormat` implements `ICloneableFormat`, the children of the node +will also be deep cloned. + +## Dispose + +`Node` implements `IDisposable`. The `Dispose` method will remove and dispose +all of its children, recursively. It will also dispose its format if it +implements `IDisposable`. + +> [!IMPORTANT] +> A node cannot be used anymore after calling `Dispose`. This also affects to +> all its children (recursively). diff --git a/docs/articles/media-text/encodings.md b/docs/articles/media-text/encodings.md new file mode 100644 index 00000000..7886b4b6 --- /dev/null +++ b/docs/articles/media-text/encodings.md @@ -0,0 +1,3 @@ +# Encodings + +TODO diff --git a/docs/articles/media-text/po-format.md b/docs/articles/media-text/po-format.md new file mode 100644 index 00000000..c79c6d55 --- /dev/null +++ b/docs/articles/media-text/po-format.md @@ -0,0 +1,3 @@ +# PO translation format + +TODO diff --git a/docs/articles/media-text/tables.md b/docs/articles/media-text/tables.md new file mode 100644 index 00000000..88971339 --- /dev/null +++ b/docs/articles/media-text/tables.md @@ -0,0 +1,3 @@ +# Tables + +TODO diff --git a/docs/articles/media-text/toc.yml b/docs/articles/media-text/toc.yml new file mode 100644 index 00000000..e386a32d --- /dev/null +++ b/docs/articles/media-text/toc.yml @@ -0,0 +1,7 @@ +- name: ๐Ÿ“ Text formats +- name: ๐Ÿšง GNU Gettext PO format + href: ./po-format.md +- name: ๐Ÿšง Encodings + href: ./encodings.md +- name: ๐Ÿšง Tables + href: ./tables.md diff --git a/docs/articles/plugins/overview.md b/docs/articles/plugins/overview.md new file mode 100644 index 00000000..6a1cfb1c --- /dev/null +++ b/docs/articles/plugins/overview.md @@ -0,0 +1,3 @@ +# Plugins overview + +TODO diff --git a/docs/dev/toc.yml b/docs/dev/toc.yml deleted file mode 100644 index caf18e7e..00000000 --- a/docs/dev/toc.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: Changelog - href: Changelog.md - -- name: API - href: ../api/toc.yml diff --git a/docs/docfx.json b/docs/docfx.json index 976fafdd..2776af6d 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -1,67 +1,67 @@ { - "metadata": [ - { - "src": [ - { - "files": [ - "Yarhl/bin/**/Yarhl.dll", - "Yarhl.Media.Text/bin/**/Yarhl.Media.Text.dll" - ], - "src": "../src" - } - ], - "dest": "api", - "filter": "filterConfig.yml", - "disableGitFeatures": false, - "disableDefaultFilter": false - } - ], - "build": { - "content": [ - { - "files": [ "api/**.yml", "dev/**" ] - }, - { - "files": [ "toc.yml", "index.md" ] - }, - { - "files": [ "guides/**" ] - }, - { - "files": ["README.md", "CONTRIBUTING.md"], - "src": "../" - } - ], - "resource": [ - { - "files": ["images/**"] - } - ], - "overwrite": [ + "metadata": [ + { + "src": [ { "files": [ - "apidoc/**.md" + "Yarhl/*.csproj", + "Yarhl.Media.Text/*.csproj" ], - "exclude": [ - "obj/**", - "_site/**" - ] + "src": "../src" } ], - "dest": "_site", - "globalMetadataFiles": ["global_metadata.json"], - "fileMetadataFiles": [], - "template": [ - "default", - "statictoc", - "default-widescreen" - ], - "postProcessors": ["ExtractSearchIndex"], - "markdownEngineName": "markdig", - "noLangKeyword": false, - "keepFileLink": false, - "cleanupCacheHistory": false, + "dest": "api", + "includePrivateMembers": false, "disableGitFeatures": false, - "xrefService": [ "https://xref.docs.microsoft.com/query?uid={uid}" ] + "disableDefaultFilter": false, + "noRestore": false, + "namespaceLayout": "nested", + "memberLayout": "samePage", + "EnumSortOrder": "alphabetic", + "allowCompilationErrors": false + } + ], + "build": { + "content": [ + { + "files": [ + "api/**.yml", + "api/index.md" // TODO: homepage with namespace overview + ] + }, + { "files": "**/*.{md,yml}", "src": "articles", "dest": "docs" }, + { "files": [ "toc.yml", "*.md" ] } + ], + "resource": [ + { + "files": [ "**/images/**", "**/resources/**" ], + "exclude": [ "_site/**", "obj/**" ] + } + ], + "output": "_site", + "globalMetadata": { + "_appTitle": "Yarhl", + "_appName": "Yarhl", + "_appFooter": "Part of the SceneGate framework. Docs made with docfx", + "_appLogoPath": "images/logo-50.png", + "_appFaviconPath": "images/favicon.ico", + "_enableSearch": true, + "_enableNewTab": true, + "_lang": "en" + }, + "fileMetadataFiles": [], + "template": [ + "default", + "modern", + "template" + ], + "postProcessors": [], + "keepFileLink": false, + "disableGitFeatures": false, + "sitemap": { + "baseUrl": "https://scenegate.github.io/Yarhl", + "priority": 0.5, + "changefreq": "monthly" } + } } diff --git a/docs/filterConfig.yml b/docs/filterConfig.yml deleted file mode 100644 index 4d53c6ad..00000000 --- a/docs/filterConfig.yml +++ /dev/null @@ -1,5 +0,0 @@ -apiRules: - # Remove object method inheritance - - exclude: - uidRegex: ^System\.Object -type: Type \ No newline at end of file diff --git a/docs/global_metadata.json b/docs/global_metadata.json deleted file mode 100644 index 70bf6b98..00000000 --- a/docs/global_metadata.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "_appTitle": "SceneGate Yarhl", - "_appFooter": "Copyright (c) 2018 SceneGate", - "_appLogoPath": "images/favicon-48.png", - "_appFaviconPath": "images/favicon.ico", - "_enableSearch": true, - "_enableNewTab": true, - "_gitContribute": { - "apiSpecFolder": "docs/apidoc", - "repo": "https://github.com/SceneGate/Yarhl", - "branch": "develop" - } -} \ No newline at end of file diff --git a/docs/guides/Contributing.md b/docs/guides/Contributing.md deleted file mode 100644 index 00bde098..00000000 --- a/docs/guides/Contributing.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -uid: CONTRIBUTING ---- - -[!include[CONTRIBUTING](../../CONTRIBUTING.md)] diff --git a/docs/guides/Cookbook.md b/docs/guides/Cookbook.md deleted file mode 100644 index 8a0f7505..00000000 --- a/docs/guides/Cookbook.md +++ /dev/null @@ -1,102 +0,0 @@ -# Cookbook - -## IO - -### Padding - -```csharp -// Add 0s until position is divisible by 0x4 -filesWriter.WritePadding(0x00, 0x4); - -// Add 0s until position is divisible by 0x10 -myDataWriter.WritePadding(0x00, 0x10); -``` - -## FileSystem - -### Using converters with parameters - -```csharp -public class ConverterWithParameter : - IInitializer, - IConverter, - IConverter - { - - public int Parameter { get; set; } - - public void Initialize(int param) - { - Parameter = param; - } - - public Po Convert(BinaryFormat source) - { - // Converter - } - - public BinaryFormat Convert(Po source) - { - // Converter - } - } -``` - -Then you can use `TransformWith(3)`. - -You can use a custom class too: - -```csharp -public class ConverterWithParameter : - IInitializer, - IConverter, - IConverter - -TransformWith(myClassInstance) -``` - -### Creating directory structure - -```csharp -Node root = new Node("root"); - -string path = "/parent1/parent2/"; -Node child = new Node("child"); - -// This will create /root/parent1/parent2/child -NodeFactory.CreateContainersForChild(root, path, child); -``` - -### Iterating children nodes - -```csharp -foreach (Node node in Navigator.IterateNodes(source.Root)) { - if (!node.IsContainer) { - //This is your child, take care of him - } -} -``` - -### Find a node - -```csharp -// You can use full paths (starting with '/') or relative paths. -Navigator.SearchNode(nodeParent, "Child/SubChild"); -``` - -###ย Deleting nodes - -```csharp -var parent = new Node("Parent"); -var child1 = new Node("Child1"); -var child2 = new Node("Child2"); -node.Add(child1); -node.Add(child2); - -// If you remove passing a reference, the node is removed but it is NOT disposed -node.Remove(child1); - -// If you remove passing a name (you don't have the reference), -// the node is removed AND disposed. -node.Remove("Child2"); -``` diff --git a/docs/guides/Yarhl-nutshell.md b/docs/guides/Yarhl-nutshell.md deleted file mode 100644 index 2c4b2131..00000000 --- a/docs/guides/Yarhl-nutshell.md +++ /dev/null @@ -1,586 +0,0 @@ -๏ปฟ# Yarhl in a nutshell - -![Yarhl Logo](../images/logo.png) - -**Yarhl** - _Yet Another ROM Hacking Library_ - is a library for _ROM Hacking_ -and fan-translation projects. It provides a virtual file system, file format, -and format conversion features and plugin support. - -But what it really has to offer? Why should you use it? And how? This tutorial -will teach you how to use Yarhl and how to take advantage of the 100% of it. - -Remember that if you have any question you can create GitHub issues, contact the -contributors by email, Twitter or Discord. But first make sure you've read the -whole docs. - -## Your first steps: Reading and Writing - -Oh, hi! I'm Mister Yarhl (or M.Y.). Nice to meet you. I will be your guide! -Erm... y-you can... picture me like this: - -![Mister Yarhl](../images/mister.png) - -Let's get started! The first module I'm teaching you is -[`Yarhl.IO`](xref:Yarhl.IO) (IO stands for _Input/Output_), which is similar to -.NET standard `System.IO` but with specific features to work with binary files. - -This module is divided into binary and text files. Easy-peasy! Let's go deeper -into these classes! - -### DataStream - -[`DataStream`](xref:Yarhl.IO.DataStream) wraps any kind of .NET `Stream`. - -#### Reuse of Stream - -It allows to reuse a parent `Stream` to have substreams to reduce the number of -resources to use. For instance, to unpack a file you would just need to create -`DataStream` instances from the same parent `DataStream` having different -offsets and lengths. - -Disposing the last instance of a `DataStream` that has a reference to a `Stream` -will dispose the `Stream` too. - -#### Comparison - -The `DataStream` class provides the -[`Compare`]() method to -check if two streams are identical. - -#### Push and pop positions - -Similar to the terminal commands `pushd` and `popd`, our `DataStream` provides -methods for moving temporarily to a position to perform an operation and then -restore the position. This is very useful when you need to read or write a few -fields in another section of the file. It works with an stack so you can push -several positions. - -- [`PushCurrentPosition`](xref:Yarhl.IO.DataStream.PushCurrentPosition): save - the current position. -- [`PushToPosition`](xref:Yarhl.IO.DataStream.PushToPosition*): save the current - position and move. -- [`PopPosition`](xref:Yarhl.IO.DataStream.PopPosition): restore the last saved - position. -- [`RunInPosition`](): - push, run the lambda expression and pop again. - -#### Read and Write - -We have also the typical read and write methods for arrays of bytes. And don't -forget about the [`WriteTo`]() -methods that allows to write a full `DataStream` in another `DataStream` or in a -file in your disk. Very useful mate! - -### DataReader and DataWriter - -[`DataReader`](xref:Yarhl.IO.DataReader) is the equivalent of the .NET -`BinaryReader` and [`DataWriter`](xref:Yarhl.IO.DataWriter) of `BinaryWriter. -Apart from the typical read and write methods, they provide the following very -useful features. - -#### Endianness - -By properties or constructor you can specify if the endianness of the stream is -little or big. This will affect to all the read and write operations. - -#### Strings - -By using the different overloads of `ReadString` and `Write` you can read and -write strings with different encodings, fixed sizes, null terminated or not or -in the format _size + content_ style. I recommend you to take a look into them, -they cover most of the cases you will need to work with files. - -#### Padding - -Are you tired of writing logic to skip or write padding bytes? Well, we too! If -you are reading a file and you want to skip padding bytes, you can call -[`SkipPadding`]() and if you -need to write padding bytes, then -[`WritePadding`]() -will be your friend. - -### TextDataReader and TextDataWriter - -So far, `DataReader` and `DataWriter` have been very useful when you are dealing -with a file that contains some integer fields for size or offset, arrays of -bytes and maybe null-terminated strings. But, what about if you need to work -with a file that only contains text and you are interested in reading line by -line? In that case, you need [`TextDataReader`](xref:Yarhl.IO.TextDataReader) -and [`TextDataWriter`](xref:Yarhl.IO.TextDataWriter). - -#### New lines - -By default, `TextDataWriter` uses always (Windows too) the new line `\n`. It -doesn't use `\r\n`. The reason is that most file formats uses `\n` and in some -games having the `\r` may crash. It's sometimes difficult to notice that. If you -want to use any other new line string (you can even use `
`), you just need -to change the [`NewLine`](xref:Yarhl.IO.TextDataWriter.NewLine) property. - -In the case of the `TextDataReader` the behavior is different. The default value -for the [`NewLine`](xref:Yarhl.IO.TextDataReader.NewLine) property depends on -the OS (Windows: `\r\n`, Unix: `\n`). In addition, we provided with an automatic -mechanism enabled by default: -[`AutoNewLine`](xref:Yarhl.IO.TextDataReader.AutoNewLine*). If it's enabled, you -don't need to know the line ending in advance because we will stop at `\n` and -remove the last `\r` if present. This is also useful if a file mix both line -endings. And remember, by setting the `NewLine` property `AutoNewLine` is -disabled. - -#### Encoding - -The encoding can only by specified in the constructor. We believe that it -doesn't have sense to change the encoding once you start using the reader -because a text file must not mix encodings. - -#### Peeking - -Do you need to read a line without actually moving the position of the stream? -Maybe you want to check if the line contains a token but you are not sure and -don't want to keep the current position all the time. Well, in that case you -have the `Peek*` methods. - -#### Preambles / BOM - -Some encodings may have a specific -[BOM](https://en.wikipedia.org/wiki/Byte_order_mark) (_Byte Order Mark_) (or -_preamble_ in the .NET world). These are some bytes at the beginning of the -stream that confirms the encoding of the file. For instance, when using UTF-16, -the file will begin with the bytes `0xFEFF`. It also specifies if the encoding -is little-ending or big-endian (needed for UTF-16). - -Our `TextDataReader` will skip the BOM (_if it's present_) at the beginning of -the file. In the case of the `TextDataWriter`, the behavior is defined by the -property [`AutoPreamble`](xref:Yarhl.IO.TextDataWriter.AutoPreamble) which is -set to `false` by default (again, some games may see it as unexpected bytes). -When enabled, the first write call will also write the BOM. You can also write -it manually by calling -[`WritePreamble()`](xref:Yarhl.IO.TextDataWriter.WritePreamble) (but remember, -only if you are at the beginning of the stream). - -I know... I talk too much... Let's continue! - -### Examples - -#### Reading / writing a binary file - -```csharp -public void LoadFile(string path) -{ - using (var stream = DataStreamFactory.FromFile(path, FileOpenMode.Read)) { - var reader = new DataReader(stream) { - DefaultEncoding = new EscapeOutRangeEncoding("ascii"), - Endianness = EndiannessMode.BigEndian, - }; - - string id = reader.ReadString(4); - int offset = reader.ReadInt32(); - reader.SkipPadding(32); - double myDouble = reader.ReadDouble(); - - string name; - stream.RunInPosition( - () => name = reader.ReadString(), - offset); - } -} - -public void SaveFile(string path) -{ - using (var stream = DataStreamFactory.FromFile(path, FileOpenMode.Read)) { - var writer = new DataWriter(stream); - - writer.Write("TEX0", false); - writer.Write(0xCAFE); - writer.Write(0x00); - writer.WritePadding(0xFF, 32); - writer.Write("My long text of 80 bytes", 80); - - stream.PushToPosition(0x08); - writer.Write(0x65402); - stream.PopPosition(); - } -} -``` - -#### Reading / writing a text file - -```csharp -public void LoadFile(DataStream stream) -{ - var reader = new TextDataReader(stream, Encoding.Unicode); - - string firstLine = reader.ReadLine(); - char[] someChars = reader.Read(4); - string beforeToken = reader.ReadToToken("#"); - - if (reader.Peek() == ':') - reader.ReadLine(); - string restFile = reader.ReadToEnd(); -} - -public void SaveFile(DataStream stream) -{ - var writer = new TextDataWriter(stream) { - AutoPreamble = true, - }; - - writer.WriteLine("Hello world!"); - writer.WriteLine("Count is {0}", 42); - writer.Write("No new line"); -} -``` - -## Implementing file formats - -Every game contains many files, which have a specific formats. For example files -with extension `.nclr` are a palettes, or `.aar` are a package files. Yarhl -helps you to code type as you were actually coding a game format. - -To implement a file format, you just need to create a new class that implements -the (empty) [`IFormat`](xref:Yarhl.FileFormat.IFormat) interface. In this class -you just need to add the fields of your format. In more programming terms, your -format it's just a data model. - -Let's go for a quick example! Take a look into the following bytes from a file -that seems to have text from a game menu: - -![Hex view of example file](../images/hex_example.png) - -This file seems to follow the following specification in little endian: - -| Size | Name | -| ---- | ------------------------- | -| 4 | Magic ID | -| 2 | Number of sentences | -| 2 | Size of the file | -| \* | Null-terminated sentences | - -So given this format, we would implement the following class that maps the -specification: - -```csharp -public class MenuSentences : IFormat -{ - public MenuSentences() - { - Sentences = new Collection(); - } - - public uint MagicID { get; set; } - - public ushort FileSize { get; set; } - - public Collection Sentences { get; private set; } -} -``` - -Easy! Don't worry about how to convert that format, we will talk about that -later. - -### BinaryFormat - -[`BinaryFormat`](xref:Yarhl.IO.BinaryFormat) is the most basic format since it -just represents raw bytes, a stream. It's... a _binary format_. This format is -assigned automatically when we open a file from Yarhl as we will see later. - -Its only property [`Stream`](xref:Yarhl.IO.BinaryFormat.Stream*) allows you to -access to its inner stream. - -### NodeContainerFormat - -You may wonder... what about package formats like `.zip`? They are represented -with the format (or by inheriting it) -[`NodeContainerFormat`](xref:Yarhl.FileSystem.NodeContainerFormat). This format -contains a root folder, also known as `Node`. So let's see what a `Node` is. - -## Entering the virtual world: Nodes - -This is the main feature of Yarhl and the most important one, no doubt, 10/10 -Yarhl users would say so1. Yarhl has a virtual file system to handle -your files while maintaining your computer intact, you can now delete your -"tests" folder and clean your desktop after-ages. - -1 None of Yarhl users wants to talk with me anymore. This may -not be 100% accurate. - -### Nodes - -A [`Node`](xref:Yarhl.FileSystem.Node) is a virtual file. It's like having a -file system with files and folder but only in memory for the duration of your -program. You can dynamically add and remove files / folders. These files and -folders are called nodes in Yarhl. - -A node may have child nodes like a folder may have folders and files. You can -add the _subnodes_ with the -[`Add`]() method and you iterate -and access to its children with the -[`Children`](xref:Yarhl.FileSystem.NavigableNode`1.Children) property. - -The node [`Name`](xref:Yarhl.FileSystem.NavigableNode`1.Name) must be unique. -You can also get the full path to the node in this new virtual filesystem. That -is, if you have a _root_ node with name `MyRoot` and you add a node `Node1`, the -[`Path`](xref:Yarhl.FileSystem.NavigableNode`1.Path) property for `Node1` will -be `/MyRoot/Node1`. - -Ah, one more thing before I forget. Regular files in your disk have some bytes -associated, right? Well, in the case of nodes they have a -[Format](#implementing-file-formats) that we were talking before. That is, it -doesn't have to have bytes but it could be a type to represent image, texture, -text, font, ... The actual type of the node. For instance, let's say we create a -node from a disk file, it will have a `BinaryFormat` because for now it's just a -bunch of bytes. But if those bytes store a set of menu texts, we could transform -its format and associate its actual content type: `MenuSentences`. To the node -[`Format`](xref:Yarhl.FileSystem.Node.Format*) property you can set any type -that implements the [`IFormat`](xref:Yarhl.FileFormat.IFormat) interface. - -By the way, there is a property to get the inner `DataStream` when the format of -the node is a `BinaryFormat`: [`Stream`](xref:Yarhl.FileSystem.Node.Stream). It -will return `null` if the type is not `BinaryFormat`. We added it because to do -cool things like: - -```csharp -node.Stream.WriteTo("/home/mister_yarhl/my_node.bin"); -``` - -If the type is different, you may want to check the method -[`GetFormatAs()`](xref:Yarhl.FileSystem.Node.GetFormatAs``1). - -### Why nodes? - -Well, imagine that you have a Nintendo DS game `.nds`, you could open it with -Yarhl and access to its files and folders without actually extracting the files -in your disk. All access would be in memory, probably even sharing the same -stream thanks to the `sub-DataStreams`. - -### Files vs folders - -As said, folders in our virtual file system are also nodes. They always have the -`NodeContainerFormat` or any class that inherits it. This format is just a root -node folder that becomes the children of the node. You can use the property -[`IsContainer`](xref:Yarhl.FileSystem.Node.IsContainer*) to check if the node is -a node container, that is, if it's a folder. - -It's more clear with a picture: - -![A directory named with tho files inside](../images/node_example.png) - -`mastering` would be our root node with format `NodeContainerFormat`. While -`example.example` and `example2.example` would be our two child nodes. By -default they will have a `BinaryFormat` format. - -### Navigating the sea of nodes - -Do you have many nodes? I guess it will be difficult to navigate through all of -them. Then, you want to review the class -[`Navigator`](xref:Yarhl.FileSystem.Navigator). It provides with features like -iterators and finders. - -To iterate over the node children you would just do: - -```csharp -foreach (var node in root.Children) { - // Something -} -``` - -But if you want to iterate over the full tree of nodes, that is, including the -children of your children (like `node` in the above example), you can use the -`Navigator`. - -```csharp -foreach (var node in Navigator.IterateNodes(root)) { - // Something -} -``` - -You can even specify how to do the iteration: going first deeper into each node -([_depth-first search_](https://en.wikipedia.org/wiki/Depth-first_search)) or -getting first every children from the current node -([_breadth-first search_](https://en.wikipedia.org/wiki/Breadth-first_search)). - -If you just want to search a specific node given a full path, you want to use -[`Navigator.SearchNode(path)`](). - -### Creating the file system - -So you want to start using node right now, eh. Well, it will be easy. You will -want to check out the static helper class: -[`NodeFactory`](xref:Yarhl.FileSystem.NodeFactory). Let's check some of its -methods: - -```csharp -// Manual -var node1 = new Node("name", new BinaryFormat(filePath)); - -// Factory -var node2 = NodeFactory.FromFile(filePath); // Node name is the file name -``` - -So, what about creating a node from a folder you would say. - -```csharp -var emptyFolder = NodeFactory.CreateContainer("name"); - -var folderFromDisk = NodeFactory.FromDirectory(folderPath); -``` - -Yeeeah! That's the face I was looking for! You can create a virtual file that -quick! - -Don't forget to review the overloads and the method -[`FromMemory`]() to -create a node with data in memory. - -Yarhl is way more interesting now, right!? - -## Converters: putting together all the pieces - -Finally! We have _formats_, _nodes_ and some classes for _IO_ operations. Now -everything begin to fall into place, you'll see. - -Creating a tool to work with files usually require to work with different -formats, right? We need to convert from `.dat` to `.txt`, from `.bin` into a a -palette or unpacking several files from a `.pak` file. - -Well, that's easy to do. Yarhl is all about converting formats, let's see an -example: - -```csharp -public void ExportFontImage(string fontPath, string outputPath) -{ - using (var binary = new BinaryFormat(fontPath)) { - var font = (Font)ConvertFormat.With(binary); - var image = (Image)ConvertFormat.With(font); - image.Save(outputPath); - } -} -``` - -1. We start creating a new `BinaryFormat` from a file path. -2. We convert the `BinaryFormat` (reading its `Stream`) into a `Font` type. -3. We convert the `Font` format into an `Image` type. -4. We save it to a physical file in the hard-drive. - -_"But what's `Font2Binary` and `Font2Image`, **Mister Yarhl**?"_, you would say. -They are **converters**! A converter is a class which implements the -[IConverter](xref:Yarhl.FileFormat.IConverter`2) interface. - -You can check some converter examples from the tools to translate _Pokรฉmon -Conquest_. For instance: -[Font2Binary](https://github.com/pleonex/PokemonConquest/blob/master/AmbitionConquest/AmbitionConquest/Fonts/Font2Binary.cs) -and -[Font2Image](https://github.com/pleonex/PokemonConquest/blob/master/AmbitionConquest/AmbitionConquest/Fonts/Font2Image.cs). - -### Converting formats - -But let's come back to our example of `MenuSentences` from the -[format](#implementing-file-formats) section. It's turn to create a converter to -fill the `MenuSentences` class from a file. That it's to _read_ a file with that -format. - -```csharp -public class Binary2MenuSentences : IConverter -{ - public MenuSentences Convert(BinaryFormat source) - { - var menu = new MenuSentences(); - var reader = new DataReader(source.Stream); - - menu.MagicID = reader.ReadUInt32(); - ushort numSentences = reader.ReadUInt16(); - menu.FileSize = reader.ReadUInt16(); - - for (int i = 0; i < numSentences; i++) { - menu.Sentences.Add(reader.ReadString()); - } - - return menu; - } -} -``` - -So now we can get our menu instance with: - -```csharp -public void ReadMenuFile(string filePath) -{ - using (var binary = new BinaryFormat(filePath)) { - var menu = (MenuSentences)ConvertFormat.With(binary); - // Do something with the menu instance - } -} -``` - -Since there is just one converter `BinaryFormat -> MenuSentences` we can -simplify it even more: - -```csharp -public void ReadMenuFile(string filePath) -{ - using (var binary = new BinaryFormat(filePath)) { - MenuSentences menu = ConvertFormat.To(binary); - // Do something with the menu instance - } -} -``` - -Do you want to write your _new_ or _updated_ menu instance? Let's write a -converter to convert from `MenuSentences` to `BinaryFormat` and save in a file -in disk. - -```csharp -public class MenuSentences2Binary : IConverter -{ - public BinaryFormat Convert(MenuSentences menu) - { - var binary = new BinaryFormat(); - var writer = new DataWriter(binary.Stream); - - writer.Write(menu.MagicID); - writer.Write((ushort)menu.Sentences.Count); - writer.Write((ushort)0x00); // Placeholder size to override later - - foreach (string sentence in menu.Sentences) { - writer.Write(sentence); - } - - binary.Stream.Position = 0x06; - writer.Write((ushort)binary.Stream.Length); - - return binary; - } -} -``` - -And that's it! I'm pretty sure you've got enough of converters - -### Transforming nodes - -Don't forget that a node can have a format. How do we _convert_ the format from -a node? We could use the approach from before, but there are two methods that -will **convert and update** the format of the node: -[`TransformTo`](xref:Yarhl.FileSystem.Node.TransformTo*) and -[`TransformWith`](xref:Yarhl.FileSystem.Node.TransformWith*). They will also -dispose the old format so we don't need to do anything, just transform several -times the format of our node until it's the one we want. - -```csharp -var node = NodeFactory.FromFile(path); -node.TransformTo(); - -// Now node.Format is MenuSentences -``` - -or from the first example: - -```csharp -public void ExportFontImage(string fontPath, string outputPath) -{ - using (var node = NodeFactory.FromFile(fontPath)) { - node.TransformWith() - .TransformWith(); - - node.GetFormatAs().Save(outputPath); - } -} -``` diff --git a/docs/guides/toc.yml b/docs/guides/toc.yml deleted file mode 100644 index 8f1775ad..00000000 --- a/docs/guides/toc.yml +++ /dev/null @@ -1,12 +0,0 @@ -- name: Guides - items: - - name: Yarhl in a nutshell - href: Yarhl-nutshell.md - - - name: Cookbook - href: Cookbook.md - -- name: Contributing - items: - - name: Guidelines - href: Contributing.md diff --git a/docs/images/favicon-48.png b/docs/images/favicon-48.png deleted file mode 100644 index e49e90f4..00000000 Binary files a/docs/images/favicon-48.png and /dev/null differ diff --git a/docs/images/favicon.png b/docs/images/favicon.png deleted file mode 100644 index afcdd0bb..00000000 Binary files a/docs/images/favicon.png and /dev/null differ diff --git a/docs/images/hex_example.png b/docs/images/hex_example.png deleted file mode 100644 index 1029316d..00000000 Binary files a/docs/images/hex_example.png and /dev/null differ diff --git a/docs/images/logo-50.png b/docs/images/logo-50.png new file mode 100644 index 00000000..f70cb01e Binary files /dev/null and b/docs/images/logo-50.png differ diff --git a/docs/images/logo-large.png b/docs/images/logo-large.png new file mode 100644 index 00000000..7c91792f Binary files /dev/null and b/docs/images/logo-large.png differ diff --git a/docs/images/logo.png b/docs/images/logo.png index 7c91792f..06ba5be5 100644 Binary files a/docs/images/logo.png and b/docs/images/logo.png differ diff --git a/docs/images/mister.png b/docs/images/mister.png deleted file mode 100644 index 06ba5be5..00000000 Binary files a/docs/images/mister.png and /dev/null differ diff --git a/docs/images/node_example.png b/docs/images/node_example.png deleted file mode 100644 index d5a8f466..00000000 Binary files a/docs/images/node_example.png and /dev/null differ diff --git a/docs/index.md b/docs/index.md index d1950d12..3aeca610 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,118 @@ ---- -uid: README ---- +# Yarhl, A format ResearcH Library ![SceneGate awesome](https://img.shields.io/badge/SceneGate-awesome%20%F0%9F%95%B6-blue?logo=csharp) -[!include[README](../README.md)] +![Yarhl logo](./images/logo-large.png) + + +

+ + Stable version + +   + + GitHub commits since latest release (by SemVer) + +   + + Build and release + +   + + CII Best Practices + +   + + MIT License + +   +

+ +_Yarhl_ is a set of libraries that helps to **implement and convert file +formats** It empowers you with... + +- โ™ป๏ธ ... APIs to easily **convert** between custom formats. +- ๐Ÿ“š ... **guidelines** to implement and test custom format converters. +- ๐Ÿ”ข ... advance **binary and text** reading / writing, encoding and + serialization. +- ๐Ÿ“ƒ ... **standard formats** implementation like **PO** for translations. +- ๐Ÿ“‚ ... virtual **file system** to unpack and pack containers efficiently. + +## Usage + +The project has the following .NET libraries (NuGet packages via nuget.org). The +libraries only support the latest .NET LTS version: **.NET 6.0**. + +- [![Yarhl](https://img.shields.io/nuget/v/Yarhl?label=Yarhl&logo=nuget)](https://www.nuget.org/packages/Yarhl) + - `Yarhl.FileFormat`: format conversion APIs. + - `Yarhl.FileSystem`: virtual file system. + - `Yarhl.IO`: streams, binary and text reading / writing. +- [![Yarhl.Media.Text](https://img.shields.io/nuget/v/Yarhl.Media.Text?label=Yarhl.Media.Text&logo=nuget)](https://www.nuget.org/packages/Yarhl.Media.Text) + - `Yarhl.Media.Text`: translation formats and converters (Po), table replacer. + - `Yarhl.Media.Text.Encoding`: _euc-jp_ and token-escaped encodings. +- [![Yarhl.Plugins](https://img.shields.io/nuget/v/Yarhl.Plugins?label=Yarhl.Plugins&logo=nuget)](https://www.nuget.org/packages/Yarhl.Plugins) + - `Yarhl.Plugins`: discover formats and converters from .NET assemblies. + +> [!NOTE] +> _Are you planning to try a preview version?_ +> Check-out the +> [GitHub project readme](https://github.com/SceneGate/Yarhl#install) for +> details on how to setup the NuGet preview feed for SceneGate projects. + +## Quick demo + +_Yarhl_ allows you to work with different file formats with an unified API for +conversion into binary (serialization / deserialization). Let's try to create a +new translatable file format +[PO](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) from +[Yarhl.Media.Text](./articles/media-text/po-format.md) and save it into disk. + +[!code-csharp[Demo PO](./../src/Yarhl.Examples/Introduction.cs?name=Demo_Po)] + +It's frequent to find formats that are _containers_. _Yarhl_ allows to have a +_virtual file system_ to work with its content without having to extract it into +disk (saving space and time). For instance, let's open a game file from +_Nintendo DS_ that contains thousand of files. Then we will navigate through its +files, unpacking, decompressing and finally converting one file into _PO_. We +can use the following libraries for this task: + +- [Ekona](https://scenegate.github.com/Ekona/): support of NDS game file system. +- [LayTea](https://github.com/pleonex/LayTea): support of formats from + _Professor Layton_ games. + +[!code-csharp[Demo containers](./../src/Yarhl.Examples/Introduction.cs?name=Demo_Containers)] + +## Showcase + +Some cool projects built with _Yarhl_: + +- [**Texim**](https://github.com/SceneGate/Texim): experimental API for image + file formats. +- [**Ekona**](https://scenegate.github.io/Ekona/): support Nintendo DS file + formats. +- [**Lemon**](https://github.com/SceneGate/Lemon/): support Nintendo 3DS file + formats. +- [**LayTea**](https://www.pleonex.dev/LayTea/): modding tools for _Professor + Layton_ games. +- [**Attack of Friday Monsters tools**](https://github.com/pleonex/AttackFridayMonsters): + modding tools for _Attack of the Friday Monsters_ game. +- [**Metatron**](https://github.com/TraduSquare/Metatron): translation framework + for _Shin Megami Tensei_ saga games. + +## License + +The software is licensed under the terms of the +[MIT license](https://choosealicense.com/licenses/mit/). diff --git a/docs/template/public/main.css b/docs/template/public/main.css new file mode 100644 index 00000000..fca2c28f --- /dev/null +++ b/docs/template/public/main.css @@ -0,0 +1,42 @@ +/* Changing the site font */ +@import url("https://fonts.googleapis.com/css2?family=Nunito:wght@100;400;700&display=swap"); +/* @import url('https://fonts.googleapis.com/css2?family=Inconsolata&display=swap'); */ +@import url("https://fonts.googleapis.com/css2?family=Fira Code&display=swap"); + +/*@import url('https://fonts.cdnfonts.com/css/cascadia-code');*/ +/* @font-face { + font-family: 'Cascadia Code'; + font-style: normal; + font-weight: 100; + src: local('Cascadia Code'), url('https://fonts.cdnfonts.com/s/29131/Cascadia.woff') format('woff'); +} */ + +:root { + --bs-font-sans-serif: "Nunito"; + --bs-font-monospace: "Fira Code"; +} + +/* Hide breadcrum bar on large screen as it only links to itself */ +@media (min-width: 768px) { + .actionbar { + display: none !important; + } +} + +/* Give more space for section separation in a doc */ +h2 { + margin-top: 2rem !important; +} + +/* Improve TOC with a line for categories (entries without link) */ +.toc span.name-only { + border-bottom-color: var(--bs-tertiary-color) !important; + border-bottom-width: 2px !important; + border-bottom-style: solid !important; + margin-bottom: 0 !important; + margin-top: 0.6rem !important; +} + +.toc span.name-only:first() { + margin-top: 0.4rem !important; +} diff --git a/docs/template/public/main.js b/docs/template/public/main.js new file mode 100644 index 00000000..74aae83e --- /dev/null +++ b/docs/template/public/main.js @@ -0,0 +1,9 @@ +export default { + iconLinks: [ + { + icon: "github", + href: "https://github.com/SceneGate/Yarhl", + title: "GitHub", + }, + ], +}; diff --git a/docs/templates/material b/docs/templates/material deleted file mode 160000 index 2cc98b18..00000000 --- a/docs/templates/material +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2cc98b18b70bcb5f7639269c58ab1e148eb12415 diff --git a/docs/toc.yml b/docs/toc.yml index 5df0e0c0..f19f033e 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,11 +1,14 @@ -- name: Home - href: index.md +- name: Core + href: articles/core/ -- name: Guides - href: guides/ +- name: Text formats + href: articles/media-text/ - name: API - href: dev/ + href: api/ + +- name: Changelog + href: articles/Changelog.md - name: GitHub href: https://github.com/SceneGate/Yarhl diff --git a/src/.editorconfig b/src/.editorconfig index 64703e4b..f19da40b 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -5,7 +5,7 @@ root = true [*.cs] ## Generic options -charset = utf-8-bom +charset = utf-8 indent_size = 4 indent_style = space tab_width = 8 @@ -14,7 +14,7 @@ end_of_line = lf insert_final_newline = true ## .NET style rules -### Organize usings +### Organize using dotnet_separate_import_directive_groups = false dotnet_sort_system_directives_first = true file_header_template = unset # too long to set here @@ -122,39 +122,39 @@ csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true ### Indentation preferences -csharp_indent_case_contents = true:warning -csharp_indent_switch_labels = true:warning -csharp_indent_labels = one_less_than_current:warning -csharp_indent_block_contents = true:warning -csharp_indent_braces = false:warning -csharp_indent_case_contents_when_block = false:warning +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false ### Spacing preferences -csharp_space_after_cast = false:warning -csharp_space_after_keywords_in_control_flow_statements = true:warning -csharp_space_between_parentheses = false:warning -csharp_space_before_colon_in_inheritance_clause = true:warning -csharp_space_after_colon_in_inheritance_clause = true:warning -csharp_space_around_binary_operators = before_and_after:warning -csharp_space_between_method_declaration_parameter_list_parentheses = false:warning -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false:warning -csharp_space_between_method_declaration_name_and_open_parenthesis = false:warning -csharp_space_between_method_call_parameter_list_parentheses = false:warning -csharp_space_between_method_call_empty_parameter_list_parentheses = false:warning -csharp_space_between_method_call_name_and_opening_parenthesis = false:warning -csharp_space_after_comma = true:warning -csharp_space_before_comma = false:warning -csharp_space_after_dot = false:warning -csharp_space_before_dot = false:warning -csharp_space_after_semicolon_in_for_statement = true:warning -csharp_space_before_semicolon_in_for_statement = false:warning -csharp_space_around_declaration_statements = false:warning -csharp_space_before_open_square_brackets = false:warning -csharp_space_between_empty_square_brackets = false:warning -csharp_space_between_square_brackets = false:warning +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_after_comma = true +csharp_space_before_comma = false +csharp_space_after_dot = false +csharp_space_before_dot = false +csharp_space_after_semicolon_in_for_statement = true +csharp_space_before_semicolon_in_for_statement = false +csharp_space_around_declaration_statements = false +csharp_space_before_open_square_brackets = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_square_brackets = false ### Wrapping preferences -csharp_preserve_single_line_statements = false:warning +csharp_preserve_single_line_statements = false csharp_preserve_single_line_blocks = true ## Naming styles rules @@ -163,6 +163,8 @@ dotnet_diagnostic.IDE1006.severity = warning ## Code analyzers ### .NET SDK dotnet_diagnostic.CA1303.severity = none # We don't translate exception and log messages from English +dotnet_diagnostic.IDE0058.severity = suggestion # Expression value is never used +dotnet_diagnostic.SA1025.severity = none # Allow spaces in comments to structure info ### StyleCop dotnet_diagnostic.SA1101.severity = none # Do not force to prefix local calls with 'this' @@ -174,8 +176,8 @@ dotnet_diagnostic.SA1633.severity = none # No XML-format header in source files ### SonarAnalyzer dotnet_diagnostic.S1135.severity = suggestion # It's almost inevitable to have TODO but add bug ID -# Special rules for test projects -[*Tests/**] +# Special rules for test and example projects +[{*Tests/**,*Examples/**}] dotnet_diagnostic.CS1591.severity = none # Disable documentation dotnet_diagnostic.CA1001.severity = none # No need to implement IDisposable in test classes with cleanup. dotnet_diagnostic.CA1034.severity = none # Public types in test classes for testing implementations @@ -185,5 +187,20 @@ dotnet_diagnostic.CA1305.severity = none # No culture method for quick test code dotnet_diagnostic.CA1307.severity = none # No culture method for quick test code dotnet_diagnostic.SA0001.severity = none # Disable documentation dotnet_diagnostic.SA1600.severity = none # Disable documentation +dotnet_diagnostic.SA1201.severity = none # Allow enums inside classes dotnet_diagnostic.S2699.severity = none # Assert may be in helper methods dotnet_diagnostic.S3966.severity = none # Dispose twice to test implementation + +[*Examples/**] +dotnet_diagnostic.SA1123.severity = none # Allow regions to insert code snippets in markdown +dotnet_diagnostic.SA1124.severity = none # Allow regions to insert code snippets in markdown +dotnet_diagnostic.S1481.severity = none # Allow unused variables +dotnet_diagnostic.IDE0059.severity = none # Allow unused variables +dotnet_diagnostic.IDE0060.severity = none # Allow unused variables +dotnet_diagnostic.RCS1163.severity = none # Allow unused variables +dotnet_diagnostic.SA1515.severity = none # Allow comments after #region +dotnet_diagnostic.SA1512.severity = none # Allow spaces after comments + +# TODO: Reconsider after bumping versions, seems to be buggy +dotnet_diagnostic.SA1313.severity = suggestion # Seems to be a bug with records +dotnet_diagnostic.SA1000.severity = none # Space before parentheses in new() diff --git a/src/.idea/.idea.Yarhl/.idea/.gitignore b/src/.idea/.idea.Yarhl/.idea/.gitignore new file mode 100644 index 00000000..165216ed --- /dev/null +++ b/src/.idea/.idea.Yarhl/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/.idea.Yarhl.iml +/projectSettingsUpdater.xml +/modules.xml +/contentModel.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/src/.idea/.idea.Yarhl/.idea/.name b/src/.idea/.idea.Yarhl/.idea/.name new file mode 100644 index 00000000..87f19058 --- /dev/null +++ b/src/.idea/.idea.Yarhl/.idea/.name @@ -0,0 +1 @@ +Yarhl \ No newline at end of file diff --git a/src/.idea/.idea.Yarhl/.idea/CakeRider.xml b/src/.idea/.idea.Yarhl/.idea/CakeRider.xml new file mode 100644 index 00000000..e921cbb8 --- /dev/null +++ b/src/.idea/.idea.Yarhl/.idea/CakeRider.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/src/.idea/.idea.Yarhl/.idea/indexLayout.xml b/src/.idea/.idea.Yarhl/.idea/indexLayout.xml new file mode 100644 index 00000000..bc2da5d3 --- /dev/null +++ b/src/.idea/.idea.Yarhl/.idea/indexLayout.xml @@ -0,0 +1,10 @@ + + + + + ../../yarhl + + + + + \ No newline at end of file diff --git a/src/.idea/.idea.Yarhl/.idea/vcs.xml b/src/.idea/.idea.Yarhl/.idea/vcs.xml new file mode 100644 index 00000000..8a1a68bc --- /dev/null +++ b/src/.idea/.idea.Yarhl/.idea/vcs.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 2cb8a764..75793b38 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -13,7 +13,7 @@ https://scenegate.github.io/Yarhl/ https://github.com/SceneGate/Yarhl icon.png - translation;localization;romhacking;fan-translation + binary;encoding;text;translation;localization;reverse-engineering README.md diff --git a/src/Yarhl.Examples/FileSystem/NodeExamples.cs b/src/Yarhl.Examples/FileSystem/NodeExamples.cs new file mode 100644 index 00000000..87bdbeb6 --- /dev/null +++ b/src/Yarhl.Examples/FileSystem/NodeExamples.cs @@ -0,0 +1,138 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples.FileSystem; + +using Yarhl.FileFormat; +using Yarhl.FileSystem; +using Yarhl.IO; +using Yarhl.Media.Text; + +public static class NodeExamples +{ + public static void Overview() + { + #region Overview + // Create node from a disk file + using Node game = NodeFactory.FromFile("game.nds", FileOpenMode.Read); + + // Use the `Binary2NitroRom` converter to convert the binary format + // into node containers (virtual file system tree). + game.TransformWith(); + + // Now we can access to every game file. For instance, we can export one file + Node gameFile = game.Children["data"].Children["graphics"].Children["map.bin"]; + + // Same FileStream but reading from different offsets. + // No disk writing was required. + bool isSame = gameFile.Stream.BaseStream == game.Stream.BaseStream; + #endregion + } + + public static void Children() + { + #region AccessChildren + using Node node = NodeFactory.FromDirectory("inputs/"); + + Node childByIndex = node.Children[0]; + Node childByName = node.Children["menu.txt"]; + Node subChild = node.Children["maps"].Children["map1.scr"]; + + foreach (Node child in node.Children) { + // ... + } + + for (int i = 0; i < node.Children.Count; i++) { + Node child = node.Children[i]; + // ... + } + #endregion + } + + public static void AddRemove() + { + #region AddRemove + using Node root = NodeFactory.CreateContainer("root"); + + Node child = new Node("file1", new Po()); + root.Add(child); + + Node childSameName = new Node("file1", new BinaryFormat()); + root.Add(childSameName); // now root.Children[0] has BinaryFormat + + Node child2 = new Node("file2"); + root.Add(child2); + + // The nodes are NOT disposed + root.Remove("file1"); + bool found = root.Remove(child2); // true + bool notFound = root.Remove("IDontExists"); // false + + // or alternatively + root.RemoveChildren(dispose: true); + #endregion + } + + public static void Transform() + { + #region Transform + using Node text = NodeFactory.FromFile("input.bin"); + + text.TransformWith(new XorEncryptor("password")); + text.TransformWith(); + #endregion + + #region TransformChain + using Node graphics = NodeFactory.FromFile("graphics.bin.lz") + .TransformWith(new XorEncryptor("password")) + .TransformWith() + .TransformWith(); + #endregion + } + + private sealed class Binary2NitroRom : IConverter + { + public NodeContainerFormat Convert(IBinary source) => + throw new NotImplementedException(); + } + + private sealed class Binary2Texts : IConverter + { + public Po Convert(IBinary source) => throw new NotImplementedException(); + } + + private sealed class LzDecompressor : IConverter + { + public IBinary Convert(IBinary source) => throw new NotImplementedException(); + } + + private sealed class XorEncryptor : IConverter + { + public XorEncryptor(string password) + { + } + + public IBinary Convert(IBinary source) => throw new NotImplementedException(); + } + + private sealed class Narc2Container : IConverter + { + public NodeContainerFormat Convert(IBinary source) => throw new NotImplementedException(); + } +} diff --git a/src/Yarhl.Examples/FileSystem/NodeFactoryExamples.cs b/src/Yarhl.Examples/FileSystem/NodeFactoryExamples.cs new file mode 100644 index 00000000..4022a465 --- /dev/null +++ b/src/Yarhl.Examples/FileSystem/NodeFactoryExamples.cs @@ -0,0 +1,127 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples.FileSystem; + +using Yarhl.FileFormat; +using Yarhl.FileSystem; +using Yarhl.IO; + +public static class NodeFactoryExamples +{ + public static void Constructor() + { + #region Constructor + IFormat nodeFormat = CreateNodeFormat(); + using var node = new Node("myNode", nodeFormat); + + using var nodeWithoutFormat = new Node("myNode2"); + #endregion + } + + public static void FromBinary() + { + #region BinaryData + using Node binMemoryNode = NodeFactory.FromMemory("memory"); + + byte[] data = new byte[] { 0xBA, 0xAD, 0xCA, 0xFE, 0xD0, 0x0D }; + using Node binArrayNode = NodeFactory.FromArray("bad coffe", data); + + using Node binSubArrayNode = NodeFactory.FromArray("coffee", data, 2, 2); + + var fileStream = new FileStream("file.bin", FileMode.Open); + using Node binStreamNode = NodeFactory.FromStream("stream", fileStream); + + using Node binSubStreamNode = NodeFactory.FromSubstream("substream", fileStream, 0x100, 0x80); + #endregion + } + + public static void FromFiles() + { + #region Files + using Node fileNode = NodeFactory.FromFile("inputs/file.bin", FileOpenMode.Read); + + using Node textsNode = NodeFactory.FromFile("inputs/file.bin", "texts", FileOpenMode.ReadWrite); + + FileInfo fileInfo = fileNode.Tags["FileInfo"]; + #endregion + } + + public static void CreateContainer() + { + #region Container + using Node container = NodeFactory.CreateContainer("graphics"); + #endregion + } + + public static void FromDirectory() + { + #region Directory1 + // File system: + // inputs/ + // |- menu.txt + // |- logo.png + // |- maps/ + // |--- names.txt + // |--- world.png + // |--- scenarios/ + // |----- map1.png + // |--- regular_font.ttf + + using Node inputNode = NodeFactory.FromDirectory("inputs/"); + Console.WriteLine(inputNode.Name); // input + Console.WriteLine(inputNode.Children.Count); // 2: menu.txt and logo.png + + using Node topTexts = NodeFactory.FromDirectory("inputs/", "*.txt"); + Console.WriteLine(inputNode.Children.Count); // 1: menu.txt + #endregion + + #region Directory2 + // Same as 'topTexts' with custom name + using Node topTextsWithName = NodeFactory.FromDirectory("inputs/", "*.txt", "texts"); + + // Node with full hierarchy and filter + using Node images = NodeFactory.FromDirectory("inputs/", "*.png", "images", true); + Node map1 = images.Children["maps"].Children["scenarios"].Children["map1.png"]; + #endregion + + #region Directory3 + static bool IsScenarioImageOrFont(string path) => + (path.StartsWith("inputs/maps/scenarios") && path.EndsWith(".png")) + || path.EndsWith(".ttf"); + + using Node scenarios = NodeFactory.FromDirectory("inputs/", IsScenarioImageOrFont, "data", true); + #endregion + } + + public static void CreateHierarchy() + { + #region CreateHierarchy + using Node root = NodeFactory.CreateContainer("root"); + + string childPath = "data/gfx/scene1"; + using Node child = new Node("child1", CreateNodeFormat()); + + NodeFactory.CreateContainersForChild(root, childPath, child); + Node sameChild = root.Children["data"].Children["gfx"].Children["scene1"].Children["child1"]; + #endregion + } + + private static IFormat CreateNodeFormat() => new BinaryFormat(); +} diff --git a/src/Yarhl.Examples/Formats/AdvancedConverters.cs b/src/Yarhl.Examples/Formats/AdvancedConverters.cs new file mode 100644 index 00000000..11c32385 --- /dev/null +++ b/src/Yarhl.Examples/Formats/AdvancedConverters.cs @@ -0,0 +1,168 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples.Formats; + +using Yarhl.FileFormat; +using Yarhl.FileSystem; +using Yarhl.IO; + +#region ManyToOneFont +public class Font2Binary : IConverter +{ + private readonly RgbImage fontImage; + + public Font2Binary(RgbImage fontImage) + { + this.fontImage = fontImage; + } + + public BinaryFormat Convert(Font source) + { + var binaryFont = new BinaryFormat(); + + // TODO: Use source and fontImage to serialize into binary. + return binaryFont; + } +} +#endregion + +#region ManyToOneIndexedImage +public class IndexedImage2RgbImage : IConverter +{ + private readonly Palette palette; + + public IndexedImage2RgbImage(Palette palette) + { + this.palette = palette; + } + + public RgbImage Convert(IndexedImage source) + { + var fullImage = new RgbImage(); + + // TODO: Convert each pixel into RGB using the provided palette. + return fullImage; + } +} +#endregion + +#region OneToManyIndexedImage +public class RgbImage2IndexedImage : IConverter +{ + public NodeContainerFormat Convert(RgbImage source) + { + // TODO: Run a quantization algorithm that generates a palette and indexed pixels + IndexedImage indexedImage = null; + Palette palette = null; + + var container = new NodeContainerFormat(); + + var indexedImageNode = new Node("image", indexedImage); + container.Root.Add(indexedImageNode); + + var paletteNode = new Node("palette", palette); + container.Root.Add(paletteNode); + + return container; + } +} +#endregion + +#region FontImporter +public class FontGlyphsImporter : IConverter +{ + private readonly RgbImage glyphs; + + public FontGlyphsImporter(RgbImage glyphs) + { + this.glyphs = glyphs; + } + + public Font Convert(Font source) + { + // TODO: Update Font instance with the glyph images + return source; + } +} +#endregion + +#region ExecutableTextImporter +public record TextBlockInfo(uint Position, string Text); + +public class ExecutableTextImporter : IConverter +{ + private readonly IEnumerable textInfos; + + public ExecutableTextImporter(IEnumerable textInfos) + { + this.textInfos = textInfos ?? throw new ArgumentNullException(nameof(textInfos)); + } + + public IBinary Convert(IBinary source) + { + var writer = new DataWriter(source.Stream); + + foreach (var info in textInfos) { + writer.Stream.Position = info.Position; + + // you should check it doesn't overwrite more data than it can + writer.Write(info.Text); + } + + return source; + } +} +#endregion + +public static class Program +{ + public static void OneToManyIndexedImage() + { + #region OneToManyIndexedImageProgram + using Node imageNode = NodeFactory.FromFile("image.png", FileOpenMode.Read) + .TransformWith() + .TransformWith(); + + var indexedImage = imageNode.Children["image"].GetFormatAs(); + var palette = imageNode.Children["palette"].GetFormatAs(); + #endregion + } +} + +public class Font : IFormat +{ +} + +public class RgbImage : IFormat +{ +} + +public class IndexedImage : IFormat +{ +} + +public class Palette : IFormat +{ +} + +public class BinaryPng2RgbImage : IConverter +{ + public RgbImage Convert(IBinary source) => throw new NotImplementedException(); +} diff --git a/src/Yarhl.Examples/Formats/Converters.cs b/src/Yarhl.Examples/Formats/Converters.cs new file mode 100644 index 00000000..ed21f9f0 --- /dev/null +++ b/src/Yarhl.Examples/Formats/Converters.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples.Formats; + +using Yarhl.Media.Text; + +public class Converters +{ + public void SerializePo() + { + #region SerializePo + // Create a test PO model format to convert. + var poFormat = new Po(new PoHeader("software", "SceneGate", "en-US")); + poFormat.Add(new PoEntry("Hello, world!")); + + // Create a new converter instance + var po2binary = new Po2Binary(); + + // Convert! + using var binaryPoFormat = po2binary.Convert(poFormat); + + // Binary format is a wrapper over a DataStream (enhanced System.IO.Stream) + // We can now save the Stream into a file + binaryPoFormat.Stream.WriteTo("strings.po"); + #endregion + } +} diff --git a/src/Yarhl.Examples/Formats/Formats.cs b/src/Yarhl.Examples/Formats/Formats.cs new file mode 100644 index 00000000..4f69c58c --- /dev/null +++ b/src/Yarhl.Examples/Formats/Formats.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples.Formats; + +using System.Collections.ObjectModel; +using Yarhl.FileFormat; + +public static class Formats +{ + #region FormatImpl + public class GameTextFormat : IFormat + { + public Collection Texts { get; init; } + + public int SceneId { get; set; } + } + #endregion + + #region FormatWrapper + public class SoundFormat : ThirdPartyWave, IFormat + { + } + #endregion + + public class ThirdPartyWave + { + } + + #region CloneableFormat + public class Image : ICloneableFormat + { + public int Width { get; set; } + + public int Height { get; set; } + + public byte[] IndexedPixels { get; set; } + + public object DeepClone() + { + return new Image { + Width = Width, + Height = Height, + IndexedPixels = IndexedPixels.ToArray(), + }; + } + } + #endregion +} diff --git a/src/Yarhl.Examples/Introduction.cs b/src/Yarhl.Examples/Introduction.cs new file mode 100644 index 00000000..22e1928c --- /dev/null +++ b/src/Yarhl.Examples/Introduction.cs @@ -0,0 +1,97 @@ +๏ปฟ// Copyright (c) 2022 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples; + +using Yarhl.FileFormat; +using Yarhl.FileSystem; +using Yarhl.IO; +using Yarhl.Media.Text; + +internal static class Introduction +{ + internal static void ExportText(string gameFilePath) + { + #region Demo_Containers + // 1. Read the game file system from a file (deserialize) + using Node game = NodeFactory.FromFile(gameFilePath, FileOpenMode.Read) + .TransformWith(); + + // 2. Navigate to the container that has our text file and unpack it. + Node msgNode = Navigator.SearchNode(game, "data/ll/common/ll_common.darc") + .TransformWith() // binary -> file system (container) + .Children[2] // text file is the third file + .TransformWith(); // the file is compressed with LZSS + + // 3. Convert its proprietary binary format into industry-standard translation format PO. + // As it's a big text file, the converter splits the content into different files. + msgNode.TransformWith() + .TransformWith(LondonLifeRegion.Usa); + + foreach (var children in msgNode.Children) { + // 4. Convert the PO into a binary format (serialize) and write to disk + children.TransformWith() + .Stream.WriteTo(children.Name + ".po"); + } + #endregion + + #region Demo_Po + // Let's create a new PO format + var metadata = new PoHeader("software1", "SceneGate", "es-ES"); + Po translatableContent = new Po(metadata); + + // Now let's add one entry. + var entry1 = new PoEntry("Hello world!"); + entry1.Translated = "ยกHola mundo!"; + translatableContent.Add(entry1); + + // Finally let's save the format into a file on disk + var serializer = new Po2Binary(); + using BinaryFormat binaryPo = serializer.Convert(translatableContent); + binaryPo.Stream.WriteTo("strings.po"); + #endregion + } + + // Fake converters to avoid external dependencies + private sealed class Binary2NitroRom : IConverter + { + } + + private sealed class BinaryDarc2Container : IConverter + { + } + + private sealed class DencDecompression : IConverter + { + } + + private sealed class Binary2MessageCollection : IConverter + { + } + + private sealed class MessageCollection2PoContainer : IConverter, IInitializer + { + public void Initialize(LondonLifeRegion parameters) => throw new NotImplementedException(); + } + + private enum LondonLifeRegion + { + Usa = 0, + } +} diff --git a/src/Yarhl.Examples/Program.cs b/src/Yarhl.Examples/Program.cs new file mode 100644 index 00000000..6ac700c1 --- /dev/null +++ b/src/Yarhl.Examples/Program.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2022 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples; + +public static class Program +{ + public static void Main(string[] args) + { + // Just used to test from time to time. + Tutorial.Program.FakeMain2(args); + } +} diff --git a/src/Yarhl.Examples/Tutorial/Binary2Txti.cs b/src/Yarhl.Examples/Tutorial/Binary2Txti.cs new file mode 100644 index 00000000..30b96d57 --- /dev/null +++ b/src/Yarhl.Examples/Tutorial/Binary2Txti.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples.Tutorial; + +#region Class +using System.Text; +using Yarhl.FileFormat; +using Yarhl.IO; + +public class Binary2Txti : IConverter +{ +#endregion + + #region ValidateHeader + public TxtiFormat Convert(IBinary source) + { + var reader = new DataReader(source.Stream); + + if (reader.ReadString(bytesCount: 4) != "TXTI") { + throw new FormatException("Invalid format"); + } + #endregion + + #region ReadEntries + var txtiFormat = new TxtiFormat(); + + int numEntries = reader.ReadInt32(); + for (int i = 0; i < numEntries; i++) { + var entry = new TextEntry(); + entry.Id = reader.ReadUInt16(); + entry.Text = reader.ReadString(Encoding.Unicode); + + txtiFormat.Entries.Add(entry); + } + + return txtiFormat; + #endregion + } +} diff --git a/src/Yarhl.Examples/Tutorial/Program.cs b/src/Yarhl.Examples/Tutorial/Program.cs new file mode 100644 index 00000000..3204bfcc --- /dev/null +++ b/src/Yarhl.Examples/Tutorial/Program.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples.Tutorial; + +using Yarhl.FileSystem; +using Yarhl.IO; +using Yarhl.Media.Text; + +public static class Program +{ + public static void FakeMain(string[] args) + { + #region OpenFile + DataStream fileStream = DataStreamFactory.FromFile(args[0], FileOpenMode.Read); + using var binaryFormat = new BinaryFormat(fileStream); + #endregion + + #region Deserialize + var deserializer = new Binary2Txti(); + var txti = deserializer.Convert(binaryFormat); + + Console.WriteLine($"Number of entries: {txti.Entries.Count}"); + Console.WriteLine($"First text: '{txti.Entries[0].Text}'"); + #endregion + } + + public static void FakeMain2(string[] args) + { + #region ExportNodes + // Create a node from a file (BinaryFormat) + using Node node = NodeFactory.FromFile(args[0], FileOpenMode.Read); + + // Transform chaining conversions fluent-like. + node.TransformWith(new Binary2Txti()) + .TransformWith(new Txti2Po()) + .TransformWith(new Po2Binary()); + + // Save our new binary data into disk + node.Stream!.WriteTo(args[1]); + #endregion + } +} diff --git a/src/Yarhl.Examples/Tutorial/TextEntry.cs b/src/Yarhl.Examples/Tutorial/TextEntry.cs new file mode 100644 index 00000000..7f08ee1e --- /dev/null +++ b/src/Yarhl.Examples/Tutorial/TextEntry.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples.Tutorial; + +#region Class +public class TextEntry +{ + public ushort Id { get; set; } + + public string Text { get; set; } +} +#endregion diff --git a/src/Yarhl.Examples/Tutorial/Txti2Po.cs b/src/Yarhl.Examples/Tutorial/Txti2Po.cs new file mode 100644 index 00000000..6dd2cc00 --- /dev/null +++ b/src/Yarhl.Examples/Tutorial/Txti2Po.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples.Tutorial; + +#region Converter +using Yarhl.FileFormat; +using Yarhl.Media.Text; + +public class Txti2Po : IConverter +{ + public Po Convert(TxtiFormat source) + { + var metadata = new PoHeader("MyProject", "me@example.com", "en"); + var po = new Po(metadata); + + foreach (var entry in source.Entries) { + var poEntry = new PoEntry(entry.Text); + poEntry.Context = $"ID: 0x{entry.Id:X2}"; + + po.Add(poEntry); + } + + return po; + } +} +#endregion diff --git a/src/Yarhl.Examples/Tutorial/TxtiFormat.cs b/src/Yarhl.Examples/Tutorial/TxtiFormat.cs new file mode 100644 index 00000000..50789ecd --- /dev/null +++ b/src/Yarhl.Examples/Tutorial/TxtiFormat.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2023 SceneGate + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +namespace Yarhl.Examples.Tutorial; + +#region Class +using System.Collections.ObjectModel; +using Yarhl.FileFormat; + +public class TxtiFormat : IFormat +{ + public Collection Entries { get; init; } = new(); +} +#endregion diff --git a/src/Yarhl.Examples/Yarhl.Examples.csproj b/src/Yarhl.Examples/Yarhl.Examples.csproj new file mode 100644 index 00000000..c014aebf --- /dev/null +++ b/src/Yarhl.Examples/Yarhl.Examples.csproj @@ -0,0 +1,16 @@ + + + + Exe + net6.0 + enable + + false + + + + + + + + diff --git a/src/Yarhl.sln b/src/Yarhl.sln index 49a70031..47e47418 100644 --- a/src/Yarhl.sln +++ b/src/Yarhl.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yarhl.IntegrationTests", "Y EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yarhl.PerformanceTests", "Yarhl.PerformanceTests\Yarhl.PerformanceTests.csproj", "{ADF44DD6-B355-45FB-A50A-06CBCE2EFF6E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yarhl.Examples", "Yarhl.Examples\Yarhl.Examples.csproj", "{F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,5 +88,17 @@ Global {ADF44DD6-B355-45FB-A50A-06CBCE2EFF6E}.Release|x64.Build.0 = Release|Any CPU {ADF44DD6-B355-45FB-A50A-06CBCE2EFF6E}.Release|x86.ActiveCfg = Release|Any CPU {ADF44DD6-B355-45FB-A50A-06CBCE2EFF6E}.Release|x86.Build.0 = Release|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Debug|x64.ActiveCfg = Debug|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Debug|x64.Build.0 = Debug|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Debug|x86.ActiveCfg = Debug|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Debug|x86.Build.0 = Debug|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Release|Any CPU.Build.0 = Release|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Release|x64.ActiveCfg = Release|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Release|x64.Build.0 = Release|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Release|x86.ActiveCfg = Release|Any CPU + {F6CF5BDF-FD87-47DB-9BB3-0B9369236B85}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/Yarhl.sln.DotSettings b/src/Yarhl.sln.DotSettings new file mode 100644 index 00000000..3226316e --- /dev/null +++ b/src/Yarhl.sln.DotSettings @@ -0,0 +1,4 @@ +๏ปฟ + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> + True \ No newline at end of file