Compare commits

...

54 commits

Author SHA1 Message Date
dcronqvist
7f78a971f9
Merge pull request #67 from dcronqvist/dev
Release v0.3.0
2024-12-02 22:03:37 +01:00
dcronqvist
de41fb5508
Merge pull request #68 from dcronqvist/fix-before-release
Add new version stuff
2024-12-02 22:02:20 +01:00
Daniel Cronqvist
a38df45869 Add new version stuff 2024-12-02 22:00:04 +01:00
dcronqvist
58e03188a8
Merge pull request #66 from dcronqvist/fix-benchmarks
Fix benchmarks and update ratio numbers in README
2024-12-02 21:35:23 +01:00
Daniel Cronqvist
111403d7fc Fix benchmarks and update ratio numbers in README 2024-12-02 21:33:09 +01:00
dcronqvist
fc710daf8c
Merge pull request #65 from dcronqvist/enum-bug
Enum properties were not being properly parsed due to incorrect usage of Optional
2024-12-02 20:55:31 +01:00
Daniel Cronqvist
88ceee46e5 Add some test cases for the enum parsing bug and fix issue with .tmj format 2024-11-27 22:53:28 +01:00
Daniel Cronqvist
b978b8b50d Add documentation about enum property behaviour 2024-11-27 22:20:42 +01:00
Daniel Cronqvist
ade3d8840a Fix bug where enum properties were mistakenly parsed as uint when they were string 2024-11-27 22:15:24 +01:00
dcronqvist
f3c4478125
Merge pull request #63 from dcronqvist/color-bug
Unset colors are now parsed correctly, color properties have optional colors
2024-11-22 21:18:59 +01:00
Daniel Cronqvist
94c1ac0f32 Unset colors are now parsed correctly, color properties have optional colors 2024-11-21 20:55:51 +01:00
dcronqvist
6deb28c1ce
Merge pull request #57 from dcronqvist/custom-types-not-required
Make custom types optional
2024-11-21 18:24:47 +01:00
dcronqvist
374f2b2194
Merge pull request #62 from dcronqvist/from-class-docs
Add disclaimer about FromClass with classes that contain enums
2024-11-21 18:05:05 +01:00
Daniel Cronqvist
f192a71c56 Add disclaimer about FromClass with classes that contain enums 2024-11-21 17:52:54 +01:00
dcronqvist
416cba6604
Merge pull request #61 from Metraberryy/enum-property-fromclass
Support enum properties in CustomClassDefinition.FromClass
2024-11-21 17:48:03 +01:00
Kat
7a7f360e22
Implement requested changes 2024-11-20 05:11:45 -08:00
Kat
1027b922fe
Enum properties in CustomClassDefinition.FromClass 2024-11-19 03:08:04 -08:00
dcronqvist
1e41443704
Merge pull request #60 from dcronqvist/overrideobject-bug
Add object override for rectangle objects
2024-11-17 15:56:37 +01:00
Daniel Cronqvist
c9e85c9fd6 Add object override for rectangle objects 2024-11-17 15:52:20 +01:00
dcronqvist
080f95c698
Merge pull request #59 from dcronqvist/fix-multiline-string-prop
Fix multiline string property value parsing
2024-11-17 15:13:30 +01:00
Daniel Cronqvist
90a57b125d Fix multiline string property value parsing 2024-11-17 09:15:19 +01:00
dcronqvist
52f148f71d
Merge pull request #58 from dcronqvist/tileobject-flippingflags
Add flipping flags parsing/clearing to tile objects
2024-11-17 09:03:25 +01:00
Daniel Cronqvist
54bc132154 Add flipping flags parsing/clearing to tile objects 2024-11-17 08:59:02 +01:00
Daniel Cronqvist
e553c8e05a Update custom type documentation with optionality disclaimer 2024-11-16 21:33:06 +01:00
Daniel Cronqvist
8c9068cc97 Make custom types optional 2024-11-16 21:14:23 +01:00
dcronqvist
67876c6532
Merge pull request #46 from differenceclouds/tilerendersize_fix
"rendersize" -> "tilerendersize" in TmxReaderBase.Tileset.cs
2024-11-16 19:39:19 +01:00
dcronqvist
66d59ffbe1
Merge pull request #56 from dcronqvist/fix-readme
Remove claims about map saving
2024-11-16 19:30:51 +01:00
Daniel Cronqvist
feb4375cd5 Make sure docs index.md depends on README.md as it is copied 2024-11-16 19:26:44 +01:00
Daniel Cronqvist
837f58bf68 Remove claims about being able to save Tiled maps 2024-11-16 19:26:14 +01:00
dcronqvist
23d218bac7
Merge pull request #54 from dcronqvist/from-enum-storage-type
Add storage type parameter to CustomEnumDefinition.FromEnum
2024-11-16 19:16:44 +01:00
dcronqvist
cbd03bc224
Merge pull request #55 from dcronqvist/fix-template-dir
Fix directory name of pull request templates
2024-11-16 19:06:52 +01:00
dcronqvist
dcdceb8b78
Fix directory name of pull request templates 2024-11-16 19:02:50 +01:00
Daniel Cronqvist
666a3433e3 Update docs section on CustomEnumDefinitions with new storage type parameter info 2024-11-16 18:48:15 +01:00
Daniel Cronqvist
2e8eaa5a72 Update FromEnum tests with storage type parameter 2024-11-16 18:35:59 +01:00
Daniel Cronqvist
35c6ba5002 Add storageType parameter to FromEnum, default value is consistent with Tiled 2024-11-16 18:35:21 +01:00
differenceclouds
50036075f5 "rendersize" -> "tilerendersize"
fix to match the XML.
2024-11-14 09:26:32 -05:00
dcronqvist
88352cfa45
Merge pull request #39 from dcronqvist/release-v0.2.1
Release v0.2.1
2024-10-04 21:26:48 +02:00
Daniel Cronqvist
d943a8d8b7 Update version in .csproj 2024-10-04 21:25:36 +02:00
Daniel Cronqvist
69f68d5853 Update version compatibility matrix 2024-10-04 21:22:43 +02:00
dcronqvist
aaa1ba7de4
Merge pull request #38 from dcronqvist/example-running
Make sure to run console example application as part of test suite
2024-10-04 21:18:16 +02:00
Daniel Cronqvist
e411d51573 Make sure to run console example application as part of test suite 2024-10-04 21:16:00 +02:00
dcronqvist
5969b509e4
Merge pull request #37 from Serdan/feature/visible-type-in-tileset
Fix type of visible attribute in tileset object group.
2024-10-04 21:05:42 +02:00
Anders Kehlet
782e771a41 Fix type of visible attribute in tileset object group. 2024-10-03 22:26:47 +02:00
dcronqvist
871270ab00
Merge pull request #36 from dcronqvist/dev
Documentation updates
2024-09-28 20:03:19 +02:00
dcronqvist
f8f21a8a7d
Merge pull request #35 from dcronqvist/remove-monogame-reference
Remove text about MonoGame support
2024-09-28 19:56:44 +02:00
dcronqvist
e8dc677341
Update README that is published with NuGet to reflect MonoGame support 2024-09-28 19:55:06 +02:00
dcronqvist
50c14011bc
Remove text about MonoGame support 2024-09-28 19:52:28 +02:00
dcronqvist
e66cd36f6d
Merge pull request #32 from krnlexception/dev
Example projects (.NET console and Godot)
2024-09-16 19:09:08 +02:00
krnlException
f72cfd397b Removed backup file 2024-09-16 16:33:07 +02:00
krnlexception
0515ba3256
Godot example 2024-09-13 19:32:24 +02:00
krnlException
3d649fab95 Program class to public and top-level warning disabled 2024-09-12 20:23:43 +02:00
krnlException
44cbf5b90a Style changes, and usage of embedded resources 2024-09-11 17:11:24 +02:00
krnlexception
eb22de169c
JetBrains Rider .idea directory ignore 2024-09-10 00:57:23 +02:00
krnlexception
7407edccb3
Example project 2024-09-10 00:56:24 +02:00
85 changed files with 1806 additions and 127 deletions

View file

@ -237,6 +237,7 @@ dotnet_diagnostic.IDE0008.severity = silent
dotnet_diagnostic.IDE0055.severity = silent
dotnet_diagnostic.IDE0058.severity = silent
dotnet_diagnostic.IDE0160.severity = none
dotnet_diagnostic.IDE0210.severity = none
dotnet_diagnostic.CA1707.severity = silent
dotnet_diagnostic.CA1852.severity = none
dotnet_diagnostic.CA1805.severity = none

1
.gitignore vendored
View file

@ -402,3 +402,4 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
.idea

View file

@ -1,6 +1,7 @@
test:
dotnet build src/DotTiled.sln
dotnet test src/DotTiled.sln
dotnet run --project src/DotTiled.Examples/DotTiled.Example.Console/DotTiled.Example.Console.csproj -- src/DotTiled.Examples/DotTiled.Example.Console
docs-serve: docs/index.md
docfx docs/docfx.json --serve
@ -8,7 +9,7 @@ docs-serve: docs/index.md
docs-build: docs/index.md
docfx docs/docfx.json
docs/index.md:
docs/index.md: README.md
cp README.md docs/index.md
lint:

View file

