Compare commits

...

36 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
65 changed files with 1442 additions and 129 deletions

View file

@ -9,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 |
@ -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, 0.2.1 |
| 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

@ -73,12 +73,12 @@ public class Program
return templateReader.ReadTemplate();
}
private static ICustomTypeDefinition ResolveCustomType(string name)
private static Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
ICustomTypeDefinition[] allDefinedTypes =
[
new CustomClassDefinition() { Name = "a" },
];
return allDefinedTypes.FirstOrDefault(type => type.Name == name) ?? throw new InvalidOperationException();
return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional<ICustomTypeDefinition>(ctd) : Optional<ICustomTypeDefinition>.Empty;
}
}

View file

@ -1,4 +1,3 @@
using System;
using System.Globalization;
using System.Linq;
using DotTiled.Serialization;
@ -57,12 +56,12 @@ public partial class MapParser : Node2D
return templateReader.ReadTemplate();
}
private static ICustomTypeDefinition ResolveCustomType(string name)
private static Optional<ICustomTypeDefinition> ResolveCustomType(string name)
{
ICustomTypeDefinition[] allDefinedTypes =
[
new CustomClassDefinition() { Name = "a" },
];
return allDefinedTypes.FirstOrDefault(type => type.Name == name) ?? throw new InvalidOperationException();
return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional<ICustomTypeDefinition>(ctd) : Optional<ICustomTypeDefinition>.Empty;
}
}

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
}],
"nextlayerid":2,
"nextobjectid":1,
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"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

@ -18,7 +18,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Copyright>Copyright © 2024 dcronqvist</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<Version>0.2.1</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,8 +15,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 |

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 = CreateInstanceOfCustomClass((CustomClassDefinition)customTypeResolver(cp.PropertyType), customTypeResolver)
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(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,8 +64,24 @@ 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)
{
if (!element.TryGetProperty("value", out var valueElement))
return new ClassProperty { Name = name, PropertyType = propertyType, Value = [] };
return new ClassProperty
{
Name = name,
PropertyType = propertyType,
Value = ReadPropertiesInsideClass(valueElement, null)
};
}
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);
@ -77,20 +94,69 @@ public abstract partial class TmjReaderBase
};
}
throw new JsonException($"Unknown custom class '{propertyType}'.");
}
internal List<IProperty> ReadPropertiesInsideClass(
JsonElement element,
CustomClassDefinition customClassDefinition)
{
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

@ -118,10 +118,15 @@ 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;
foundObject.Name = name;
@ -143,8 +148,6 @@ public abstract partial class TmxReaderBase
if (obj is null)
return foundObject;
if (obj.GetType() != foundObject.GetType())
{
obj.ID = foundObject.ID;
obj.Name = foundObject.Name;
obj.Type = foundObject.Type;
@ -156,6 +159,9 @@ public abstract partial class TmxReaderBase
obj.Visible = foundObject.Visible;
obj.Properties = Helpers.MergeProperties(obj.Properties, foundObject.Properties).ToList();
obj.Template = foundObject.Template;
if (obj.GetType() != foundObject.GetType())
{
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 props = ReadProperties();
_reader.ReadEndElement();
return new ClassProperty { Name = name, PropertyType = propertyType, Value = props };
}
return new ClassProperty { Name = name, PropertyType = propertyType, Value = [] };
}
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 };
}
else
{
var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver);
return new ClassProperty { Name = name, PropertyType = propertyType, Value = propsInType };
}
}
throw new XmlException($"Unkonwn custom class definition: {propertyType}");
}
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)
{ }