@ -2,7 +2,7 @@
<img src="https://www.mapeditor.org/img/tiled-logo-white.png" align="right" width="20%"/>
DotTiled is a simple and easy-to-use library for loading, saving, and managing [Tiled maps and tilesets](https://mapeditor.org) in your .NET projects. After [TiledCS](https://github.com/TheBoneJarmer/TiledCS) unfortunately became unmaintained (since 2022), I aimed to create a new library that could fill its shoes. DotTiled is the result of that effort.
DotTiled is a simple and easy-to-use library for loading [Tiled maps and tilesets](https://mapeditor.org) in your .NET projects. After [TiledCS](https://github.com/TheBoneJarmer/TiledCS) unfortunately became unmaintained (since 2022), I aimed to create a new library that could fill its shoes. DotTiled is the result of that effort.
DotTiled is designed to be a lightweight and efficient library that provides a simple API for loading and managing Tiled maps and tilesets. It is built with performance in mind and aims to be as fast and memory-efficient as possible.
@ -17,8 +17,8 @@ Other similar libraries exist, and you may want to consider them for your projec
|**Comparison**|**DotTiled**|[TiledLib](https://github.com/Ragath/TiledLib.Net)|[TiledCSPlus](https://github.com/nolemretaWxd/TiledCSPlus)|[TiledSharp](https://github.com/marshallward/TiledSharp)|[TiledCS](https://github.com/TheBoneJarmer/TiledCS)|[TiledNet](https://github.com/napen123/Tiled.Net)|
|---------------------------------|:-----------------------:|:--------:|:-----------:|:----------:|:-------:|:------:|
| Actively maintained | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Benchmark (time)* | 1.00 | 1.83 | 2.16 | - | - | - |
| Benchmark (memory)* | 1.00 | 1.43 | 2.03 | - | - | - |
| Benchmark (time)* | 1.00 | 1.78 | 2.11 | - | - | - |
| Benchmark (memory)* | 1.00 | 1.32 | 1.88 | - | - | - |
| .NET Targets | `net8.0` | `net8.0` |`netstandard2.1`|`netstandard2.0`|`netstandard2.0`|`net45`|
| Docs |Usage, API,<br>XML Docs|Usage|Usage, API,<br>XML Docs|Usage, API|Usage, XML Docs|Usage, XML Docs|
| License | MIT | MIT | MIT | Apache-2.0 | MIT | BSD 3-Clause |
@ -26,7 +26,7 @@ Other similar libraries exist, and you may want to consider them for your projec
> [!NOTE]
> *Both benchmark time and memory ratios are relative to DotTiled. Lower is better. Benchmark (time) refers to the execution time of loading the same map from an in-memory string that contains XML data in the `.tmx` format. Benchmark (memory) refers to the memory allocated during that loading process. For further details on the benchmark results, see the collapsible section below.
[MonoGame](https://www.monogame.net) users may also want to consider using [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended) for loading Tiled maps and tilesets. Like MonoGame.Extended, DotTiled also provides a way to properly import Tiled maps and tilesets with the MonoGame content pipeline (with the DotTiled.MonoGame.Pipeline NuGet). However, unlike MonoGame.Extended, DotTiled does *not* include any kind of rendering capabilities, and it is up to you as a developer to implement any kind of rendering for your maps when using DotTiled. The feature coverage by MonoGame.Extended is less than that of DotTiled, so you may want to consider using DotTiled if you need access to more Tiled features and flexibility.
[MonoGame](https://www.monogame.net) users may also want to consider using [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended) for loading Tiled maps and tilesets. The feature coverage by MonoGame.Extended is less than that of DotTiled, so you may want to consider using DotTiled if you need access to more Tiled features and flexibility.
<details>
<summary>
@ -36,7 +36,7 @@ Benchmark details
The following benchmark results were gathered using the `DotTiled.Benchmark` project which uses [BenchmarkDotNet](https://benchmarkdotnet.org/) to compare the performance of DotTiled with other similar libraries. The benchmark results are grouped by category and show the mean execution time, memory consumption metrics, and ratio to DotTiled.
```
BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4651/22H2/2022Update)
BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.5131/22H2/2022Update)
12th Gen Intel Core i7-12700K, 1 CPU, 20 logical and 12 physical cores
.NET SDK 8.0.202
[Host] : .NET 8.0.3 (8.0.324.11423), X64 RyuJIT AVX2
@ -44,12 +44,12 @@ BenchmarkDotNet v0.13.12, Windows 10 (10.0.19045.4651/22H2/2022Update)
```
| Method | Categories | Mean | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio |
|------------ |------------------------- |---------:|------:|-------:|-------:|----------:|------------:|
| DotTiled | MapFromInMemoryTmjString | 4.431 μs | 1.00 | 0.4349 | - | 5.58 KB | 1.00 |
| TiledLib | MapFromInMemoryTmjString | 6.369 μs | 1.44 | 0.7019 | 0.0153 | 9.01 KB | 1.61 |
| DotTiled | MapFromInMemoryTmjString | 4.602 μs | 1.00 | 0.5417 | - | 7 KB | 1.00 |
| TiledLib | MapFromInMemoryTmjString | 6.385 μs | 1.39 | 0.7019 | 0.0153 | 9.01 KB | 1.29 |
| | | | | | | | |
| DotTiled | MapFromInMemoryTmxString | 3.125 μs | 1.00 | 1.2817 | 0.0610 | 16.36 KB | 1.00 |
| TiledLib | MapFromInMemoryTmxString | 5.709 μs | 1.83 | 1.8005 | 0.0916 | 23.32 KB | 1.43 |
| TiledCSPlus | MapFromInMemoryTmxString | 6.757 μs | 2.16 | 2.5940 | 0.1831 | 33.16 KB | 2.03 |
| DotTiled | MapFromInMemoryTmxString | 3.216 μs | 1.00 | 1.3733 | 0.0610 | 17.68 KB | 1.00 |
| TiledLib | MapFromInMemoryTmxString | 5.721 μs | 1.78 | 1.8005 | 0.0916 | 23.32 KB | 1.32 |
| TiledCSPlus | MapFromInMemoryTmxString | 6.696 μs | 2.11 | 2.5940 | 0.1831 | 33.23 KB | 1.88 |
It is important to note that the above benchmark results come from loading a very small map with a single tile layer as I had to find a common denominator between the libraries so that they all could load the same map. The results aim to be indicative of the performance of the libraries, but should be taken with a grain of salt. Only the actively maintained libraries are included in the benchmark results. TiledCSPlus does not support the `.tmj` format, so it was not included for that benchmark category.

View file

@ -73,7 +73,10 @@ In addition to these primitive property types, [Tiled also supports more complex
Tiled allows you to define custom property types that can be used in your maps. These custom property types can be of type `class` or `enum`. DotTiled supports custom property types by allowing you to define the equivalent in C#. This section will guide you through how to define custom property types in DotTiled and how to map properties in loaded maps to C# classes or enums.
> [!NOTE]
> In the future, DotTiled could provide a way to configure the use of custom property types such that they aren't necessary to be defined, given that you have set the `Resolve object types and properties` setting in Tiled.
> While custom types are powerful, they will incur a bit of overhead as you attempt to sync them between Tiled and DotTiled. Defining custom types is recommended, but not necessary for simple use cases as Tiled supports arbitrary strings as classes.
> [!IMPORTANT]
> If you choose to use custom types in your maps, but don't define them properly in DotTiled, you may get inconsistencies between the map in Tiled and the loaded map with DotTiled. If you still want to use custom types in Tiled without having to define them in DotTiled, it is recommended to set the `Resolve object types and properties` setting in Tiled to `true`. This will make Tiled resolve the custom types for you, but it will still require you to define the custom types in DotTiled if you want to access the properties in a type-safe manner.
### Class properties
@ -138,7 +141,7 @@ The equivalent definition in DotTiled would look like the following:
var entityTypeDefinition = new CustomEnumDefinition
{
Name = "EntityType",
StorageType = CustomEnumStorageType.Int,
StorageType = CustomEnumStorageType.String,
ValueAsFlags = false,
Values = [
"Bomb",
@ -149,7 +152,7 @@ var entityTypeDefinition = new CustomEnumDefinition
};
```
Similarly to custom class definitions, you can also automatically generate custom enum definitions from C# enums. This is done by using the <xref:DotTiled.CustomEnumDefinition.FromEnum``1> method, or one of its overloads. This method will generate a <xref:DotTiled.CustomEnumDefinition> from a given C# enum, and you can then use this definition when loading your maps.
Similarly to custom class definitions, you can also automatically generate custom enum definitions from C# enums. This is done by using the <xref:DotTiled.CustomEnumDefinition.FromEnum``1(DotTiled.CustomEnumStorageType)> method, or one of its overloads. This method will generate a <xref:DotTiled.CustomEnumDefinition> from a given C# enum, and you can then use this definition when loading your maps.
```csharp
enum EntityType
@ -171,6 +174,12 @@ The generated custom enum definition will be identical to the one defined manual
For enum definitions, the <xref:System.FlagsAttribute> can be used to indicate that the enum should be treated as a flags enum. This will make it so the enum definition will have `ValueAsFlags = true` and the enum values will be treated as flags when working with them in DotTiled.
> [!NOTE]
> Tiled supports enums which can store their values as either strings or integers, and depending on the storage type you have specified in Tiled, you must make sure to have the same storage type in your <xref:DotTiled.CustomEnumDefinition>. This can be done by setting the `StorageType` property to either `CustomEnumStorageType.String` or `CustomEnumStorageType.Int` when creating the definition, or by passing the storage type as an argument to the <xref:DotTiled.CustomEnumDefinition.FromEnum``1(DotTiled.CustomEnumStorageType)> method. To be consistent with Tiled, <xref:DotTiled.CustomEnumDefinition.FromEnum``1(DotTiled.CustomEnumStorageType)> will default to `CustomEnumStorageType.String` for the storage type parameter.
> [!WARNING]
> If you have a custom enum type in Tiled, but do not define it in DotTiled, you must be aware that the type of the parsed property will be either <xref:DotTiled.StringProperty> or <xref:IntProperty>. It is not possible to determine the correct way to parse the enum property without the custom enum definition, which is why you will instead be given a property of type `string` or `int` when accessing the property in DotTiled. This can lead to inconsistencies between the map in Tiled and the loaded map with DotTiled. It is therefore recommended to define your custom enum types in DotTiled if you want to access the properties as <xref:EnumProperty> instances.
## Mapping properties to C# classes or enums
So far, we have only discussed how to define custom property types in DotTiled, and why they are needed. However, the most important part is how you can map properties inside your maps to their corresponding C# classes or enums.
@ -200,6 +209,9 @@ var entityDataDef = CustomClassDefinition.FromClass<EntityData>();
The above gives us two custom type definitions that we can supply to our map loader. Given a map that looks like this:
> [!WARNING]
> For classes that you call `FromClass` on, which also contain enum properties (at some level of depth) that you want to map to a C# enum, you must also supply the custom enum definitions to the map loader. This is so that the map loader can resolve the enum values correctly.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="5" height="5" tilewidth="32" tileheight="32" infinite="0" nextlayerid="8" nextobjectid="7">

View file

@ -14,4 +14,4 @@ The representation model is designed to be compatible with the latest version of
| Tiled version | Compatible DotTiled version(s) |
|---------------|--------------------------------|
| 1.11 | 0.1.0, 0.2.0 |
| 1.11 | 0.1.0, 0.2.0, 0.2.1, 0.3.0 |

View file

@ -15,10 +15,10 @@ namespace DotTiled.Benchmark
[HideColumns(["StdDev", "Error", "RatioSD"])]
public class MapLoading
{
private readonly string _tmxPath = @"DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmx";
private readonly string _tmxPath = @"DotTiled.Tests/TestData/Maps/default-map/default-map.tmx";
private readonly string _tmxContents = "";
private readonly string _tmjPath = @"DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmj";
private readonly string _tmjPath = @"DotTiled.Tests/TestData/Maps/default-map/default-map.tmj";
private readonly string _tmjContents = "";
public MapLoading()

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\DotTiled\DotTiled.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="tilemap.tmx" />
<EmbeddedResource Include="tileset.png" />
<EmbeddedResource Include="tileset.tsx" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,84 @@
using System.Reflection;
using DotTiled.Serialization;
namespace DotTiled.Example;
public class Program
{
private static void Main(string[] args)
{
Quick(args[0]);
Manual();
}
// QUICK START
// Automatic and easy way to load tilemaps.
private static void Quick(string basePath)
{
var tilemapPath = Path.Combine(basePath, "tilemap.tmx");
var loader = Loader.Default();
var map = loader.LoadMap(tilemapPath);
// You can do stuff with it like...
Console.WriteLine($"Tile width and height: {map.TileWidth}x{map.TileHeight}");
TileLayer layer0 = (TileLayer)map.Layers[0]; // Get a layer
Console.WriteLine($"Tile in layer 0 at 0, 0: {layer0.Data.Value.GlobalTileIDs.Value[0]}");
}
// MANUAL
// Manually load a map, if you need to load from a custom source
private static void Manual()
{
using Stream? tilemapStream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"DotTiled.Example.Console.tilemap.tmx")
?? throw new FileLoadException($"DotTiled.Example.Console.tilemap.tmx not found in assembly.");
string tileMapString = new StreamReader(tilemapStream).ReadToEnd();
using var mapReader = new MapReader(tileMapString, ResolveTileset, ResolveTemplate, ResolveCustomType);
var map = mapReader.ReadMap();
// Now do some other stuff with it...
StringProperty hello = map.GetProperty<StringProperty>("hello");
Console.WriteLine($"Layer 1 name: {map.Layers[0].Name}");
Console.WriteLine($"Property 'hello': {hello.Value}");
// Now with tileset
Tileset tileset = map.Tilesets[0];
Console.WriteLine($"Tileset 0 source: {tileset.Source.Value}");
Console.WriteLine($"Tileset 0 image 0 source: {tileset.Image.Value.Source.Value}");
}
// This function is responsible for loading all tilesets required by a tilemap, if you
// want to use a custom source.
private static Tileset ResolveTileset(string source)
{
// Read a file from assembly
// You can use any other source for files, eg. compressed archive, or even file from internet.
using Stream? tilesetStream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"DotTiled.Example.Console.{source}")
?? throw new FileLoadException($"{source} not found in assembly.");
string tilesetString = new StreamReader(tilesetStream).ReadToEnd();
using TilesetReader tilesetReader = new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType); // Parse loaded tileset.
return tilesetReader.ReadTileset(); // Return loaded tileset
}
// This is pretty similar to above, but instead it loads templates, not tilesets.
private static Template ResolveTemplate(string source)
{
using Stream? templateStream = Assembly.GetExecutingAssembly().GetManifestResourceStream($"DotTiled.Example.Console.{source}")
?? throw new FileLoadException($"{source} not found in assembly.");
string templateString = new StreamReader(templateStream).ReadToEnd();
using TemplateReader templateReader = new TemplateReader(templateString, ResolveTileset, ResolveTemplate, ResolveCustomType);
return templateReader.ReadTemplate();
}
private static Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
ICustomTypeDefinition[] allDefinedTypes =
[
new CustomClassDefinition() { Name = "a" },
];
return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional<ICustomTypeDefinition>(ctd) : Optional<ICustomTypeDefinition>.Empty;
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="10" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="1">
<properties>
<property name="hello" value="Hello from DotTiled!"/>
</properties>
<tileset firstgid="1" source="tileset.tsx"/>
<layer id="1" name="Tile Layer 1" width="10" height="10">
<data encoding="base64">
AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAA==
</data>
</layer>
</map>

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.8" tiledversion="1.10.2" name="tileset" tilewidth="16" tileheight="16" tilecount="1" columns="1">
<image source="tileset.png" width="16" height="16"/>
</tileset>

View file

@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

View file

@ -0,0 +1,2 @@
# Godot 4+ specific ignores
.godot/

View file

@ -0,0 +1,11 @@
<Project Sdk="Godot.NET.Sdk/4.3.0">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'android' ">net7.0</TargetFramework>
<TargetFramework Condition=" '$(GodotTargetPlatform)' == 'ios' ">net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\DotTiled\DotTiled.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,19 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled.Example.Godot", "DotTiled.Example.Godot.csproj", "{61468FCF-ACC1-4E3B-B4B4-270279E45BF5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{61468FCF-ACC1-4E3B-B4B4-270279E45BF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61468FCF-ACC1-4E3B-B4B4-270279E45BF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61468FCF-ACC1-4E3B-B4B4-270279E45BF5}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{61468FCF-ACC1-4E3B-B4B4-270279E45BF5}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{61468FCF-ACC1-4E3B-B4B4-270279E45BF5}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{61468FCF-ACC1-4E3B-B4B4-270279E45BF5}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,67 @@
using System.Globalization;
using System.Linq;
using DotTiled.Serialization;
using Godot;
namespace DotTiled.Example.Godot;
public partial class MapParser : Node2D
{
public override void _Ready()
{
// Load map
var mapString = FileAccess.Open("res://tilemap.tmx", FileAccess.ModeFlags.Read).GetAsText(); //Get file from Godot filesystem
using var mapReader = new MapReader(mapString, ResolveTileset, ResolveTemplate, ResolveCustomType);
var map = mapReader.ReadMap();
TileLayer layer0 = (TileLayer)map.Layers[0];
for (int y = 0; y < layer0.Height; y++)
{
for (int x = 0; x < layer0.Width; x++)
{
uint tile = layer0.Data.Value.GlobalTileIDs.Value[(y * layer0.Width) + x];
if (tile == 0) continue; // If block is 0, i.e. air, then continue
// Load actual block from Godot resources
Node2D block = (Node2D)GD.Load<PackedScene>($"res://blocks/{tile}.tscn").Instantiate();
// Calculate where block should be
Vector2I scale = (Vector2I)block.GetNode<Sprite2D>(tile.ToString(CultureInfo.CurrentCulture)).Scale;
int blockX = (block.GetNode<Sprite2D>(tile.ToString(CultureInfo.CurrentCulture)).Texture.GetWidth() * scale.X / 2) +
(x * block.GetNode<Sprite2D>(tile.ToString(CultureInfo.CurrentCulture)).Texture.GetWidth() * scale.X);
int blockY = (block.GetNode<Sprite2D>(tile.ToString(CultureInfo.CurrentCulture)).Texture.GetHeight() * scale.Y / 2) +
(y * block.GetNode<Sprite2D>(tile.ToString(CultureInfo.CurrentCulture)).Texture.GetHeight() * scale.Y);
block.Position = new Vector2(blockX, blockY);
// Add block to current scene
AddChild(block);
GD.Print($"{blockX}, {blockY}: {tile}");
}
}
}
private Tileset ResolveTileset(string source)
{
string tilesetString = FileAccess.Open($"res://{source}", FileAccess.ModeFlags.Read).GetAsText();
using TilesetReader tilesetReader =
new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset();
}
private Template ResolveTemplate(string source)
{
string templateString = FileAccess.Open($"res://{source}", FileAccess.ModeFlags.Read).GetAsText();
using TemplateReader templateReader =
new TemplateReader(templateString, ResolveTileset, ResolveTemplate, ResolveCustomType);
return templateReader.ReadTemplate();
}
private static Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
ICustomTypeDefinition[] allDefinedTypes =
[
new CustomClassDefinition() { Name = "a" },
];
return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional<ICustomTypeDefinition>(ctd) : Optional<ICustomTypeDefinition>.Empty;
}
}

View file

@ -0,0 +1,9 @@
[gd_scene load_steps=2 format=3 uid="uid://ce10iald4cb3f"]
[ext_resource type="Texture2D" uid="uid://da08vay832u8c" path="res://tileset.png" id="1_c5fs4"]
[node name="1" type="Node2D"]
[node name="1" type="Sprite2D" parent="."]
scale = Vector2(2, 2)
texture = ExtResource("1_c5fs4")

View file

@ -0,0 +1 @@
<svg height="128" width="128" xmlns="http://www.w3.org/2000/svg"><rect x="2" y="2" width="124" height="124" rx="14" fill="#363d52" stroke="#212532" stroke-width="4"/><g transform="scale(.101) translate(122 122)"><g fill="#fff"><path d="M105 673v33q407 354 814 0v-33z"/><path d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z" fill="#478cbf"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></g></svg>

After

Width:  |  Height:  |  Size: 949 B

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://0kywmrvvqqyr"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://p4rpwsvyslew"]
[ext_resource type="Script" path="res://MapParser.cs" id="1_xjmxv"]
[node name="Node2D" type="Node2D"]
script = ExtResource("1_xjmxv")

View file

@ -0,0 +1,20 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="DotTiled.Example.Godot"
run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.3", "C#", "Forward Plus")
config/icon="res://icon.svg"
[dotnet]
project/assembly_name="DotTiled.Example.Godot"

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="10" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="5" nextobjectid="1">
<properties>
<property name="hello" value="Hello from DotTiled!"/>
</properties>
<tileset firstgid="1" source="tileset.tsx"/>
<layer id="1" name="Tile Layer 1" width="10" height="10">
<data encoding="base64">
AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAA==
</data>
</layer>
</map>

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://da08vay832u8c"
path="res://.godot/imported/tileset.png-a39e944f25b35d62f55d4f98a36e2b5e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://tileset.png"
dest_files=["res://.godot/imported/tileset.png-a39e944f25b35d62f55d4f98a36e2b5e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.8" tiledversion="1.10.2" name="tileset" tilewidth="16" tileheight="16" tilecount="1" columns="1">
<image source="tileset.png" width="16" height="16"/>
</tileset>

View file

@ -177,4 +177,96 @@ public class FromTypeUsedInLoaderTests
// Assert
DotTiledAssert.AssertMap(expectedMap, result);
}
private enum TestEnum
{
Value1,
Value2
}
private sealed class TestClassWithEnum
{
public TestEnum Enum { get; set; } = TestEnum.Value1;
}
[Fact]
public void LoadMap_MapHasClassWithEnumAndClassIsDefined_ReturnsCorrectMap()
{
// Arrange
var resourceReader = Substitute.For<IResourceReader>();
resourceReader.Read("map.tmx").Returns(
"""
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.0" class="TestClassWithEnum" orientation="orthogonal" renderorder="right-down" width="5" height="5" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
<layer id="1" name="Tile Layer 1" width="5" height="5">
<data encoding="csv">
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0
</data>
</layer>
</map>
""");
var classDefinition = CustomClassDefinition.FromClass<TestClassWithEnum>();
var loader = Loader.DefaultWith(
resourceReader: resourceReader,
customTypeDefinitions: [classDefinition]);
var expectedMap = new Map
{
Class = "TestClassWithEnum",
Orientation = MapOrientation.Orthogonal,
Width = 5,
Height = 5,
TileWidth = 32,
TileHeight = 32,
Infinite = false,
ParallaxOriginX = 0,
ParallaxOriginY = 0,
RenderOrder = RenderOrder.RightDown,
CompressionLevel = -1,
BackgroundColor = new Color { R = 0, G = 0, B = 0, A = 0 },
Version = "1.10",
TiledVersion = "1.11.0",
NextLayerID = 2,
NextObjectID = 1,
Layers = [
new TileLayer
{
ID = 1,
Name = "Tile Layer 1",
Width = 5,
Height = 5,
Data = new Data
{
Encoding = DataEncoding.Csv,
GlobalTileIDs = new Optional<uint[]>([
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0
]),
FlippingFlags = new Optional<FlippingFlags[]>([
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None
])
}
}
],
Properties = [
new EnumProperty { Name = "Enum", PropertyType = "TestEnum", Value = new HashSet<string> { "Value1" } }
]
};
// Act
var result = loader.LoadMap("map.tmx");
// Assert
DotTiledAssert.AssertMap(expectedMap, result);
}
}

View file

@ -0,0 +1,242 @@
using System.Numerics;
namespace DotTiled.Tests;
public partial class TestData
{
public static Map MapOverrideObjectBug(string fileExt) => new Map
{
Class = "",
Orientation = MapOrientation.Orthogonal,
Width = 5,
Height = 5,
TileWidth = 32,
TileHeight = 32,
Infinite = false,
ParallaxOriginX = 0,
ParallaxOriginY = 0,
RenderOrder = RenderOrder.RightDown,
CompressionLevel = -1,
BackgroundColor = new Color { R = 0, G = 0, B = 0, A = 0 },
Version = "1.10",
TiledVersion = "1.11.0",
NextLayerID = 8,
NextObjectID = 8,
Tilesets = [
new Tileset
{
Version = "1.10",
TiledVersion = "1.11.0",
FirstGID = 1,
Name = "tileset",
TileWidth = 32,
TileHeight = 32,
TileCount = 24,
Columns = 8,
Source = $"tileset.{(fileExt == "tmx" ? "tsx" : "tsj")}",
Image = new Image
{
Format = ImageFormat.Png,
Source = "tileset.png",
Width = 256,
Height = 96,
}
}
],
Layers = [
new Group
{
ID = 2,
Name = "Root",
Layers = [
new ObjectLayer
{
ID = 3,
Name = "Objects",
Objects = [
new RectangleObject
{
ID = 1,
Name = "Object 1",
X = 25.6667f,
Y = 28.6667f,
Width = 31.3333f,
Height = 31.3333f
},
new PointObject
{
ID = 3,
Name = "P1",
X = 117.667f,
Y = 48.6667f
},
new EllipseObject
{
ID = 4,
Name = "Circle1",
X = 77f,
Y = 72.3333f,
Width = 34.6667f,
Height = 34.6667f
},
new PolygonObject
{
ID = 5,
Name = "Poly",
X = 20.6667f,
Y = 114.667f,
Points = [
new Vector2(0, 0),
new Vector2(104,20),
new Vector2(35.6667f, 32.3333f)
],
Template = fileExt == "tmx" ? "poly.tx" : "poly.tj",
Properties = [
new StringProperty { Name = "templateprop", Value = "helo there" }
]
},
new TileObject
{
ID = 6,
Name = "TileObj",
GID = 7,
X = -35,
Y = 110.333f,
Width = 64,
Height = 146
},
new RectangleObject
{
ID = 7,
Name = "",
Template = fileExt == "tmx" ? "random.tx" : "random.tj",
Type = "randomclass",
X = 134.552f,
Y = 113.638f
}
]
},
new Group
{
ID = 5,
Name = "Sub",
Layers = [
new TileLayer
{
ID = 7,
Name = "Tile 3",
Width = 5,
Height = 5,
Data = new Data
{
Encoding = DataEncoding.Csv,
GlobalTileIDs = new Optional<uint[]>([
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0
]),
FlippingFlags = new Optional<FlippingFlags[]>([
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None
])
}
},
new TileLayer
{
ID = 6,
Name = "Tile 2",
Width = 5,
Height = 5,
Data = new Data
{
Encoding = DataEncoding.Csv,
GlobalTileIDs = new Optional<uint[]>([
0, 15, 15, 0, 0,
0, 15, 15, 0, 0,
0, 15, 15, 15, 0,
15, 15, 15, 0, 0,
0, 0, 0, 0, 0
]),
FlippingFlags = new Optional<FlippingFlags[]>([
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None
])
}
}
]
},
new ImageLayer
{
ID = 4,
Name = "ImageLayer",
Image = new Image
{
Format = ImageFormat.Png,
Source = "tileset.png",
Width = fileExt == "tmx" ? 256u : 0, // Currently, json format does not
Height = fileExt == "tmx" ? 96u : 0 // include image dimensions in image layer https://github.com/mapeditor/tiled/issues/4028
},
RepeatX = true
},
new TileLayer
{
ID = 1,
Name = "Tile Layer 1",
Width = 5,
Height = 5,
Data = new Data
{
Encoding = DataEncoding.Csv,
GlobalTileIDs = new Optional<uint[]>([
1, 1, 1, 1, 1,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0
]),
FlippingFlags = new Optional<FlippingFlags[]>([
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None
])
}
}
]
}
]
};
public static IReadOnlyCollection<ICustomTypeDefinition> MapOverrideObjectBugCustomTypeDefinitions() => [
new CustomClassDefinition
{
Name = "TestClass",
UseAs = CustomClassUseAs.Map,
Members = [
new BoolProperty
{
Name = "classbool",
Value = true
},
new StringProperty
{
Name = "classstring",
Value = "Hello there default value"
}
]
},
new CustomClassDefinition
{
Name = "randomclass"
}
];
}

View file

@ -0,0 +1,171 @@
{ "compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"id":2,
"layers":[
{
"draworder":"topdown",
"id":3,
"name":"Objects",
"objects":[
{
"height":31.3333,
"id":1,
"name":"Object 1",
"rotation":0,
"type":"",
"visible":true,
"width":31.3333,
"x":25.6667,
"y":28.6667
},
{
"height":0,
"id":3,
"name":"P1",
"point":true,
"rotation":0,
"type":"",
"visible":true,
"width":0,
"x":117.667,
"y":48.6667
},
{
"ellipse":true,
"height":34.6667,
"id":4,
"name":"Circle1",
"rotation":0,
"type":"",
"visible":true,
"width":34.6667,
"x":77,
"y":72.3333
},
{
"id":5,
"template":"poly.tj",
"x":20.6667,
"y":114.667
},
{
"gid":7,
"height":146,
"id":6,
"name":"TileObj",
"rotation":0,
"type":"",
"visible":true,
"width":64,
"x":-35,
"y":110.333
},
{
"id":7,
"template":"random.tj",
"type":"randomclass",
"x":134.551764025448,
"y":113.637941006362
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
},
{
"id":5,
"layers":[
{
"data":[0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0],
"height":5,
"id":7,
"name":"Tile 3",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
},
{
"data":[0, 15, 15, 0, 0,
0, 15, 15, 0, 0,
0, 15, 15, 15, 0,
15, 15, 15, 0, 0,
0, 0, 0, 0, 0],
"height":5,
"id":6,
"name":"Tile 2",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
}],
"name":"Sub",
"opacity":1,
"type":"group",
"visible":true,
"x":0,
"y":0
},
{
"id":4,
"image":"tileset.png",
"name":"ImageLayer",
"opacity":1,
"repeatx":true,
"type":"imagelayer",
"visible":true,
"x":0,
"y":0
},
{
"data":[1, 1, 1, 1, 1,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0],
"height":5,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
}],
"name":"Root",
"opacity":1,
"type":"group",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":8,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.0",
"tileheight":32,
"tilesets":[
{
"firstgid":1,
"source":"tileset.tsj"
}],
"tilewidth":32,
"type":"map",
"version":"1.10",
"width":5
}

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="5" height="5" tilewidth="32" tileheight="32" infinite="0" nextlayerid="8" nextobjectid="8">
<tileset firstgid="1" source="tileset.tsx"/>
<group id="2" name="Root">
<objectgroup id="3" name="Objects">
<object id="1" name="Object 1" x="25.6667" y="28.6667" width="31.3333" height="31.3333"/>
<object id="3" name="P1" x="117.667" y="48.6667">
<point/>
</object>
<object id="4" name="Circle1" x="77" y="72.3333" width="34.6667" height="34.6667">
<ellipse/>
</object>
<object id="5" template="poly.tx" x="20.6667" y="114.667"/>
<object id="6" name="TileObj" gid="7" x="-35" y="110.333" width="64" height="146"/>
<object id="7" template="random.tx" type="randomclass" x="134.552" y="113.638"/>
</objectgroup>
<group id="5" name="Sub">
<layer id="7" name="Tile 3" width="5" height="5">
<data encoding="csv">
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0
</data>
</layer>
<layer id="6" name="Tile 2" width="5" height="5">
<data encoding="csv">
0,15,15,0,0,
0,15,15,0,0,
0,15,15,15,0,
15,15,15,0,0,
0,0,0,0,0
</data>
</layer>
</group>
<imagelayer id="4" name="ImageLayer" repeatx="1">
<image source="tileset.png" width="256" height="96"/>
</imagelayer>
<layer id="1" name="Tile Layer 1" width="5" height="5">
<data encoding="csv">
1,1,1,1,1,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0
</data>
</layer>
</group>
</map>

View file

@ -0,0 +1,31 @@
{ "object":
{
"height":0,
"id":5,
"name":"Poly",
"polygon":[
{
"x":0,
"y":0
},
{
"x":104,
"y":20
},
{
"x":35.6667,
"y":32.3333
}],
"properties":[
{
"name":"templateprop",
"type":"string",
"value":"helo there"
}],
"rotation":0,
"type":"",
"visible":true,
"width":0
},
"type":"template"
}

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<object name="Poly">
<properties>
<property name="templateprop" value="helo there"/>
</properties>
<polygon points="0,0 104,20 35.6667,32.3333"/>
</object>
</template>

View file

@ -0,0 +1,12 @@
{ "object":
{
"height":0,
"id":7,
"name":"",
"rotation":0,
"type":"randomclass",
"visible":true,
"width":0
},
"type":"template"
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<object type="randomclass"/>
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,14 @@
{ "columns":8,
"image":"tileset.png",
"imageheight":96,
"imagewidth":256,
"margin":0,
"name":"tileset",
"spacing":0,
"tilecount":24,
"tiledversion":"1.11.0",
"tileheight":32,
"tilewidth":32,
"type":"tileset",
"version":"1.10"
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.11.0" name="tileset" tilewidth="32" tileheight="32" tilecount="24" columns="8">
<image source="tileset.png" width="256" height="96"/>
</tileset>

View file

@ -57,7 +57,8 @@ public partial class TestData
new FloatProperty { Name = "floatprop", Value = 4.2f },
new IntProperty { Name = "intprop", Value = 8 },
new ObjectProperty { Name = "objectprop", Value = 5 },
new StringProperty { Name = "stringprop", Value = "This is a string, hello world!" }
new StringProperty { Name = "stringprop", Value = "This is a string, hello world!" },
new ColorProperty { Name = "unsetcolorprop", Value = Optional<Color>.Empty }
]
};
}

View file

@ -58,6 +58,11 @@
"name":"stringprop",
"type":"string",
"value":"This is a string, hello world!"
},
{
"name":"unsetcolorprop",
"type":"color",
"value":""
}],
"renderorder":"right-down",
"tiledversion":"1.11.0",

View file

@ -8,6 +8,7 @@
<property name="intprop" type="int" value="8"/>
<property name="objectprop" type="object" value="5"/>
<property name="stringprop" value="This is a string, hello world!"/>
<property name="unsetcolorprop" type="color" value=""/>
</properties>
<layer id="1" name="Tile Layer 1" width="5" height="5">
<data encoding="csv">

View file

@ -0,0 +1,85 @@
using System.Globalization;
namespace DotTiled.Tests;
public partial class TestData
{
public static Map MapWithCustomTypePropsWithoutDefs() => new Map
{
Class = "",
Orientation = MapOrientation.Orthogonal,
Width = 5,
Height = 5,
TileWidth = 32,
TileHeight = 32,
Infinite = false,
ParallaxOriginX = 0,
ParallaxOriginY = 0,
RenderOrder = RenderOrder.RightDown,
CompressionLevel = -1,
BackgroundColor = Color.Parse("#00000000", CultureInfo.InvariantCulture),
Version = "1.10",
TiledVersion = "1.11.0",
NextLayerID = 2,
NextObjectID = 1,
Layers = [
new TileLayer
{
ID = 1,
Name = "Tile Layer 1",
Width = 5,
Height = 5,
Data = new Data
{
Encoding = DataEncoding.Csv,
GlobalTileIDs = new Optional<uint[]>([
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0
]),
FlippingFlags = new Optional<FlippingFlags[]>([
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None
])
}
}
],
Properties = [
new ClassProperty
{
Name = "customclassprop",
PropertyType = "CustomClass",
Value = [
new BoolProperty { Name = "boolinclass", Value = true },
new FloatProperty { Name = "floatinclass", Value = 13.37f },
new StringProperty { Name = "stringinclass", Value = "This is a set string" }
]
},
new IntProperty
{
Name = "customenumintflagsprop",
Value = 6
},
new IntProperty
{
Name = "customenumintprop",
Value = 3
},
new StringProperty
{
Name = "customenumstringprop",
Value = "CustomEnumString_2"
},
new StringProperty
{
Name = "customenumstringflagsprop",
Value = "CustomEnumStringFlags_1,CustomEnumStringFlags_2"
}
]
};
}

View file

@ -0,0 +1,68 @@
{ "compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0],
"height":5,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
}],
"nextlayerid":2,
"nextobjectid":1,
"orientation":"orthogonal",
"properties":[
{
"name":"customclassprop",
"propertytype":"CustomClass",
"type":"class",
"value":
{
"boolinclass":true,
"floatinclass":13.37,
"stringinclass":"This is a set string"
}
},
{
"name":"customenumintflagsprop",
"propertytype":"CustomEnumIntFlags",
"type":"int",
"value":6
},
{
"name":"customenumintprop",
"propertytype":"CustomEnumInt",
"type":"int",
"value":3
},
{
"name":"customenumstringflagsprop",
"propertytype":"CustomEnumStringFlags",
"type":"string",
"value":"CustomEnumStringFlags_1,CustomEnumStringFlags_2"
},
{
"name":"customenumstringprop",
"propertytype":"CustomEnumString",
"type":"string",
"value":"CustomEnumString_2"
}],
"renderorder":"right-down",
"tiledversion":"1.11.0",
"tileheight":32,
"tilesets":[],
"tilewidth":32,
"type":"map",
"version":"1.10",
"width":5
}

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="5" height="5" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
<properties>
<property name="customclassprop" type="class" propertytype="CustomClass">
<properties>
<property name="boolinclass" type="bool" value="true"/>
<property name="floatinclass" type="float" value="13.37"/>
<property name="stringinclass" value="This is a set string"/>
</properties>
</property>
<property name="customenumintflagsprop" type="int" propertytype="CustomEnumIntFlags" value="6"/>
<property name="customenumintprop" type="int" propertytype="CustomEnumInt" value="3"/>
<property name="customenumstringflagsprop" propertytype="CustomEnumStringFlags" value="CustomEnumStringFlags_1,CustomEnumStringFlags_2"/>
<property name="customenumstringprop" propertytype="CustomEnumString" value="CustomEnumString_2"/>
</properties>
<layer id="1" name="Tile Layer 1" width="5" height="5">
<data encoding="csv">
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0
</data>
</layer>
</map>

View file

@ -20,8 +20,8 @@ public partial class TestData
BackgroundColor = Color.Parse("#00000000", CultureInfo.InvariantCulture),
Version = "1.10",
TiledVersion = "1.11.0",
NextLayerID = 2,
NextObjectID = 1,
NextLayerID = 3,
NextObjectID = 3,
Tilesets = [
new Tileset
{
@ -68,6 +68,33 @@ public partial class TestData
FlippingFlags.FlippedHorizontally, FlippingFlags.FlippedHorizontally, FlippingFlags.FlippedHorizontally, FlippingFlags.FlippedHorizontally, FlippingFlags.None
])
}
},
new ObjectLayer
{
ID = 2,
Name = "Object Layer 1",
Objects = [
new TileObject
{
ID = 1,
GID = 21,
X = 80.0555f,
Y = 48.3887f,
Width = 32,
Height = 32,
FlippingFlags = FlippingFlags.FlippedHorizontally
},
new TileObject
{
ID = 2,
GID = 21,
X = 15.833f,
Y = 112.056f,
Width = 32,
Height = 32,
FlippingFlags = FlippingFlags.FlippedHorizontally | FlippingFlags.FlippedVertically
}
]
}
]
};

View file

@ -17,9 +17,44 @@
"width":5,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"Object Layer 1",
"objects":[
{
"gid":2147483669,
"height":32,
"id":1,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":80.0555234239445,
"y":48.3886639676113
},
{
"gid":1073741845,
"height":32,
"id":2,
"name":"",
"rotation":0,
"type":"",
"visible":true,
"width":32,
"x":15.8334297281666,
"y":112.055523423944
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":2,
"nextobjectid":1,
"nextlayerid":3,
"nextobjectid":3,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.11.0",

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="5" height="5" tilewidth="32" tileheight="32" infinite="0" nextlayerid="2" nextobjectid="1">
<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="5" height="5" tilewidth="32" tileheight="32" infinite="0" nextlayerid="3" nextobjectid="3">
<tileset firstgid="1" source="tileset.tsx"/>
<layer id="1" name="Tile Layer 1" width="5" height="5">
<data encoding="csv">
@ -10,4 +10,8 @@
2147483669,2147483669,2147483669,2147483669,1
</data>
</layer>
<objectgroup id="2" name="Object Layer 1">
<object id="1" gid="2147483669" x="80.0555" y="48.3887" width="32" height="32"/>
<object id="2" gid="1073741845" x="15.8334" y="112.056" width="32" height="32"/>
</objectgroup>
</map>

View file

@ -0,0 +1,65 @@
using System.Globalization;
namespace DotTiled.Tests;
public partial class TestData
{
public static Map MapWithMultilineStringProp() => new Map
{
Class = "",
Orientation = MapOrientation.Isometric,
Width = 5,
Height = 5,
TileWidth = 32,
TileHeight = 16,
Infinite = false,
ParallaxOriginX = 0,
ParallaxOriginY = 0,
RenderOrder = RenderOrder.RightDown,
CompressionLevel = -1,
BackgroundColor = Color.Parse("#00ff00", CultureInfo.InvariantCulture),
Version = "1.10",
TiledVersion = "1.11.0",
NextLayerID = 2,
NextObjectID = 1,
Layers = [
new TileLayer
{
ID = 1,
Name = "Tile Layer 1",
Width = 5,
Height = 5,
Data = new Data
{
Encoding = DataEncoding.Csv,
GlobalTileIDs = new Optional<uint[]>([
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0
]),
FlippingFlags = new Optional<FlippingFlags[]>([
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None,
FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None, FlippingFlags.None
])
}
}
],
Properties =
[
new BoolProperty { Name = "boolprop", Value = true },
new ColorProperty { Name = "colorprop", Value = Color.Parse("#ff55ffff", CultureInfo.InvariantCulture) },
new FileProperty { Name = "fileprop", Value = "file.txt" },
new FloatProperty { Name = "floatprop", Value = 4.2f },
new IntProperty { Name = "intprop", Value = 8 },
new ObjectProperty { Name = "objectprop", Value = 5 },
new StringProperty { Name = "stringmultiline", Value = "hello there\n\ni am a multiline\nstring property" },
new StringProperty { Name = "stringprop", Value = "This is a string, hello world!" },
new StringProperty { Name = "unsetstringprop", Value = "" }
]
};
}

View file

@ -0,0 +1,80 @@
{ "backgroundcolor":"#00ff00",
"compressionlevel":-1,
"height":5,
"infinite":false,
"layers":[
{
"data":[0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0],
"height":5,
"id":1,
"name":"Tile Layer 1",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":5,
"x":0,
"y":0
}],
"nextlayerid":2,
"nextobjectid":1,
"orientation":"isometric",
"properties":[
{
"name":"boolprop",
"type":"bool",
"value":true
},
{
"name":"colorprop",
"type":"color",
"value":"#ff55ffff"
},
{
"name":"fileprop",
"type":"file",
"value":"file.txt"
},
{
"name":"floatprop",
"type":"float",
"value":4.2
},
{
"name":"intprop",
"type":"int",
"value":8
},
{
"name":"objectprop",
"type":"object",
"value":5
},
{
"name":"stringmultiline",
"type":"string",
"value":"hello there\n\ni am a multiline\nstring property"
},
{
"name":"stringprop",
"type":"string",
"value":"This is a string, hello world!"
},
{
"name":"unsetstringprop",
"type":"string",
"value":""
}],
"renderorder":"right-down",
"tiledversion":"1.11.0",
"tileheight":16,
"tilesets":[],
"tilewidth":32,
"type":"map",
"version":"1.10",
"width":5
}

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.11.0" orientation="isometric" renderorder="right-down" width="5" height="5" tilewidth="32" tileheight="16" infinite="0" backgroundcolor="#00ff00" nextlayerid="2" nextobjectid="1">
<properties>
<property name="boolprop" type="bool" value="true"/>
<property name="colorprop" type="color" value="#ff55ffff"/>
<property name="fileprop" type="file" value="file.txt"/>
<property name="floatprop" type="float" value="4.2"/>
<property name="intprop" type="int" value="8"/>
<property name="objectprop" type="object" value="5"/>
<property name="stringmultiline">hello there
i am a multiline
string property</property>
<property name="stringprop" value="This is a string, hello world!"/>
<property name="unsetstringprop" value=""/>
</properties>
<layer id="1" name="Tile Layer 1" width="5" height="5">
<data encoding="csv">
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0,
0,0,0,0,0
</data>
</layer>
</map>

View file

@ -70,11 +70,42 @@ public class CustomClassDefinitionTests
]
};
private enum TestEnum1
{
Value1,
Value2
}
[Flags]
private enum TestFlags1
{
Value1 = 0b001,
Value2 = 0b010,
Value3 = 0b100
}
private sealed class TestClass4WithEnums
{
public TestEnum1 Enum { get; set; } = TestEnum1.Value2;
public TestFlags1 Flags { get; set; } = TestFlags1.Value1 | TestFlags1.Value2;
}
private static CustomClassDefinition ExpectedTestClass4WithEnumsDefinition => new CustomClassDefinition
{
Name = "TestClass4WithEnums",
UseAs = CustomClassUseAs.All,
Members = [
new EnumProperty { Name = "Enum", PropertyType = "TestEnum1", Value = new HashSet<string> { "Value2" } },
new EnumProperty { Name = "Flags", PropertyType = "TestFlags1", Value = new HashSet<string> { "Value1", "Value2" } }
]
};
private static IEnumerable<(Type, CustomClassDefinition)> GetCustomClassDefinitionTestData()
{
yield return (typeof(TestClass1), ExpectedTestClass1Definition);
yield return (typeof(TestClass2WithNestedClass), ExpectedTestClass2WithNestedClassDefinition);
yield return (typeof(TestClass3WithOverridenNestedClass), ExpectedTestClass3WithOverridenNestedClassDefinition);
yield return (typeof(TestClass4WithEnums), ExpectedTestClass4WithEnumsDefinition);
}
public static IEnumerable<object[]> CustomClassDefinitionTestData =>

View file

@ -14,8 +14,10 @@ public class CustomEnumDefinitionTests
private enum TestEnum1 { Value1, Value2, Value3 }
[Fact]
public void FromEnum_Type_WhenTypeIsEnum_ReturnsCustomEnumDefinition()
[Theory]
[InlineData(CustomEnumStorageType.String)]
[InlineData(CustomEnumStorageType.Int)]
public void FromEnum_Type_WhenTypeIsEnum_ReturnsCustomEnumDefinition(CustomEnumStorageType storageType)
{
// Arrange
var type = typeof(TestEnum1);
@ -23,13 +25,13 @@ public class CustomEnumDefinitionTests
{
ID = 0,
Name = "TestEnum1",
StorageType = CustomEnumStorageType.Int,
StorageType = storageType,
Values = ["Value1", "Value2", "Value3"],
ValueAsFlags = false
};
// Act
var result = CustomEnumDefinition.FromEnum(type);
var result = CustomEnumDefinition.FromEnum(type, storageType);
// Assert
DotTiledAssert.AssertCustomEnumDefinitionEqual(expected, result);
@ -38,8 +40,10 @@ public class CustomEnumDefinitionTests
[Flags]
private enum TestEnum2 { Value1, Value2, Value3 }
[Fact]
public void FromEnum_Type_WhenEnumIsFlags_ReturnsCustomEnumDefinition()
[Theory]
[InlineData(CustomEnumStorageType.String)]
[InlineData(CustomEnumStorageType.Int)]
public void FromEnum_Type_WhenEnumIsFlags_ReturnsCustomEnumDefinition(CustomEnumStorageType storageType)
{
// Arrange
var type = typeof(TestEnum2);
@ -47,53 +51,57 @@ public class CustomEnumDefinitionTests
{
ID = 0,
Name = "TestEnum2",
StorageType = CustomEnumStorageType.Int,
StorageType = storageType,
Values = ["Value1", "Value2", "Value3"],
ValueAsFlags = true
};
// Act
var result = CustomEnumDefinition.FromEnum(type);
var result = CustomEnumDefinition.FromEnum(type, storageType);
// Assert
DotTiledAssert.AssertCustomEnumDefinitionEqual(expected, result);
}
[Fact]
public void FromEnum_T_WhenTypeIsEnum_ReturnsCustomEnumDefinition()
[Theory]
[InlineData(CustomEnumStorageType.String)]
[InlineData(CustomEnumStorageType.Int)]
public void FromEnum_T_WhenTypeIsEnum_ReturnsCustomEnumDefinition(CustomEnumStorageType storageType)
{
// Arrange
var expected = new CustomEnumDefinition
{
ID = 0,
Name = "TestEnum1",
StorageType = CustomEnumStorageType.Int,
StorageType = storageType,
Values = ["Value1", "Value2", "Value3"],
ValueAsFlags = false
};
// Act
var result = CustomEnumDefinition.FromEnum<TestEnum1>();
var result = CustomEnumDefinition.FromEnum<TestEnum1>(storageType);
// Assert
DotTiledAssert.AssertCustomEnumDefinitionEqual(expected, result);
}
[Fact]
public void FromEnum_T_WhenEnumIsFlags_ReturnsCustomEnumDefinition()
[Theory]
[InlineData(CustomEnumStorageType.String)]
[InlineData(CustomEnumStorageType.Int)]
public void FromEnum_T_WhenEnumIsFlags_ReturnsCustomEnumDefinition(CustomEnumStorageType storageType)
{
// Arrange
var expected = new CustomEnumDefinition
{
ID = 0,
Name = "TestEnum2",
StorageType = CustomEnumStorageType.Int,
StorageType = storageType,
Values = ["Value1", "Value2", "Value3"],
ValueAsFlags = true
};
// Act
var result = CustomEnumDefinition.FromEnum<TestEnum2>();
var result = CustomEnumDefinition.FromEnum<TestEnum2>(storageType);
// Assert
DotTiledAssert.AssertCustomEnumDefinitionEqual(expected, result);

View file

@ -246,7 +246,7 @@ public class LoaderTests
}
[Fact]
public void LoadMap_MapHasClassAndLoaderHasNoCustomTypes_ThrowsException()
public void LoadMap_MapHasClassAndLoaderHasNoCustomTypes_ReturnsMapWithEmptyProperties()
{
// Arrange
var resourceReader = Substitute.For<IResourceReader>();
@ -270,8 +270,11 @@ public class LoaderTests
var customTypeDefinitions = Enumerable.Empty<ICustomTypeDefinition>();
var loader = new Loader(resourceReader, resourceCache, customTypeDefinitions);
// Act & Assert
Assert.Throws<KeyNotFoundException>(() => loader.LoadMap("map.tmx"));
// Act
var result = loader.LoadMap("map.tmx");
// Assert
DotTiledAssert.AssertProperties([], result.Properties);
}
[Fact]

View file

@ -32,9 +32,14 @@ public partial class MapReaderTests
using var tilesetReader = new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset();
}
ICustomTypeDefinition ResolveCustomType(string name)
Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!;
if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd)
{
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
}
using var mapReader = new MapReader(mapString, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -36,14 +36,17 @@ public static partial class TestData
[GetMapPath("default-map"), (string f) => DefaultMap(), Array.Empty<ICustomTypeDefinition>()],
[GetMapPath("map-with-common-props"), (string f) => MapWithCommonProps(), Array.Empty<ICustomTypeDefinition>()],
[GetMapPath("map-with-custom-type-props"), (string f) => MapWithCustomTypeProps(), MapWithCustomTypePropsCustomTypeDefinitions()],
[GetMapPath("map-with-custom-type-props-without-defs"), (string f) => MapWithCustomTypePropsWithoutDefs(), Array.Empty<ICustomTypeDefinition>()],
[GetMapPath("map-with-embedded-tileset"), (string f) => MapWithEmbeddedTileset(), Array.Empty<ICustomTypeDefinition>()],
[GetMapPath("map-with-external-tileset"), (string f) => MapWithExternalTileset(f), Array.Empty<ICustomTypeDefinition>()],
[GetMapPath("map-with-flippingflags"), (string f) => MapWithFlippingFlags(f), Array.Empty<ICustomTypeDefinition>()],
[GetMapPath("map-external-tileset-multi"), (string f) => MapExternalTilesetMulti(f), Array.Empty<ICustomTypeDefinition>()],
[GetMapPath("map-external-tileset-wangset"), (string f) => MapExternalTilesetWangset(f), Array.Empty<ICustomTypeDefinition>()],
[GetMapPath("map-with-many-layers"), (string f) => MapWithManyLayers(f), Array.Empty<ICustomTypeDefinition>()],
[GetMapPath("map-with-multiline-string-prop"), (string f) => MapWithMultilineStringProp(), Array.Empty<ICustomTypeDefinition>()],
[GetMapPath("map-with-deep-props"), (string f) => MapWithDeepProps(), MapWithDeepPropsCustomTypeDefinitions()],
[GetMapPath("map-with-class"), (string f) => MapWithClass(), MapWithClassCustomTypeDefinitions()],
[GetMapPath("map-with-class-and-props"), (string f) => MapWithClassAndProps(), MapWithClassAndPropsCustomTypeDefinitions()],
[GetMapPath("map-override-object-bug"), (string f) => MapOverrideObjectBug(f), MapOverrideObjectBugCustomTypeDefinitions()],
];
}

View file

@ -28,9 +28,14 @@ public partial class TmjMapReaderTests
using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset();
}
ICustomTypeDefinition ResolveCustomType(string name)
Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!;
if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd)
{
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
}
using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -28,9 +28,14 @@ public partial class TmxMapReaderTests
using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset();
}
ICustomTypeDefinition ResolveCustomType(string name)
Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!;
if (customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd)
{
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
}
using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -9,6 +9,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled.Tests", "DotTiled.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled.Benchmark", "DotTiled.Benchmark\DotTiled.Benchmark.csproj", "{510F3077-8EA4-47D1-8D01-E2D538F1B899}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{8C54542E-3C2C-486C-9BEF-4C510391AFDA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled.Example.Console", "DotTiled.Examples\DotTiled.Example.Console\DotTiled.Example.Console.csproj", "{F9892295-6C2C-4ABD-9D6F-2AC81D2C6E67}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotTiled.Example.Godot", "DotTiled.Examples\DotTiled.Example.Godot\DotTiled.Example.Godot.csproj", "{7541A9B3-43A5-45A7-939E-6F542319D990}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -30,5 +36,17 @@ Global
{510F3077-8EA4-47D1-8D01-E2D538F1B899}.Debug|Any CPU.Build.0 = Debug|Any CPU
{510F3077-8EA4-47D1-8D01-E2D538F1B899}.Release|Any CPU.ActiveCfg = Release|Any CPU
{510F3077-8EA4-47D1-8D01-E2D538F1B899}.Release|Any CPU.Build.0 = Release|Any CPU
{F9892295-6C2C-4ABD-9D6F-2AC81D2C6E67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F9892295-6C2C-4ABD-9D6F-2AC81D2C6E67}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F9892295-6C2C-4ABD-9D6F-2AC81D2C6E67}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F9892295-6C2C-4ABD-9D6F-2AC81D2C6E67}.Release|Any CPU.Build.0 = Release|Any CPU
{7541A9B3-43A5-45A7-939E-6F542319D990}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7541A9B3-43A5-45A7-939E-6F542319D990}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7541A9B3-43A5-45A7-939E-6F542319D990}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{7541A9B3-43A5-45A7-939E-6F542319D990}.Release|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F9892295-6C2C-4ABD-9D6F-2AC81D2C6E67} = {8C54542E-3C2C-486C-9BEF-4C510391AFDA}
{7541A9B3-43A5-45A7-939E-6F542319D990} = {8C54542E-3C2C-486C-9BEF-4C510391AFDA}
EndGlobalSection
EndGlobal

View file

@ -18,7 +18,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Copyright>Copyright © 2024 dcronqvist</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<Version>0.2.0</Version>
<Version>0.3.0</Version>
</PropertyGroup>
<ItemGroup>

View file

@ -9,4 +9,9 @@ public class TileObject : Object
/// A reference to a tile.
/// </summary>
public uint GID { get; set; }
/// <summary>
/// The flipping flags for the tile.
/// </summary>
public FlippingFlags FlippingFlags { get; set; }
}

View file

@ -3,7 +3,7 @@ namespace DotTiled;
/// <summary>
/// Represents a color property.
/// </summary>
public class ColorProperty : IProperty<Color>
public class ColorProperty : IProperty<Optional<Color>>
{
/// <inheritdoc/>
public required string Name { get; set; }
@ -14,7 +14,7 @@ public class ColorProperty : IProperty<Color>
/// <summary>
/// The color value of the property.
/// </summary>
public required Color Value { get; set; }
public required Optional<Color> Value { get; set; }
/// <inheritdoc/>
public IProperty Clone() => new ColorProperty

View file

@ -165,6 +165,17 @@ public class CustomClassDefinition : HasPropertiesBase, ICustomTypeDefinition
return new IntProperty { Name = propertyInfo.Name, Value = (int)propertyInfo.GetValue(instance) };
case Type t when t.IsClass:
return new ClassProperty { Name = propertyInfo.Name, PropertyType = t.Name, Value = GetNestedProperties(propertyInfo.PropertyType, propertyInfo.GetValue(instance)) };
case Type t when t.IsEnum:
var enumDefinition = CustomEnumDefinition.FromEnum(t);
if (!enumDefinition.ValueAsFlags)
return new EnumProperty { Name = propertyInfo.Name, PropertyType = t.Name, Value = new HashSet<string> { propertyInfo.GetValue(instance).ToString() } };
var flags = (Enum)propertyInfo.GetValue(instance);
var enumValues = Enum.GetValues(t).Cast<Enum>();
var enumNames = enumValues.Where(flags.HasFlag).Select(e => e.ToString());
return new EnumProperty { Name = propertyInfo.Name, PropertyType = t.Name, Value = enumNames.ToHashSet() };
default:
break;
}

View file

@ -51,8 +51,9 @@ public class CustomEnumDefinition : ICustomTypeDefinition
/// Creates a custom enum definition from the specified enum type.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="storageType">The storage type of the custom enum. Defaults to <see cref="CustomEnumStorageType.String"/> to be consistent with Tiled.</param>
/// <returns></returns>
public static CustomEnumDefinition FromEnum<T>() where T : Enum
public static CustomEnumDefinition FromEnum<T>(CustomEnumStorageType storageType = CustomEnumStorageType.String) where T : Enum
{
var type = typeof(T);
var isFlags = type.GetCustomAttributes(typeof(FlagsAttribute), false).Length != 0;
@ -60,7 +61,7 @@ public class CustomEnumDefinition : ICustomTypeDefinition
return new CustomEnumDefinition
{
Name = type.Name,
StorageType = CustomEnumStorageType.Int,
StorageType = storageType,
Values = Enum.GetNames(type).ToList(),
ValueAsFlags = isFlags
};
@ -69,8 +70,10 @@ public class CustomEnumDefinition : ICustomTypeDefinition
/// <summary>
/// Creates a custom enum definition from the specified enum type.
/// </summary>
/// <param name="type">The enum type to create a custom enum definition from.</param>
/// <param name="storageType">The storage type of the custom enum. Defaults to <see cref="CustomEnumStorageType.String"/> to be consistent with Tiled.</param>
/// <returns></returns>
public static CustomEnumDefinition FromEnum(Type type)
public static CustomEnumDefinition FromEnum(Type type, CustomEnumStorageType storageType = CustomEnumStorageType.String)
{
if (!type.IsEnum)
throw new ArgumentException("Type must be an enum.", nameof(type));
@ -80,7 +83,7 @@ public class CustomEnumDefinition : ICustomTypeDefinition
return new CustomEnumDefinition
{
Name = type.Name,
StorageType = CustomEnumStorageType.Int,
StorageType = storageType,
Values = Enum.GetNames(type).ToList(),
ValueAsFlags = isFlags
};

View file

@ -105,7 +105,9 @@ public abstract class HasPropertiesBase : IHasProperties
type.GetProperty(prop.Name)?.SetValue(instance, boolProp.Value);
break;
case ColorProperty colorProp:
type.GetProperty(prop.Name)?.SetValue(instance, colorProp.Value);
if (!colorProp.Value.HasValue)
break;
type.GetProperty(prop.Name)?.SetValue(instance, colorProp.Value.Value);
break;
case FloatProperty floatProp:
type.GetProperty(prop.Name)?.SetValue(instance, floatProp.Value);

View file

@ -1,6 +1,6 @@
# 📚 DotTiled
DotTiled is a simple and easy-to-use library for loading, saving, and managing [Tiled maps and tilesets](https://mapeditor.org) in your .NET projects. After [TiledCS](https://github.com/TheBoneJarmer/TiledCS) unfortunately became unmaintained (since 2022), I aimed to create a new library that could fill its shoes. DotTiled is the result of that effort.
DotTiled is a simple and easy-to-use library for loading [Tiled maps and tilesets](https://mapeditor.org) in your .NET projects. After [TiledCS](https://github.com/TheBoneJarmer/TiledCS) unfortunately became unmaintained (since 2022), I aimed to create a new library that could fill its shoes. DotTiled is the result of that effort.
DotTiled is designed to be a lightweight and efficient library that provides a simple API for loading and managing Tiled maps and tilesets. It is built with performance in mind and aims to be as fast and memory-efficient as possible.
@ -15,15 +15,15 @@ Other similar libraries exist, and you may want to consider them for your projec
|**Comparison**|**DotTiled**|[TiledLib](https://github.com/Ragath/TiledLib.Net)|[TiledCSPlus](https://github.com/nolemretaWxd/TiledCSPlus)|[TiledSharp](https://github.com/marshallward/TiledSharp)|[TiledCS](https://github.com/TheBoneJarmer/TiledCS)|[TiledNet](https://github.com/napen123/Tiled.Net)|
|---------------------------------|:-----------------------:|:--------:|:-----------:|:----------:|:-------:|:------:|
| Actively maintained | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Benchmark (time)* | 1.00 | 1.83 | 2.16 | - | - | - |
| Benchmark (memory)* | 1.00 | 1.43 | 2.03 | - | - | - |
| Benchmark (time)* | 1.00 | 1.78 | 2.11 | - | - | - |
| Benchmark (memory)* | 1.00 | 1.32 | 1.88 | - | - | - |
| .NET Targets | `net8.0` | `net8.0` |`netstandard2.1`|`netstandard2.0`|`netstandard2.0`|`net45`|
| Docs |Usage, API,<br>XML Docs|Usage|Usage, API,<br>XML Docs|Usage, API|Usage, XML Docs|Usage, XML Docs|
| License | MIT | MIT | MIT | Apache-2.0 | MIT | BSD 3-Clause |
> *Both benchmark time and memory ratios are relative to DotTiled. Lower is better. Benchmark (time) refers to the execution time of loading the same map from an in-memory string that contains XML data in the `.tmx` format. Benchmark (memory) refers to the memory allocated during that loading process.
[MonoGame](https://www.monogame.net) users may also want to consider using [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended) for loading Tiled maps and tilesets. Like MonoGame.Extended, DotTiled also provides a way to properly import Tiled maps and tilesets with the MonoGame content pipeline (with the DotTiled.MonoGame.Pipeline NuGet). However, unlike MonoGame.Extended, DotTiled does *not* include any kind of rendering capabilities, and it is up to you as a developer to implement any kind of rendering for your maps when using DotTiled. The feature coverage by MonoGame.Extended is less than that of DotTiled, so you may want to consider using DotTiled if you need access to more Tiled features and flexibility.
[MonoGame](https://www.monogame.net) users may also want to consider using [MonoGame.Extended](https://github.com/craftworkgames/MonoGame.Extended) for loading Tiled maps and tilesets. The feature coverage by MonoGame.Extended is less than that of DotTiled, so you may want to consider using DotTiled if you need access to more Tiled features and flexibility.
# Feature coverage comparison

View file

@ -86,13 +86,16 @@ internal static partial class Helpers
};
}
internal static List<IProperty> ResolveClassProperties(string className, Func<string, ICustomTypeDefinition> customTypeResolver)
internal static List<IProperty> ResolveClassProperties(string className, Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
if (string.IsNullOrWhiteSpace(className))
return null;
var customType = customTypeResolver(className) ?? throw new InvalidOperationException($"Could not resolve custom type '{className}'.");
if (customType is not CustomClassDefinition ccd)
if (!customType.HasValue)
return null;
if (customType.Value is not CustomClassDefinition ccd)
throw new InvalidOperationException($"Custom type '{className}' is not a class.");
return CreateInstanceOfCustomClass(ccd, customTypeResolver);
@ -100,17 +103,31 @@ internal static partial class Helpers
internal static List<IProperty> CreateInstanceOfCustomClass(
CustomClassDefinition customClassDefinition,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
return customClassDefinition.Members.Select(x =>
{
if (x is ClassProperty cp)
{
var resolvedType = customTypeResolver(cp.PropertyType);
if (!resolvedType.HasValue)
{
return new ClassProperty
{
Name = cp.Name,
PropertyType = cp.PropertyType,
Value = []
};
}
if (resolvedType.Value is not CustomClassDefinition ccd)
throw new InvalidOperationException($"Custom type '{cp.PropertyType}' is not a class.");
return new ClassProperty
{
Name = cp.Name,
PropertyType = cp.PropertyType,
Value = CreateInstanceOfCustomClass((CustomClassDefinition)customTypeResolver(cp.PropertyType), customTypeResolver)
Value = CreateInstanceOfCustomClass(ccd, customTypeResolver)
};
}

View file

@ -12,7 +12,7 @@ public class Loader
{
private readonly IResourceReader _resourceReader;
private readonly IResourceCache _resourceCache;
private readonly IDictionary<string, ICustomTypeDefinition> _customTypeDefinitions;
private readonly Dictionary<string, ICustomTypeDefinition> _customTypeDefinitions;
/// <summary>
/// Initializes a new instance of the <see cref="Loader"/> class with the given <paramref name="resourceReader"/>, <paramref name="resourceCache"/>, and <paramref name="customTypeDefinitions"/>.
@ -114,5 +114,11 @@ public class Loader
return templateReader.ReadTemplate();
});
private ICustomTypeDefinition CustomTypeResolver(string name) => _customTypeDefinitions[name];
private Optional<ICustomTypeDefinition> CustomTypeResolver(string name)
{
if (_customTypeDefinitions.TryGetValue(name, out var customTypeDefinition))
return new Optional<ICustomTypeDefinition>(customTypeDefinition);
return Optional<ICustomTypeDefinition>.Empty;
}
}

View file

@ -14,7 +14,7 @@ public class MapReader : IMapReader
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly StringReader _mapStringReader;
private readonly XmlReader _xmlReader;
@ -33,7 +33,7 @@ public class MapReader : IMapReader
string map,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));

View file

@ -14,7 +14,7 @@ public class TemplateReader : ITemplateReader
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly StringReader _templateStringReader;
private readonly XmlReader _xmlReader;
@ -33,7 +33,7 @@ public class TemplateReader : ITemplateReader
string template,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));

View file

@ -14,7 +14,7 @@ public class TilesetReader : ITilesetReader
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly StringReader _tilesetStringReader;
private readonly XmlReader _xmlReader;
@ -33,7 +33,7 @@ public class TilesetReader : ITilesetReader
string tileset,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));
_externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver));

View file

@ -19,7 +19,7 @@ public class TjTemplateReader : TmjReaderBase, ITemplateReader
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }

View file

@ -19,7 +19,7 @@ public class TmjMapReader : TmjReaderBase, IMapReader
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }

View file

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Text.Json;
@ -117,6 +118,8 @@ public abstract partial class TmjReaderBase
if (gid.HasValue)
{
var (clearedGIDs, flippingFlags) = Helpers.ReadAndClearFlippingFlagsFromGIDs([gid.Value]);
return new TileObject
{
ID = id,
@ -130,7 +133,8 @@ public abstract partial class TmjReaderBase
Visible = visible,
Template = template,
Properties = properties,
GID = gid.Value
GID = clearedGIDs.Single(),
FlippingFlags = flippingFlags.Single()
};
}

View file

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@ -35,7 +36,7 @@ public abstract partial class TmjReaderBase
PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty<int>("value") },
PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty<float>("value") },
PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty<bool>("value") },
PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable<Color>("value") },
PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable<Color>("value", s => s == "" ? default : Color.Parse(s, CultureInfo.InvariantCulture)) },
PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty<string>("value") },
PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty<uint>("value") },
PropertyType.Class => throw new JsonException("Class property must have a property type"),
@ -63,21 +64,34 @@ public abstract partial class TmjReaderBase
var propertyType = element.GetRequiredProperty<string>("propertytype");
var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is CustomClassDefinition ccd)
// If the custom class definition is not found,
// we assume an empty class definition.
if (!customTypeDef.HasValue)
{
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
var props = element.GetOptionalPropertyCustom<List<IProperty>>("value", e => ReadPropertiesInsideClass(e, ccd)).GetValueOr([]);
var mergedProps = Helpers.MergeProperties(propsInType, props);
if (!element.TryGetProperty("value", out var valueElement))
return new ClassProperty { Name = name, PropertyType = propertyType, Value = [] };
return new ClassProperty
{
Name = name,
PropertyType = propertyType,
Value = mergedProps
Value = ReadPropertiesInsideClass(valueElement, null)
};
}
throw new JsonException($"Unknown custom class '{propertyType}'.");
if (customTypeDef.Value is not CustomClassDefinition ccd)
throw new JsonException($"Custom type {propertyType} is not a class.");
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
var props = element.GetOptionalPropertyCustom<List<IProperty>>("value", e => ReadPropertiesInsideClass(e, ccd)).GetValueOr([]);
var mergedProps = Helpers.MergeProperties(propsInType, props);
return new ClassProperty
{
Name = name,
PropertyType = propertyType,
Value = mergedProps
};
}
internal List<IProperty> ReadPropertiesInsideClass(
@ -86,11 +100,63 @@ public abstract partial class TmjReaderBase
{
List<IProperty> resultingProps = [];
if (customClassDefinition is null)
{
foreach (var prop in element.EnumerateObject())
{
var name = prop.Name;
var value = prop.Value;
#pragma warning disable IDE0072 // Add missing cases
IProperty property = value.ValueKind switch
{
JsonValueKind.String => new StringProperty { Name = name, Value = value.GetString() },
JsonValueKind.Number => value.TryGetInt32(out var intValue) ? new IntProperty { Name = name, Value = intValue } : new FloatProperty { Name = name, Value = value.GetSingle() },
JsonValueKind.True => new BoolProperty { Name = name, Value = true },
JsonValueKind.False => new BoolProperty { Name = name, Value = false },
JsonValueKind.Object => new ClassProperty { Name = name, PropertyType = "", Value = ReadPropertiesInsideClass(value, null) },
_ => throw new JsonException("Invalid property type")
};
#pragma warning restore IDE0072 // Add missing cases
resultingProps.Add(property);
}
return resultingProps;
}
foreach (var prop in customClassDefinition.Members)
{
if (!element.TryGetProperty(prop.Name, out var propElement))
continue;
if (prop is ClassProperty classProp)
{
var resolvedCustomType = _customTypeResolver(classProp.PropertyType);
if (!resolvedCustomType.HasValue)
{
resultingProps.Add(new ClassProperty
{
Name = classProp.Name,
PropertyType = classProp.PropertyType,
Value = []
});
continue;
}
if (resolvedCustomType.Value is not CustomClassDefinition ccd)
throw new JsonException($"Custom type '{classProp.PropertyType}' is not a class.");
var readProps = ReadPropertiesInsideClass(propElement, ccd);
resultingProps.Add(new ClassProperty
{
Name = classProp.Name,
PropertyType = classProp.PropertyType,
Value = readProps
});
continue;
}
IProperty property = prop.Type switch
{
PropertyType.String => new StringProperty { Name = prop.Name, Value = propElement.GetValueAs<string>() },
@ -100,8 +166,8 @@ public abstract partial class TmjReaderBase
PropertyType.Color => new ColorProperty { Name = prop.Name, Value = Color.Parse(propElement.GetValueAs<string>(), CultureInfo.InvariantCulture) },
PropertyType.File => new FileProperty { Name = prop.Name, Value = propElement.GetValueAs<string>() },
PropertyType.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs<uint>() },
PropertyType.Class => new ClassProperty { Name = prop.Name, PropertyType = ((ClassProperty)prop).PropertyType, Value = ReadPropertiesInsideClass(propElement, (CustomClassDefinition)_customTypeResolver(((ClassProperty)prop).PropertyType)) },
PropertyType.Enum => ReadEnumProperty(propElement),
PropertyType.Class => throw new NotImplementedException("Class properties should be handled elsewhere"),
_ => throw new JsonException("Invalid property type")
};
@ -111,11 +177,11 @@ public abstract partial class TmjReaderBase
return resultingProps;
}
internal EnumProperty ReadEnumProperty(JsonElement element)
internal IProperty ReadEnumProperty(JsonElement element)
{
var name = element.GetRequiredProperty<string>("name");
var propertyType = element.GetRequiredProperty<string>("propertytype");
var typeInXml = element.GetOptionalPropertyParseable<PropertyType>("type", (s) => s switch
var typeInJson = element.GetOptionalPropertyParseable<PropertyType>("type", (s) => s switch
{
"string" => PropertyType.String,
"int" => PropertyType.Int,
@ -123,8 +189,21 @@ public abstract partial class TmjReaderBase
}).GetValueOr(PropertyType.String);
var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is not CustomEnumDefinition ced)
throw new JsonException($"Unknown custom enum '{propertyType}'. Enums must be defined");
if (!customTypeDef.HasValue)
{
#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
#pragma warning disable IDE0072 // Add missing cases
return typeInJson switch
{
PropertyType.String => new StringProperty { Name = name, Value = element.GetRequiredProperty<string>("value") },
PropertyType.Int => new IntProperty { Name = name, Value = element.GetRequiredProperty<int>("value") },
};
#pragma warning restore IDE0072 // Add missing cases
#pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
}
if (customTypeDef.Value is not CustomEnumDefinition ced)
throw new JsonException($"Custom type '{propertyType}' is not an enum.");
if (ced.StorageType == CustomEnumStorageType.String)
{

View file

@ -13,7 +13,7 @@ public abstract partial class TmjReaderBase : IDisposable
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
/// <summary>
/// The root element of the JSON document being read.
@ -34,7 +34,7 @@ public abstract partial class TmjReaderBase : IDisposable
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
RootElement = JsonDocument.Parse(jsonString ?? throw new ArgumentNullException(nameof(jsonString))).RootElement;
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));

View file

@ -19,7 +19,7 @@ public class TsjTilesetReader : TmjReaderBase, ITilesetReader
string jsonString,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }

View file

@ -45,7 +45,7 @@ internal static class ExtensionsXmlReader
return T.Parse(value, CultureInfo.InvariantCulture);
}
internal static Optional<T> GetOptionalAttributeParseable<T>(this XmlReader reader, string attribute, Func<string, T> parser) where T : struct
internal static Optional<T> GetOptionalAttributeParseable<T>(this XmlReader reader, string attribute, Func<string, T> parser)
{
var value = reader.GetAttribute(attribute);
if (value is null)

View file

@ -16,7 +16,7 @@ public class TmxMapReader : TmxReaderBase, IMapReader
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }

View file

@ -97,7 +97,7 @@ public abstract partial class TmxReaderBase
var height = _reader.GetOptionalAttributeParseable<float>("height").GetValueOr(heightDefault);
var rotation = _reader.GetOptionalAttributeParseable<float>("rotation").GetValueOr(rotationDefault);
var gid = _reader.GetOptionalAttributeParseable<uint>("gid").GetValueOrOptional(gidDefault);
var visible = _reader.GetOptionalAttributeParseable<bool>("visible").GetValueOr(visibleDefault);
var visible = _reader.GetOptionalAttributeParseable<uint>("visible").GetValueOr(visibleDefault ? 1u : 0u) == 1;
// Elements
DotTiled.Object foundObject = null;
@ -118,9 +118,14 @@ public abstract partial class TmxReaderBase
if (foundObject is null)
{
if (gid.HasValue)
foundObject = new TileObject { ID = id, GID = gid.Value };
{
var (clearedGIDs, flippingFlags) = Helpers.ReadAndClearFlippingFlagsFromGIDs([gid.Value]);
foundObject = new TileObject { ID = id, GID = clearedGIDs.Single(), FlippingFlags = flippingFlags.Single() };
}
else
{
foundObject = new RectangleObject { ID = id };
}
}
foundObject.ID = id;
@ -143,19 +148,20 @@ public abstract partial class TmxReaderBase
if (obj is null)
return foundObject;
obj.ID = foundObject.ID;
obj.Name = foundObject.Name;
obj.Type = foundObject.Type;
obj.X = foundObject.X;
obj.Y = foundObject.Y;
obj.Width = foundObject.Width;
obj.Height = foundObject.Height;
obj.Rotation = foundObject.Rotation;
obj.Visible = foundObject.Visible;
obj.Properties = Helpers.MergeProperties(obj.Properties, foundObject.Properties).ToList();
obj.Template = foundObject.Template;
if (obj.GetType() != foundObject.GetType())
{
obj.ID = foundObject.ID;
obj.Name = foundObject.Name;
obj.Type = foundObject.Type;
obj.X = foundObject.X;
obj.Y = foundObject.Y;
obj.Width = foundObject.Width;
obj.Height = foundObject.Height;
obj.Rotation = foundObject.Rotation;
obj.Visible = foundObject.Visible;
obj.Properties = Helpers.MergeProperties(obj.Properties, foundObject.Properties).ToList();
obj.Template = foundObject.Template;
return obj;
}
@ -226,6 +232,13 @@ public abstract partial class TmxReaderBase
return obj;
}
internal static RectangleObject OverrideObject(RectangleObject obj, RectangleObject foundObject)
{
obj.Width = foundObject.Width;
obj.Height = foundObject.Height;
return obj;
}
internal TextObject ReadTextObject()
{
// Attributes

View file

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml;
@ -32,13 +34,18 @@ public abstract partial class TmxReaderBase
return ReadPropertyWithCustomType();
}
if (type == PropertyType.String)
{
return ReadStringProperty(name);
}
IProperty property = type switch
{
PropertyType.String => new StringProperty { Name = name, Value = r.GetRequiredAttribute("value") },
PropertyType.String => throw new InvalidOperationException("String properties should be handled elsewhere."),
PropertyType.Int => new IntProperty { Name = name, Value = r.GetRequiredAttributeParseable<int>("value") },
PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable<float>("value") },
PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable<bool>("value") },
PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable<Color>("value") },
PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable<Color>("value", s => s == "" ? default : Color.Parse(s, CultureInfo.InvariantCulture)) },
PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") },
PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable<uint>("value") },
PropertyType.Class => throw new XmlException("Class property must have a property type"),
@ -49,6 +56,25 @@ public abstract partial class TmxReaderBase
});
}
internal StringProperty ReadStringProperty(string name)
{
var valueAttrib = _reader.GetOptionalAttribute("value");
if (valueAttrib.HasValue)
{
return new StringProperty { Name = name, Value = valueAttrib.Value };
}
if (!_reader.IsEmptyElement)
{
_reader.ReadStartElement("property");
var value = _reader.ReadContentAsString();
_reader.ReadEndElement();
return new StringProperty { Name = name, Value = value };
}
return new StringProperty { Name = name, Value = string.Empty };
}
internal IProperty ReadPropertyWithCustomType()
{
var isClass = _reader.GetOptionalAttribute("type") == "class";
@ -66,28 +92,38 @@ public abstract partial class TmxReaderBase
var propertyType = _reader.GetRequiredAttribute("propertytype");
var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is CustomClassDefinition ccd)
// If the custom class definition is not found,
// we assume an empty class definition.
if (!customTypeDef.HasValue)
{
if (!_reader.IsEmptyElement)
{
_reader.ReadStartElement("property");
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
var props = ReadProperties();
var mergedProps = Helpers.MergeProperties(propsInType, props);
_reader.ReadEndElement();
return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps };
}
else
{
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
return new ClassProperty { Name = name, PropertyType = propertyType, Value = propsInType };
return new ClassProperty { Name = name, PropertyType = propertyType, Value = props };
}
return new ClassProperty { Name = name, PropertyType = propertyType, Value = [] };
}
throw new XmlException($"Unkonwn custom class definition: {propertyType}");
if (customTypeDef.Value is not CustomClassDefinition ccd)
throw new XmlException($"Custom type {propertyType} is not a class.");
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
if (!_reader.IsEmptyElement)
{
_reader.ReadStartElement("property");
var props = ReadProperties();
var mergedProps = Helpers.MergeProperties(propsInType, props);
_reader.ReadEndElement();
return new ClassProperty { Name = name, PropertyType = propertyType, Value = mergedProps };
}
return new ClassProperty { Name = name, PropertyType = propertyType, Value = propsInType };
}
internal EnumProperty ReadEnumProperty()
internal IProperty ReadEnumProperty()
{
var name = _reader.GetRequiredAttribute("name");
var propertyType = _reader.GetRequiredAttribute("propertytype");
@ -96,11 +132,26 @@ public abstract partial class TmxReaderBase
"string" => PropertyType.String,
"int" => PropertyType.Int,
_ => throw new XmlException("Invalid property type")
}) ?? PropertyType.String;
}).GetValueOr(PropertyType.String);
var customTypeDef = _customTypeResolver(propertyType);
if (customTypeDef is not CustomEnumDefinition ced)
throw new XmlException($"Unknown custom enum definition: {propertyType}. Enums must be defined");
// If the custom enum definition is not found,
// we assume an empty enum definition.
if (!customTypeDef.HasValue)
{
#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
#pragma warning disable IDE0072 // Add missing cases
return typeInXml switch
{
PropertyType.String => new StringProperty { Name = name, Value = _reader.GetRequiredAttribute("value") },
PropertyType.Int => new IntProperty { Name = name, Value = _reader.GetRequiredAttributeParseable<int>("value") },
};
#pragma warning restore IDE0072 // Add missing cases
#pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
}
if (customTypeDef.Value is not CustomEnumDefinition ced)
throw new XmlException($"Custom defined type {propertyType} is not an enum.");
if (ced.StorageType == CustomEnumStorageType.String)
{
@ -144,6 +195,6 @@ public abstract partial class TmxReaderBase
}
}
throw new XmlException($"Unknown custom enum storage type: {ced.StorageType}");
throw new XmlException($"Unable to read enum property {name} with type {propertyType}");
}
}

View file

@ -51,7 +51,7 @@ public abstract partial class TmxReaderBase
"bottomright" => ObjectAlignment.BottomRight,
_ => throw new InvalidOperationException($"Unknown object alignment '{s}'")
}).GetValueOr(ObjectAlignment.Unspecified);
var renderSize = _reader.GetOptionalAttributeEnum<TileRenderSize>("rendersize", s => s switch
var renderSize = _reader.GetOptionalAttributeEnum<TileRenderSize>("tilerendersize", s => s switch
{
"tile" => TileRenderSize.Tile,
"grid" => TileRenderSize.Grid,

View file

@ -11,7 +11,7 @@ public abstract partial class TmxReaderBase : IDisposable
// External resolvers
private readonly Func<string, Tileset> _externalTilesetResolver;
private readonly Func<string, Template> _externalTemplateResolver;
private readonly Func<string, ICustomTypeDefinition> _customTypeResolver;
private readonly Func<string, Optional<ICustomTypeDefinition>> _customTypeResolver;
private readonly XmlReader _reader;
private bool disposedValue;
@ -28,7 +28,7 @@ public abstract partial class TmxReaderBase : IDisposable
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver)
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver)
{
_reader = reader ?? throw new ArgumentNullException(nameof(reader));
_externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver));

View file

@ -16,7 +16,7 @@ public class TsxTilesetReader : TmxReaderBase, ITilesetReader
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }

View file

@ -16,7 +16,7 @@ public class TxTemplateReader : TmxReaderBase, ITemplateReader
XmlReader reader,
Func<string, Tileset> externalTilesetResolver,
Func<string, Template> externalTemplateResolver,
Func<string, ICustomTypeDefinition> customTypeResolver) : base(
Func<string, Optional<ICustomTypeDefinition>> customTypeResolver) : base(
reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver)
{ }