diff --git a/.editorconfig b/.editorconfig index 0c2a84a..71746d2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,11 +1,247 @@ root = true [*.cs] + +#### Core EditorConfig Options #### charset = utf-8 -end_of_line = lf + +# Indentation and spacing indent_size = 2 indent_style = space + +# New line preferences +end_of_line = lf insert_final_newline = true +trim_trailing_whitespace = true + +#### .NET Coding Conventions #### +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = false +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = false +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = false +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = false +dotnet_style_prefer_conditional_expression_over_return = false +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:silent + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = when_on_single_line +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = when_multiline +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = false +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = false +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = false +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +# Diagnostics +dotnet_analyzer_diagnostic.severity = warning + +dotnet_diagnostic.IDE0001.severity = none +dotnet_diagnostic.IDE0004.severity = silent +dotnet_diagnostic.IDE0005.severity = error +dotnet_diagnostic.IDE0008.severity = silent +dotnet_diagnostic.IDE0055.severity = silent +dotnet_diagnostic.IDE0160.severity = none +dotnet_diagnostic.CA1707.severity = silent +dotnet_diagnostic.CA1852.severity = none +dotnet_diagnostic.CA1805.severity = none +dotnet_diagnostic.CA1720.severity = silent +dotnet_diagnostic.CA1711.severity = silent +dotnet_diagnostic.CA1716.severity = silent [.github/**/*.yml] charset = utf-8 diff --git a/.github/workflows/master-pr.yml b/.github/workflows/master-pr.yml new file mode 100644 index 0000000..1193a75 --- /dev/null +++ b/.github/workflows/master-pr.yml @@ -0,0 +1,37 @@ +on: + pull_request: + branches: + - master + +jobs: + check-pr-version: + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@v3 + + - name: Get version from PR branch + id: pr_version + run: | + PR_VERSION=$(grep '' **/*.csproj | sed -E 's/.*(.*)<\/Version>.*/\1/') + echo "PR_VERSION=$PR_VERSION" >> $GITHUB_ENV + + - name: Checkout master branch + run: | + git fetch origin master + git checkout origin/master + + - name: Get version from master branch + id: master_version + run: | + MASTER_VERSION=$(grep '' **/*.csproj | sed -E 's/.*(.*)<\/Version>.*/\1/') + echo "MASTER_VERSION=$MASTER_VERSION" >> $GITHUB_ENV + + - name: Compare versions + run: | + if [ "$(printf '%s\n' "$PR_VERSION" "$MASTER_VERSION" | sort -V | head -n1)" = "$PR_VERSION" ] && [ "$PR_VERSION" != "$MASTER_VERSION" ]; then + echo "Version in PR is not higher than master." + exit 1 + else + echo "Version in PR is higher than master." + fi diff --git a/.github/workflows/tests.yml b/.github/workflows/pull-request.yml similarity index 66% rename from .github/workflows/tests.yml rename to .github/workflows/pull-request.yml index 3e0d43b..d744fce 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/pull-request.yml @@ -5,7 +5,7 @@ on: - dev jobs: - tests: + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -19,4 +19,7 @@ jobs: run: dotnet build --no-restore src/DotTiled.sln - name: Test run: dotnet test --no-build --verbosity normal src/DotTiled.sln - \ No newline at end of file + - name: Lint style + run: dotnet format style --verify-no-changes --verbosity diagnostic src/DotTiled.sln + - name: Lint analyzers + run: dotnet format analyzers --verify-no-changes --verbosity diagnostic src/DotTiled.sln diff --git a/.github/workflows/release-nuget.yml b/.github/workflows/release-nuget.yml new file mode 100644 index 0000000..cc90ca5 --- /dev/null +++ b/.github/workflows/release-nuget.yml @@ -0,0 +1,30 @@ +on: + release: + types: [published] + +jobs: + release-nuget: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Restore dependencies + run: dotnet restore src/DotTiled.sln + - name: Build + run: dotnet build --no-restore src/DotTiled.sln + - name: Test + run: dotnet test --no-build --verbosity normal src/DotTiled.sln + - name: Lint style + run: dotnet format style --verify-no-changes --verbosity diagnostic src/DotTiled.sln + - name: Lint analyzers + run: dotnet format analyzers --verify-no-changes --verbosity diagnostic src/DotTiled.sln + - name: Pack + run: make pack + - name: Publish to NuGet.org + run: | + dotnet nuget push ./nupkg/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push ./nupkg/*.snupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c6014ab --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Daniel Cronqvist (daniel@dcronqvist.se) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile index 66cf561..601eda2 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,25 @@ test: + dotnet build src/DotTiled.sln dotnet test src/DotTiled.sln -docs-serve: docs-build +docs-serve: docfx docs/docfx.json --serve docs-build: cp README.md docs/index.md docfx docs/docfx.json -BENCHMARK_SOURCES = DotTiled.Benchmark/Program.cs DotTiled.Benchmark/DotTiled.Benchmark.csproj -BENCHMARK_OUTPUTDIR = DotTiled.Benchmark/BenchmarkDotNet.Artifacts +lint: + dotnet format style --verify-no-changes src/DotTiled.sln + dotnet format analyzers --verify-no-changes src/DotTiled.sln + +pack: + dotnet pack src/DotTiled/DotTiled.csproj -c Release -o ./nupkg -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg + +BENCHMARK_SOURCES = src/DotTiled.Benchmark/Program.cs src/DotTiled.Benchmark/DotTiled.Benchmark.csproj +BENCHMARK_OUTPUTDIR = src/DotTiled.Benchmark/BenchmarkDotNet.Artifacts .PHONY: benchmark benchmark: $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md - + $(BENCHMARK_OUTPUTDIR)/results/MyBenchmarks.MapLoading-report-github.md: $(BENCHMARK_SOURCES) - dotnet run --project DotTiled.Benchmark/DotTiled.Benchmark.csproj -c Release -- $(BENCHMARK_OUTPUTDIR) \ No newline at end of file + dotnet run --project src/DotTiled.Benchmark/DotTiled.Benchmark.csproj -c Release -- $(BENCHMARK_OUTPUTDIR) \ No newline at end of file diff --git a/README.md b/README.md index 48772d1..8739d03 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ 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 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. Targeting `netstandard2.0` and `net8.0` allows DotTiled to be used in popular game engines like Unity and Godot, as well as in popular game development frameworks like MonoGame. +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. - [Alternative libraries and comparison + benchmarks](#alternative-libraries-and-comparison) - [Feature coverage comparison](#feature-coverage-comparison) -- [Installing DotTiled](#installing-dottiled) +- [Quick Start](#quick-start) # Alternative libraries and comparison @@ -20,7 +20,7 @@ Other similar libraries exist, and you may want to consider them for your projec | Benchmark (time)* | 1.00 | 1.83 | 2.16 | - | - | - | | Benchmark (memory)* | 1.00 | 1.43 | 2.03 | - | - | - | | .NET Targets | `net8.0` |`net6.0`
`net7.0`|`netstandard2.1`|`netstandard2.0`|`netstandard2.0`|`net45`| -| Docs |Usage,
XML Docs|Usage|Usage, API,
XML Docs|Usage, API|Usage, XML Docs|Usage, XML Docs| +| Docs |Usage, API,
XML Docs|Usage|Usage, API,
XML Docs|Usage, API|Usage, XML Docs|Usage, XML Docs| | License | MIT | MIT | MIT | Apache-2.0 | MIT | BSD 3-Clause | > [!NOTE] @@ -73,10 +73,12 @@ Below is a comparison of the feature coverage of DotTiled and other similar libr > [!NOTE] > ✅ Full support. ⚠️ Partial support, see respective library for details about supported features. ❌ No support. -# Installing DotTiled +# Quick Start DotTiled is available as a NuGet package. You can install it by using the NuGet Package Manager UI in Visual Studio, or equivalent, or using the following command for the .NET CLI: ```pwsh dotnet add package DotTiled ``` + +Then head to the detailed [documentation](https://dcronqvist.github.io/DotTiled/docs/quickstart.html) for more information on how to use DotTiled in your project. diff --git a/docs/docfx.json b/docs/docfx.json index fa6feed..4c955cb 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -9,7 +9,8 @@ ] } ], - "dest": "api" + "dest": "api", + "enumSortOrder": "declaringOrder" } ], "build": { diff --git a/docs/docs/essentials/custom-properties.md b/docs/docs/essentials/custom-properties.md new file mode 100644 index 0000000..b18ba7f --- /dev/null +++ b/docs/docs/essentials/custom-properties.md @@ -0,0 +1,161 @@ +# Custom properties + +[Tiled facilitates a very flexible way to store custom data in your maps using properties](https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-properties). Accessing these properties is a common task when working with Tiled maps in your game since it will allow you to fully utilize the strengths of Tiled, such as customizing the behavior of your game objects or setting up the initial state of your game world. + +## All classes that can contain properties + +All classes that can contain custom properties implement the interface in some way. Below is an exhaustive list of all classes that can contain custom properties: + +- + - + - + - + - +- (allows for recursive property objects) +- (used to define custom Tiled property types) +- + - + - + - + - + - + - + - +- +- +- +- + +## How to access properties + +To access the properties on one of the classes listed above, you will make use of the interface. + +In situations where you know that a property must exist, and you simply want to retrieve it, you can use the method like so: + +```csharp +var map = LoadMap(); +var propertyValue = map.GetProperty("boolPropertyInMap").Value; +``` + +If you are unsure whether a property exists, or you want to provide some kind of default behaviour if the property is not present, you can instead use the method like so: + +```csharp +var map = LoadMap(); +if (map.TryGetProperty("boolPropertyInMap", out var property)) +{ + // Do something with existing property + var propertyValue = property.Value; +} +else +{ + // Do something if property does not exist +} +``` + +For both methods, you can replace `BoolProperty` with any of the property types that Tiled supports. You can find a list of all property types and their corresponding classes in the [next section](#all-types-of-properties). + +## All types of properties + +Tiled supports a variety of property types, which are represented in the DotTiled library as classes that implement the interface. Below is a list of all property types that Tiled supports and their corresponding classes in DotTiled: + +- `bool` - +- `color` - +- `float` - +- `file` - +- `int` - +- `object` - +- `string` - + +In addition to these primitive property types, [Tiled also supports more complex property types](https://doc.mapeditor.org/en/stable/manual/custom-properties/#custom-types). These custom property types are defined in Tiled according to the linked documentation, and to work with them in DotTiled, you *must* define their equivalences as a . You must then provide a resolving function to a defined type given a custom type name, as it is defined in Tiled. + +## Custom types + +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# and then providing a custom type resolver function that will return the equivalent definition given a custom type name. + +### Class properties + +Whenever DotTiled encounters a property that is of type `class` in a Tiled file, it will use the supplied custom type resolver function to retrieve the custom type definition. It will then use that definition to know the default values of the properties of that class, and then override those defaults with the values found in the Tiled file when populating a instance. `class` properties allow you to create hierarchical structures of properties. + +For example, if you have a `class` property in Tiled that looks like this: + +![MonsterSpawner class in Tiled UI](../../images/monster-spawner-class.png) + +The equivalent definition in DotTiled would look like the following: + +```csharp +var monsterSpawnerDefinition = new CustomClassDefinition +{ + Name = "MonsterSpawner", + UseAs = CustomClassUseAs.All, // Not really validated by DotTiled + Members = [ // Make sure that the default values match the Tiled UI + new BoolProperty { Name = "enabled", Value = true }, + new IntProperty { Name = "maxSpawnAmount", Value = 10 }, + new IntProperty { Name = "minSpawnAmount", Value = 0 }, + new StringProperty { Name = "monsterNames", Value = "" } + ] +}; +``` + +### Enum properties + +Tiled also allows you to define custom property types that work as enums. Similarly to `class` properties, you must define the equivalent in DotTiled as a . You can then return the corresponding definition in the resolving function. + +For example, if you have a custom property type in Tiled that looks like this: + +![EntityType enum in Tiled UI](../../images/entity-type-enum.png) + +The equivalent definition in DotTiled would look like the following: + +```csharp +var entityTypeDefinition = new CustomEnumDefinition +{ + Name = "EntityType", + StorageType = CustomEnumStorageType.String, + ValueAsFlags = false, + Values = [ + "Bomb", + "Chest", + "Flower", + "Chair" + ] +}; +``` + +### [Future] Automatically map custom property `class` types to C# classes + +In the future, DotTiled will support automatically mapping custom property `class` types to C# classes. This will allow you to define a C# class that matches the structure of the `class` property in Tiled, and DotTiled will automatically map the properties of the `class` property to the properties of the C# class. This will make working with `class` properties much easier and more intuitive. + +The idea is to expand on the interface with a method like `GetMappedProperty(string propertyName)`, where `T` is a class that matches the structure of the `class` property in Tiled. + +This functionality would be accompanied by a way to automatically create a matching given a C# class or enum. Something like this would then be possible: + +```csharp +class MonsterSpawner +{ + public bool Enabled { get; set; } = true; + public int MaxSpawnAmount { get; set; } = 10; + public int MinSpawnAmount { get; set; } = 0; + public string MonsterNames { get; set; } = ""; +} + +enum EntityType +{ + Bomb, + Chest, + Flower, + Chair +} + +var monsterSpawnerDefinition = CustomClassDefinition.FromClass(); +var entityTypeDefinition = CustomEnumDefinition.FromEnum(); + +// ... + +var map = LoadMap(); +var monsterSpawner = map.GetMappedProperty("monsterSpawnerPropertyInMap"); +var entityType = map.GetMappedProperty("entityTypePropertyInMap"); +``` + +Finally, it might be possible to also make some kind of exporting functionality for . Given a collection of custom type definitions, DotTiled could generate a corresponding `propertytypes.json` file that you then can import into Tiled. This would make it so that you only have to define your custom property types once (in C#) and then import them into Tiled to use them in your maps. + +Depending on implementation this might become something that can inhibit native AOT compilation due to potential reflection usage. Source generators could be used to mitigate this, but it is not yet clear how this will be implemented. \ No newline at end of file diff --git a/docs/docs/essentials/loading-maps.md b/docs/docs/essentials/loading-maps.md new file mode 100644 index 0000000..b34b964 --- /dev/null +++ b/docs/docs/essentials/loading-maps.md @@ -0,0 +1,86 @@ +# Loading maps + +Loading maps with DotTiled is straightforward and easy. The class is a representation of a Tiled map, mimicking the structure of a Tiled map file. Map files can either be in the [`.tmx`/XML](https://doc.mapeditor.org/en/stable/reference/tmx-map-format/) or [`.tmj`/json](https://doc.mapeditor.org/en/stable/reference/json-map-format/) format. DotTiled supports **both** formats fully. + +> [!NOTE] +> Using the `.tmj` file format will result in not having the same amount of information as for the `.tmx` format. This is due to the fact that the `.tmj` format does not include the full information that the `.tmx` format does. This is not a problem with DotTiled, but rather a limitation of the `.tmj` format. + +## External resolution + +Tiled maps may consist of several external files, such as tilesets or object templates. In Tiled map files, they are typically referenced by their path relative to the map file. It would be annoying to have to first load all these external resources before loading a map (which is how some other similar libraries work), so loading a map with DotTiled is designed in a way that you only have to provide a function that resolves these external resources. This way, DotTiled will figure out which external resources are needed and will invoke the corresponding resolver function to load them. + +Loading a map, tileset, or template will require you to specify **three** resolver functions. We'll go through each of them below. + +### `Func` - Tileset resolver + +This function is used to resolve external tilesets by their source path. The function should return a instance given the source path of the tileset. If you just want to load tilesets from the file system, you can use something like this: + +```csharp +Tileset ResolveTileset(string source) +{ + using var tilesetFileReader = new StreamReader(source); + var tilesetString = tilesetReader.ReadToEnd(); + using var tilesetReader = new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType); + return tilesetReader.ReadTileset(); +} +``` + +But, DotTiled is designed this way so you can retrieve your external resources from anywhere, such as a database or a custom file format, by implementing your own resolver function however you like. If you have some other means of accessing resources, you can use that instead of the file system. + +```csharp +Tileset ResolveTileset(string source) +{ + var tilesetString = ContentManager.GetString($"tilesets/{source}"); + using var tilesetReader = new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType); + return tilesetReader.ReadTileset(); +} +``` + +### `Func` - Template resolver + +This function is used to resolve external object templates by their source path. The function should return a instance given the source path of the template. If you just want to load templates from the file system, you can use something very similar to the tileset resolver by replacing with . + +### `Func` - Custom type resolver + +This function is used to resolve custom types that are defined in Tiled maps. Please refer to the [custom properties](custom-properties.md) documentation for more information on custom types. The function should return a instance given the custom type's name. + +## Putting it all together + +The following classes are the readers that you will need to use to read the map, tileset, and template: , , and . + +Here is an example of how you can load a map with DotTiled: + +```csharp +string mapPath = "path/to/map.tmx"; +string mapDirectory = Path.GetDirectoryName(mapPath); + +Tileset ResolveTileset(string source) +{ + string tilesetPath = Path.Combine(mapDirectory, source); + using var tilesetFileReader = new StreamReader(tilesetPath); + var tilesetString = tilesetReader.ReadToEnd(); + using var tilesetReader = new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType); + return tilesetReader.ReadTileset(); +} + +Template ResolveTemplate(string source) +{ + string templatePath = Path.Combine(mapDirectory, source); + using var templateFileReader = new StreamReader(templatePath); + var templateString = templateReader.ReadToEnd(); + using var templateReader = new TemplateReader(templateString, ResolveTileset, ResolveTemplate, ResolveCustomType); + return templateReader.ReadTemplate(); +} + +ICustomTypeDefinition ResolveCustomType(string name) +{ + var allDefinedTypes = [ ... ]; + return allDefinedTypes.FirstOrDefault(type => type.Name == name); +} + +using var mapFileReader = new StreamReader(mapPath); +var mapString = mapFileReader.ReadToEnd(); +using var mapReader = new MapReader(mapString, ResolveTileset, ResolveTemplate, ResolveCustomType); + +var map = mapReader.ReadMap(); +``` \ No newline at end of file diff --git a/docs/docs/loading-a-map.md b/docs/docs/loading-a-map.md deleted file mode 100644 index 9a7dc64..0000000 --- a/docs/docs/loading-a-map.md +++ /dev/null @@ -1 +0,0 @@ -# Loading a map \ No newline at end of file diff --git a/docs/docs/quickstart.md b/docs/docs/quickstart.md index 1d2e852..8d067eb 100644 --- a/docs/docs/quickstart.md +++ b/docs/docs/quickstart.md @@ -1 +1,46 @@ -# Quick Start \ No newline at end of file +# Quick Start + +Install DotTiled from NuGet: + +```bash +dotnet add package DotTiled +``` + +Load a map from file system: + +```csharp +string mapPath = "path/to/map.tmx"; +string mapDirectory = Path.GetDirectoryName(mapPath); + +Tileset ResolveTileset(string source) +{ + string tilesetPath = Path.Combine(mapDirectory, source); + using var tilesetFileReader = new StreamReader(tilesetPath); + var tilesetString = tilesetReader.ReadToEnd(); + using var tilesetReader = new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType); + return tilesetReader.ReadTileset(); +} + +Template ResolveTemplate(string source) +{ + string templatePath = Path.Combine(mapDirectory, source); + using var templateFileReader = new StreamReader(templatePath); + var templateString = templateReader.ReadToEnd(); + using var templateReader = new TemplateReader(templateString, ResolveTileset, ResolveTemplate, ResolveCustomType); + return templateReader.ReadTemplate(); +} + +ICustomTypeDefinition ResolveCustomType(string name) +{ + var allDefinedTypes = [ ... ]; + return allDefinedTypes.FirstOrDefault(type => type.Name == name); +} + +using var mapFileReader = new StreamReader(mapPath); +var mapString = mapFileReader.ReadToEnd(); +using var mapReader = new MapReader(mapString, ResolveTileset, ResolveTemplate, ResolveCustomType); + +var map = mapReader.ReadMap(); +``` + +If the above looks intimidating, don't worry! DotTiled is designed to be flexible and allow you to load maps from any source, such as a database or a custom file format. The above example is just one way to load a map from a file system. Please look at [Loading Maps](essentials/loading-maps.md) for more information on how to load maps from different sources. \ No newline at end of file diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml index 13cc1f7..6a04547 100644 --- a/docs/docs/toc.yml +++ b/docs/docs/toc.yml @@ -3,4 +3,5 @@ - href: quickstart.md - name: Essentials -- href: loading-a-map.md \ No newline at end of file +- href: essentials/loading-maps.md +- href: essentials/custom-properties.md \ No newline at end of file diff --git a/docs/images/entity-type-enum.png b/docs/images/entity-type-enum.png new file mode 100644 index 0000000..459689b Binary files /dev/null and b/docs/images/entity-type-enum.png differ diff --git a/docs/images/monster-spawner-class.png b/docs/images/monster-spawner-class.png new file mode 100644 index 0000000..c3b5ef4 Binary files /dev/null and b/docs/images/monster-spawner-class.png differ diff --git a/docs/images/resolve-types.png b/docs/images/resolve-types.png new file mode 100644 index 0000000..ea66d43 Binary files /dev/null and b/docs/images/resolve-types.png differ diff --git a/src/DotTiled.Benchmark/Program.cs b/src/DotTiled.Benchmark/Program.cs index e04c6b1..40e695b 100644 --- a/src/DotTiled.Benchmark/Program.cs +++ b/src/DotTiled.Benchmark/Program.cs @@ -1,17 +1,13 @@ -using System; -using System.Collections.Immutable; using System.Runtime.CompilerServices; -using System.Security.Cryptography; using System.Text; using System.Xml; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Order; -using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; -namespace MyBenchmarks +namespace DotTiled.Benchmark { [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] [CategoriesColumn] @@ -19,11 +15,11 @@ namespace MyBenchmarks [HideColumns(["StdDev", "Error", "RatioSD"])] public class MapLoading { - private string _tmxPath = @"DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmx"; - private string _tmxContents = ""; + private readonly string _tmxPath = @"DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmx"; + private readonly string _tmxContents = ""; - private string _tmjPath = @"DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmj"; - private string _tmjContents = ""; + private readonly string _tmjPath = @"DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.tmj"; + private readonly string _tmjContents = ""; public MapLoading() { @@ -31,27 +27,27 @@ namespace MyBenchmarks var tmxPath = Path.Combine(basePath, $"../{_tmxPath}"); var tmjPath = Path.Combine(basePath, $"../{_tmjPath}"); - _tmxContents = System.IO.File.ReadAllText(tmxPath); - _tmjContents = System.IO.File.ReadAllText(tmjPath); + _tmxContents = File.ReadAllText(tmxPath); + _tmjContents = File.ReadAllText(tmjPath); } - static string WhereAmI([CallerFilePath] string callerFilePath = "") => callerFilePath; + private static string WhereAmI([CallerFilePath] string callerFilePath = "") => callerFilePath; [BenchmarkCategory("MapFromInMemoryTmxString")] [Benchmark(Baseline = true, Description = "DotTiled")] - public DotTiled.Model.Map LoadWithDotTiledFromInMemoryTmxString() + public DotTiled.Map LoadWithDotTiledFromInMemoryTmxString() { using var stringReader = new StringReader(_tmxContents); using var xmlReader = XmlReader.Create(stringReader); - using var mapReader = new DotTiled.Serialization.Tmx.TmxMapReader(xmlReader, _ => throw new Exception(), _ => throw new Exception(), []); + using var mapReader = new DotTiled.Serialization.Tmx.TmxMapReader(xmlReader, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), _ => throw new NotSupportedException()); return mapReader.ReadMap(); } [BenchmarkCategory("MapFromInMemoryTmjString")] [Benchmark(Baseline = true, Description = "DotTiled")] - public DotTiled.Model.Map LoadWithDotTiledFromInMemoryTmjString() + public DotTiled.Map LoadWithDotTiledFromInMemoryTmjString() { - using var mapReader = new DotTiled.Serialization.Tmj.TmjMapReader(_tmjContents, _ => throw new Exception(), _ => throw new Exception(), []); + using var mapReader = new DotTiled.Serialization.Tmj.TmjMapReader(_tmjContents, _ => throw new NotSupportedException(), _ => throw new NotSupportedException(), _ => throw new NotSupportedException()); return mapReader.ReadMap(); } @@ -84,11 +80,11 @@ namespace MyBenchmarks { public static void Main(string[] args) { - var config = BenchmarkDotNet.Configs.DefaultConfig.Instance + var config = DefaultConfig.Instance .WithArtifactsPath(args[0]) .WithOptions(ConfigOptions.DisableOptimizationsValidator) .AddDiagnoser(BenchmarkDotNet.Diagnosers.MemoryDiagnoser.Default); - var summary = BenchmarkRunner.Run(config); + _ = BenchmarkRunner.Run(config); } } } diff --git a/src/DotTiled.Tests/Assert/AssertData.cs b/src/DotTiled.Tests/Assert/AssertData.cs index 3c18ef9..31ffff2 100644 --- a/src/DotTiled.Tests/Assert/AssertData.cs +++ b/src/DotTiled.Tests/Assert/AssertData.cs @@ -1,5 +1,3 @@ -using DotTiled.Model.Layers; - namespace DotTiled.Tests; public static partial class DotTiledAssert diff --git a/src/DotTiled.Tests/Assert/AssertImage.cs b/src/DotTiled.Tests/Assert/AssertImage.cs index 613318b..a674faa 100644 --- a/src/DotTiled.Tests/Assert/AssertImage.cs +++ b/src/DotTiled.Tests/Assert/AssertImage.cs @@ -1,5 +1,3 @@ -using DotTiled.Model.Tilesets; - namespace DotTiled.Tests; public static partial class DotTiledAssert diff --git a/src/DotTiled.Tests/Assert/AssertLayer.cs b/src/DotTiled.Tests/Assert/AssertLayer.cs index 1cb9e36..5432d62 100644 --- a/src/DotTiled.Tests/Assert/AssertLayer.cs +++ b/src/DotTiled.Tests/Assert/AssertLayer.cs @@ -1,5 +1,3 @@ -using DotTiled.Model.Layers; - namespace DotTiled.Tests; public static partial class DotTiledAssert diff --git a/src/DotTiled.Tests/Assert/AssertMap.cs b/src/DotTiled.Tests/Assert/AssertMap.cs index 6984b79..0110f51 100644 --- a/src/DotTiled.Tests/Assert/AssertMap.cs +++ b/src/DotTiled.Tests/Assert/AssertMap.cs @@ -1,6 +1,5 @@ using System.Collections; using System.Numerics; -using DotTiled.Model; namespace DotTiled.Tests; @@ -91,7 +90,7 @@ public static partial class DotTiledAssert AssertEqual(expected.NextObjectID, actual.NextObjectID, nameof(Map.NextObjectID)); AssertEqual(expected.Infinite, actual.Infinite, nameof(Map.Infinite)); - AssertProperties(actual.Properties, expected.Properties); + AssertProperties(expected.Properties, actual.Properties); Assert.NotNull(actual.Tilesets); AssertEqual(expected.Tilesets.Count, actual.Tilesets.Count, "Tilesets.Count"); diff --git a/src/DotTiled.Tests/Assert/AssertObject.cs b/src/DotTiled.Tests/Assert/AssertObject.cs index f234ed2..2dfb2d9 100644 --- a/src/DotTiled.Tests/Assert/AssertObject.cs +++ b/src/DotTiled.Tests/Assert/AssertObject.cs @@ -1,22 +1,22 @@ -using DotTiled.Model.Layers.Objects; - namespace DotTiled.Tests; public static partial class DotTiledAssert { - internal static void AssertObject(Model.Layers.Objects.Object expected, Model.Layers.Objects.Object actual) + internal static void AssertObject(DotTiled.Object expected, DotTiled.Object actual) { // Attributes - AssertEqual(expected.ID, actual.ID, nameof(Model.Layers.Objects.Object.ID)); - AssertEqual(expected.Name, actual.Name, nameof(Model.Layers.Objects.Object.Name)); - AssertEqual(expected.Type, actual.Type, nameof(Model.Layers.Objects.Object.Type)); - AssertEqual(expected.X, actual.X, nameof(Model.Layers.Objects.Object.X)); - AssertEqual(expected.Y, actual.Y, nameof(Model.Layers.Objects.Object.Y)); - AssertEqual(expected.Width, actual.Width, nameof(Model.Layers.Objects.Object.Width)); - AssertEqual(expected.Height, actual.Height, nameof(Model.Layers.Objects.Object.Height)); - AssertEqual(expected.Rotation, actual.Rotation, nameof(Model.Layers.Objects.Object.Rotation)); - AssertEqual(expected.Visible, actual.Visible, nameof(Model.Layers.Objects.Object.Visible)); - AssertEqual(expected.Template, actual.Template, nameof(Model.Layers.Objects.Object.Template)); +#pragma warning disable IDE0002 + AssertEqual(expected.ID, actual.ID, nameof(DotTiled.Object.ID)); + AssertEqual(expected.Name, actual.Name, nameof(DotTiled.Object.Name)); + AssertEqual(expected.Type, actual.Type, nameof(DotTiled.Object.Type)); + AssertEqual(expected.X, actual.X, nameof(DotTiled.Object.X)); + AssertEqual(expected.Y, actual.Y, nameof(DotTiled.Object.Y)); + AssertEqual(expected.Width, actual.Width, nameof(DotTiled.Object.Width)); + AssertEqual(expected.Height, actual.Height, nameof(DotTiled.Object.Height)); + AssertEqual(expected.Rotation, actual.Rotation, nameof(DotTiled.Object.Rotation)); + AssertEqual(expected.Visible, actual.Visible, nameof(DotTiled.Object.Visible)); + AssertEqual(expected.Template, actual.Template, nameof(DotTiled.Object.Template)); +#pragma warning restore IDE0002 AssertProperties(expected.Properties, actual.Properties); @@ -24,30 +24,15 @@ public static partial class DotTiledAssert AssertObject((dynamic)expected, (dynamic)actual); } - private static void AssertObject(RectangleObject expected, RectangleObject actual) - { - Assert.True(true); // A rectangle object is the same as the abstract Object - } + private static void AssertObject(RectangleObject _, RectangleObject __) => Assert.True(true); // A rectangle object is the same as the abstract Object - private static void AssertObject(EllipseObject expected, EllipseObject actual) - { - Assert.True(true); // An ellipse object is the same as the abstract Object - } + private static void AssertObject(EllipseObject _, EllipseObject __) => Assert.True(true); // An ellipse object is the same as the abstract Object - private static void AssertObject(PointObject expected, PointObject actual) - { - Assert.True(true); // A point object is the same as the abstract Object - } + private static void AssertObject(PointObject _, PointObject __) => Assert.True(true); // A point object is the same as the abstract Object - private static void AssertObject(PolygonObject expected, PolygonObject actual) - { - AssertEqual(expected.Points, actual.Points, nameof(PolygonObject.Points)); - } + private static void AssertObject(PolygonObject expected, PolygonObject actual) => AssertEqual(expected.Points, actual.Points, nameof(PolygonObject.Points)); - private static void AssertObject(PolylineObject expected, PolylineObject actual) - { - AssertEqual(expected.Points, actual.Points, nameof(PolylineObject.Points)); - } + private static void AssertObject(PolylineObject expected, PolylineObject actual) => AssertEqual(expected.Points, actual.Points, nameof(PolylineObject.Points)); private static void AssertObject(TextObject expected, TextObject actual) { @@ -67,9 +52,5 @@ public static partial class DotTiledAssert AssertEqual(expected.Text, actual.Text, nameof(TextObject.Text)); } - private static void AssertObject(TileObject expected, TileObject actual) - { - // Attributes - AssertEqual(expected.GID, actual.GID, nameof(TileObject.GID)); - } + private static void AssertObject(TileObject expected, TileObject actual) => AssertEqual(expected.GID, actual.GID, nameof(TileObject.GID)); } diff --git a/src/DotTiled.Tests/Assert/AssertProperties.cs b/src/DotTiled.Tests/Assert/AssertProperties.cs index ddd0e69..843d8d0 100644 --- a/src/DotTiled.Tests/Assert/AssertProperties.cs +++ b/src/DotTiled.Tests/Assert/AssertProperties.cs @@ -1,10 +1,8 @@ -using DotTiled.Model.Properties; - namespace DotTiled.Tests; public static partial class DotTiledAssert { - internal static void AssertProperties(Dictionary? expected, Dictionary? actual) + internal static void AssertProperties(IList? expected, IList? actual) { if (expected is null) { @@ -14,58 +12,45 @@ public static partial class DotTiledAssert Assert.NotNull(actual); AssertEqual(expected.Count, actual.Count, "Properties.Count"); - foreach (var kvp in expected) + foreach (var prop in expected) { - Assert.Contains(kvp.Key, actual.Keys); - AssertProperty((dynamic)kvp.Value, (dynamic)actual[kvp.Key]); + Assert.Contains(actual, p => p.Name == prop.Name); + + var actualProp = actual.First(p => p.Name == prop.Name); + AssertEqual(prop.Type, actualProp.Type, "Property.Type"); + AssertEqual(prop.Name, actualProp.Name, "Property.Name"); + + AssertProperty((dynamic)prop, (dynamic)actualProp); } } - private static void AssertProperty(IProperty expected, IProperty actual) - { - AssertEqual(expected.Type, actual.Type, "Property.Type"); - AssertEqual(expected.Name, actual.Name, "Property.Name"); - AssertProperties((dynamic)actual, (dynamic)expected); - } + private static void AssertProperty(StringProperty expected, StringProperty actual) => AssertEqual(expected.Value, actual.Value, "StringProperty.Value"); - private static void AssertProperty(StringProperty expected, StringProperty actual) - { - AssertEqual(expected.Value, actual.Value, "StringProperty.Value"); - } + private static void AssertProperty(IntProperty expected, IntProperty actual) => AssertEqual(expected.Value, actual.Value, "IntProperty.Value"); - private static void AssertProperty(IntProperty expected, IntProperty actual) - { - AssertEqual(expected.Value, actual.Value, "IntProperty.Value"); - } + private static void AssertProperty(FloatProperty expected, FloatProperty actual) => AssertEqual(expected.Value, actual.Value, "FloatProperty.Value"); - private static void AssertProperty(FloatProperty expected, FloatProperty actual) - { - AssertEqual(expected.Value, actual.Value, "FloatProperty.Value"); - } + private static void AssertProperty(BoolProperty expected, BoolProperty actual) => AssertEqual(expected.Value, actual.Value, "BoolProperty.Value"); - private static void AssertProperty(BoolProperty expected, BoolProperty actual) - { - AssertEqual(expected.Value, actual.Value, "BoolProperty.Value"); - } + private static void AssertProperty(ColorProperty expected, ColorProperty actual) => AssertEqual(expected.Value, actual.Value, "ColorProperty.Value"); - private static void AssertProperty(ColorProperty expected, ColorProperty actual) - { - AssertEqual(expected.Value, actual.Value, "ColorProperty.Value"); - } + private static void AssertProperty(FileProperty expected, FileProperty actual) => AssertEqual(expected.Value, actual.Value, "FileProperty.Value"); - private static void AssertProperty(FileProperty expected, FileProperty actual) - { - AssertEqual(expected.Value, actual.Value, "FileProperty.Value"); - } - - private static void AssertProperty(ObjectProperty expected, ObjectProperty actual) - { - AssertEqual(expected.Value, actual.Value, "ObjectProperty.Value"); - } + private static void AssertProperty(ObjectProperty expected, ObjectProperty actual) => AssertEqual(expected.Value, actual.Value, "ObjectProperty.Value"); private static void AssertProperty(ClassProperty expected, ClassProperty actual) { AssertEqual(expected.PropertyType, actual.PropertyType, "ClassProperty.PropertyType"); - AssertProperties(expected.Properties, actual.Properties); + AssertProperties(expected.Value, actual.Value); + } + + private static void AssertProperty(EnumProperty expected, EnumProperty actual) + { + AssertEqual(expected.PropertyType, actual.PropertyType, "EnumProperty.PropertyType"); + AssertEqual(expected.Value.Count, actual.Value.Count, "EnumProperty.Value.Count"); + foreach (var value in expected.Value) + { + Assert.Contains(actual.Value, v => v == value); + } } } diff --git a/src/DotTiled.Tests/Assert/AssertTileset.cs b/src/DotTiled.Tests/Assert/AssertTileset.cs index c0e5e8e..4646a85 100644 --- a/src/DotTiled.Tests/Assert/AssertTileset.cs +++ b/src/DotTiled.Tests/Assert/AssertTileset.cs @@ -1,6 +1,3 @@ -using DotTiled.Model.Layers; -using DotTiled.Model.Tilesets; - namespace DotTiled.Tests; public static partial class DotTiledAssert @@ -142,9 +139,9 @@ public static partial class DotTiledAssert AssertEqual(expected.Height, actual.Height, nameof(Tile.Height)); // Elements - AssertProperties(actual.Properties, expected.Properties); - AssertImage(actual.Image, expected.Image); - AssertLayer((BaseLayer?)actual.ObjectLayer, (BaseLayer?)expected.ObjectLayer); + AssertProperties(expected.Properties, actual.Properties); + AssertImage(expected.Image, actual.Image); + AssertLayer((BaseLayer?)expected.ObjectLayer, (BaseLayer?)actual.ObjectLayer); if (expected.Animation is not null) { Assert.NotNull(actual.Animation); diff --git a/src/DotTiled.Tests/Serialization/MapReaderTests.cs b/src/DotTiled.Tests/Serialization/MapReaderTests.cs new file mode 100644 index 0000000..885f57e --- /dev/null +++ b/src/DotTiled.Tests/Serialization/MapReaderTests.cs @@ -0,0 +1,49 @@ +using DotTiled.Serialization; + +namespace DotTiled.Tests; + +public partial class MapReaderTests +{ + public static IEnumerable Maps => TestData.MapTests; + [Theory] + [MemberData(nameof(Maps))] + public void MapReaderReadMap_ValidFilesExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected( + string testDataFile, + Func expectedMap, + IReadOnlyCollection customTypeDefinitions) + { + // Arrange + string[] fileFormats = [".tmx", ".tmj"]; + + foreach (var fileFormat in fileFormats) + { + var testDataFileWithFormat = testDataFile + fileFormat; + var fileDir = Path.GetDirectoryName(testDataFileWithFormat); + var mapString = TestData.GetRawStringFor(testDataFileWithFormat); + Template ResolveTemplate(string source) + { + var templateString = TestData.GetRawStringFor($"{fileDir}/{source}"); + using var templateReader = new TemplateReader(templateString, ResolveTileset, ResolveTemplate, ResolveCustomType); + return templateReader.ReadTemplate(); + } + Tileset ResolveTileset(string source) + { + var tilesetString = TestData.GetRawStringFor($"{fileDir}/{source}"); + using var tilesetReader = new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType); + return tilesetReader.ReadTileset(); + } + ICustomTypeDefinition ResolveCustomType(string name) + { + return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; + } + using var mapReader = new MapReader(mapString, ResolveTileset, ResolveTemplate, ResolveCustomType); + + // Act + var map = mapReader.ReadMap(); + + // Assert + Assert.NotNull(map); + DotTiledAssert.AssertMap(expectedMap(fileFormat[1..]), map); + } + } +} diff --git a/src/DotTiled.Tests/Serialization/TestData.cs b/src/DotTiled.Tests/Serialization/TestData.cs index 467e1df..7f68c9e 100644 --- a/src/DotTiled.Tests/Serialization/TestData.cs +++ b/src/DotTiled.Tests/Serialization/TestData.cs @@ -1,6 +1,4 @@ using System.Xml; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; namespace DotTiled.Tests; @@ -33,49 +31,15 @@ public static partial class TestData public static IEnumerable MapTests => [ - ["Serialization/TestData/Map/default_map/default-map", (string f) => TestData.DefaultMap(), Array.Empty()], - ["Serialization/TestData/Map/map_with_common_props/map-with-common-props", (string f) => TestData.MapWithCommonProps(), Array.Empty()], - ["Serialization/TestData/Map/map_with_custom_type_props/map-with-custom-type-props", (string f) => TestData.MapWithCustomTypeProps(), TestData.MapWithCustomTypePropsCustomTypeDefinitions()], - ["Serialization/TestData/Map/map_with_embedded_tileset/map-with-embedded-tileset", (string f) => TestData.MapWithEmbeddedTileset(), Array.Empty()], - ["Serialization/TestData/Map/map_with_external_tileset/map-with-external-tileset", (string f) => TestData.MapWithExternalTileset(f), Array.Empty()], - ["Serialization/TestData/Map/map_with_flippingflags/map-with-flippingflags", (string f) => TestData.MapWithFlippingFlags(f), Array.Empty()], - ["Serialization/TestData/Map/map_external_tileset_multi/map-external-tileset-multi", (string f) => TestData.MapExternalTilesetMulti(f), Array.Empty()], - ["Serialization/TestData/Map/map_external_tileset_wangset/map-external-tileset-wangset", (string f) => TestData.MapExternalTilesetWangset(f), Array.Empty()], - ["Serialization/TestData/Map/map_with_many_layers/map-with-many-layers", (string f) => TestData.MapWithManyLayers(f), Array.Empty()], - ]; - - private static CustomTypeDefinition[] typedefs = [ - new CustomClassDefinition - { - Name = "TestClass", - ID = 1, - UseAs = CustomClassUseAs.Property, - Members = [ - new StringProperty - { - Name = "Name", - Value = "" - }, - new FloatProperty - { - Name = "Amount", - Value = 0f - } - ] - }, - new CustomClassDefinition - { - Name = "Test", - ID = 2, - UseAs = CustomClassUseAs.All, - Members = [ - new ClassProperty - { - Name = "Yep", - PropertyType = "TestClass", - Properties = [] - } - ] - } + ["Serialization/TestData/Map/default_map/default-map", (string f) => DefaultMap(), Array.Empty()], + ["Serialization/TestData/Map/map_with_common_props/map-with-common-props", (string f) => MapWithCommonProps(), Array.Empty()], + ["Serialization/TestData/Map/map_with_custom_type_props/map-with-custom-type-props", (string f) => MapWithCustomTypeProps(), MapWithCustomTypePropsCustomTypeDefinitions()], + ["Serialization/TestData/Map/map_with_embedded_tileset/map-with-embedded-tileset", (string f) => MapWithEmbeddedTileset(), Array.Empty()], + ["Serialization/TestData/Map/map_with_external_tileset/map-with-external-tileset", (string f) => MapWithExternalTileset(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_flippingflags/map-with-flippingflags", (string f) => MapWithFlippingFlags(f), Array.Empty()], + ["Serialization/TestData/Map/map_external_tileset_multi/map-external-tileset-multi", (string f) => MapExternalTilesetMulti(f), Array.Empty()], + ["Serialization/TestData/Map/map_external_tileset_wangset/map-external-tileset-wangset", (string f) => MapExternalTilesetWangset(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_many_layers/map-with-many-layers", (string f) => MapWithManyLayers(f), Array.Empty()], + ["Serialization/TestData/Map/map_with_deep_props/map-with-deep-props", (string f) => MapWithDeepProps(), MapWithDeepPropsCustomTypeDefinitions()], ]; } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.cs b/src/DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.cs index c445069..eff73d9 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/default-map/default-map.cs @@ -1,6 +1,3 @@ -using DotTiled.Model; -using DotTiled.Model.Layers; - namespace DotTiled.Tests; public partial class TestData diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs index e1aecda..28d6272 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-multi/map-external-tileset-multi.cs @@ -1,8 +1,4 @@ using System.Globalization; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Tilesets; namespace DotTiled.Tests; @@ -52,16 +48,15 @@ public partial class TestData Width = 1, Height = 1 }, - Properties = new Dictionary - { - ["tilesetbool"] = new BoolProperty { Name = "tilesetbool", Value = true }, - ["tilesetcolor"] = new ColorProperty { Name = "tilesetcolor", Value = Color.Parse("#ffff0000", CultureInfo.InvariantCulture) }, - ["tilesetfile"] = new FileProperty { Name = "tilesetfile", Value = "" }, - ["tilesetfloat"] = new FloatProperty { Name = "tilesetfloat", Value = 5.2f }, - ["tilesetint"] = new IntProperty { Name = "tilesetint", Value = 9 }, - ["tilesetobject"] = new ObjectProperty { Name = "tilesetobject", Value = 0 }, - ["tilesetstring"] = new StringProperty { Name = "tilesetstring", Value = "hello world!" } - }, + Properties = [ + new BoolProperty { Name = "tilesetbool", Value = true }, + new ColorProperty { Name = "tilesetcolor", Value = Color.Parse("#ffff0000", CultureInfo.InvariantCulture) }, + new FileProperty { Name = "tilesetfile", Value = "" }, + new FloatProperty { Name = "tilesetfloat", Value = 5.2f }, + new IntProperty { Name = "tilesetint", Value = 9 }, + new ObjectProperty { Name = "tilesetobject", Value = 0 }, + new StringProperty { Name = "tilesetstring", Value = "hello world!" } + ], Tiles = [ new Tile { diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.cs index 7e21c54..9aaa7d7 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-external-tileset-wangset/map-external-tileset-wangset.cs @@ -1,7 +1,4 @@ using System.Globalization; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Tilesets; namespace DotTiled.Tests; diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs index 7b4de68..fdedbf8 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-common-props/map-with-common-props.cs @@ -1,7 +1,4 @@ using System.Globalization; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; namespace DotTiled.Tests; @@ -57,15 +54,15 @@ public partial class TestData } } ], - Properties = new Dictionary - { - ["boolprop"] = new BoolProperty { Name = "boolprop", Value = true }, - ["colorprop"] = new ColorProperty { Name = "colorprop", Value = Color.Parse("#ff55ffff", CultureInfo.InvariantCulture) }, - ["fileprop"] = new FileProperty { Name = "fileprop", Value = "file.txt" }, - ["floatprop"] = new FloatProperty { Name = "floatprop", Value = 4.2f }, - ["intprop"] = new IntProperty { Name = "intprop", Value = 8 }, - ["objectprop"] = new ObjectProperty { Name = "objectprop", Value = 5 }, - ["stringprop"] = new StringProperty { Name = "stringprop", Value = "This is a string, hello world!" } - } + 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 = "stringprop", Value = "This is a string, hello world!" } + ] }; } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs index 47a6b6b..56759d4 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.cs @@ -1,8 +1,4 @@ using System.Globalization; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; namespace DotTiled.Tests; @@ -58,28 +54,50 @@ public partial class TestData } } ], - Properties = new Dictionary - { - ["customclassprop"] = new ClassProperty + Properties = [ + new ClassProperty { Name = "customclassprop", PropertyType = "CustomClass", - Properties = new Dictionary - { - ["boolinclass"] = new BoolProperty { Name = "boolinclass", Value = true }, - ["colorinclass"] = new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) }, - ["fileinclass"] = new FileProperty { Name = "fileinclass", Value = "" }, - ["floatinclass"] = new FloatProperty { Name = "floatinclass", Value = 13.37f }, - ["intinclass"] = new IntProperty { Name = "intinclass", Value = 0 }, - ["objectinclass"] = new ObjectProperty { Name = "objectinclass", Value = 0 }, - ["stringinclass"] = new StringProperty { Name = "stringinclass", Value = "This is a set string" } - } + Value = [ + new BoolProperty { Name = "boolinclass", Value = true }, + new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) }, + new FileProperty { Name = "fileinclass", Value = "" }, + new FloatProperty { Name = "floatinclass", Value = 13.37f }, + new IntProperty { Name = "intinclass", Value = 0 }, + new ObjectProperty { Name = "objectinclass", Value = 0 }, + new StringProperty { Name = "stringinclass", Value = "This is a set string" } + ] + }, + new EnumProperty + { + Name = "customenumstringprop", + PropertyType = "CustomEnumString", + Value = new HashSet { "CustomEnumString_2" } + }, + new EnumProperty + { + Name = "customenumstringflagsprop", + PropertyType = "CustomEnumStringFlags", + Value = new HashSet { "CustomEnumStringFlags_1", "CustomEnumStringFlags_2" } + }, + new EnumProperty + { + Name = "customenumintprop", + PropertyType = "CustomEnumInt", + Value = new HashSet { "CustomEnumInt_4" } + }, + new EnumProperty + { + Name = "customenumintflagsprop", + PropertyType = "CustomEnumIntFlags", + Value = new HashSet { "CustomEnumIntFlags_2", "CustomEnumIntFlags_3" } } - } + ] }; // This comes from map-with-custom-type-props/propertytypes.json - public static IReadOnlyCollection MapWithCustomTypePropsCustomTypeDefinitions() => [ + public static IReadOnlyCollection MapWithCustomTypePropsCustomTypeDefinitions() => [ new CustomClassDefinition { Name = "CustomClass", @@ -121,6 +139,50 @@ public partial class TestData Value = "" } ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumString", + StorageType = CustomEnumStorageType.String, + ValueAsFlags = false, + Values = [ + "CustomEnumString_1", + "CustomEnumString_2", + "CustomEnumString_3" + ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumStringFlags", + StorageType = CustomEnumStorageType.String, + ValueAsFlags = true, + Values = [ + "CustomEnumStringFlags_1", + "CustomEnumStringFlags_2" + ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumInt", + StorageType = CustomEnumStorageType.Int, + ValueAsFlags = false, + Values = [ + "CustomEnumInt_1", + "CustomEnumInt_2", + "CustomEnumInt_3", + "CustomEnumInt_4", + ] + }, + new CustomEnumDefinition + { + Name = "CustomEnumIntFlags", + StorageType = CustomEnumStorageType.Int, + ValueAsFlags = true, + Values = [ + "CustomEnumIntFlags_1", + "CustomEnumIntFlags_2", + "CustomEnumIntFlags_3" + ] } ]; } diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj index a8c7f43..74f892b 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmj @@ -32,6 +32,30 @@ "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", diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx index c364577..cadc2fa 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-custom-type-props/map-with-custom-type-props.tmx @@ -8,6 +8,10 @@ + + + + diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.cs new file mode 100644 index 0000000..90aac6c --- /dev/null +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.cs @@ -0,0 +1,160 @@ +using System.Globalization; + +namespace DotTiled.Tests; + +public partial class TestData +{ + public static Map MapWithDeepProps() => new Map + { + Class = "", + Orientation = MapOrientation.Orthogonal, + Width = 5, + Height = 5, + TileWidth = 32, + TileHeight = 32, + Infinite = false, + HexSideLength = null, + StaggerAxis = null, + StaggerIndex = null, + 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, + Chunks = null, + Compression = null, + GlobalTileIDs = [ + 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 = [ + 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 = "customouterclassprop", + PropertyType = "CustomOuterClass", + Value = [ + new ClassProperty + { + Name = "customclasspropinclass", + PropertyType = "CustomClass", + Value = [ + new BoolProperty { Name = "boolinclass", Value = false }, + new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) }, + new FileProperty { Name = "fileinclass", Value = "" }, + new FloatProperty { Name = "floatinclass", Value = 0f }, + new IntProperty { Name = "intinclass", Value = 0 }, + new ObjectProperty { Name = "objectinclass", Value = 0 }, + new StringProperty { Name = "stringinclass", Value = "" } + ] + } + ] + }, + new ClassProperty + { + Name = "customouterclasspropset", + PropertyType = "CustomOuterClass", + Value = [ + new ClassProperty + { + Name = "customclasspropinclass", + PropertyType = "CustomClass", + Value = [ + new BoolProperty { Name = "boolinclass", Value = true }, + new ColorProperty { Name = "colorinclass", Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) }, + new FileProperty { Name = "fileinclass", Value = "" }, + new FloatProperty { Name = "floatinclass", Value = 13.37f }, + new IntProperty { Name = "intinclass", Value = 0 }, + new ObjectProperty { Name = "objectinclass", Value = 0 }, + new StringProperty { Name = "stringinclass", Value = "" } + ] + } + ] + } + ] + }; + + public static IReadOnlyCollection MapWithDeepPropsCustomTypeDefinitions() => [ + new CustomClassDefinition + { + Name = "CustomClass", + UseAs = CustomClassUseAs.Property, + Members = [ + new BoolProperty + { + Name = "boolinclass", + Value = false + }, + new ColorProperty + { + Name = "colorinclass", + Value = Color.Parse("#000000ff", CultureInfo.InvariantCulture) + }, + new FileProperty + { + Name = "fileinclass", + Value = "" + }, + new FloatProperty + { + Name = "floatinclass", + Value = 0f + }, + new IntProperty + { + Name = "intinclass", + Value = 0 + }, + new ObjectProperty + { + Name = "objectinclass", + Value = 0 + }, + new StringProperty + { + Name = "stringinclass", + Value = "" + } + ] + }, + new CustomClassDefinition + { + Name = "CustomOuterClass", + UseAs = CustomClassUseAs.Property, + Members = [ + new ClassProperty + { + Name = "customclasspropinclass", + PropertyType = "CustomClass", + Value = [] // So no overrides of defaults in CustomClass + } + ] + } + ]; +} diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmj b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmj new file mode 100644 index 0000000..9fa2bba --- /dev/null +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmj @@ -0,0 +1,55 @@ +{ "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":"customouterclassprop", + "propertytype":"CustomOuterClass", + "type":"class", + "value": + { + + } + }, + { + "name":"customouterclasspropset", + "propertytype":"CustomOuterClass", + "type":"class", + "value": + { + "customclasspropinclass": + { + "boolinclass":true, + "floatinclass":13.37 + } + } + }], + "renderorder":"right-down", + "tiledversion":"1.11.0", + "tileheight":32, + "tilesets":[], + "tilewidth":32, + "type":"map", + "version":"1.10", + "width":5 +} \ No newline at end of file diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmx b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmx new file mode 100644 index 0000000..56e8f2e --- /dev/null +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-deep-props/map-with-deep-props.tmx @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + +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 + + + diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.cs index 0673cf6..fb3c95f 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-embedded-tileset/map-with-embedded-tileset.cs @@ -1,7 +1,4 @@ using System.Globalization; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Tilesets; namespace DotTiled.Tests; diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.cs index efea5c0..10c4d67 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-external-tileset/map-with-external-tileset.cs @@ -1,7 +1,4 @@ using System.Globalization; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Tilesets; namespace DotTiled.Tests; diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.cs index d2f1813..4e181c4 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-flippingflags/map-with-flippingflags.cs @@ -1,7 +1,4 @@ using System.Globalization; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Tilesets; namespace DotTiled.Tests; diff --git a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs index 2b09178..64974bc 100644 --- a/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs +++ b/src/DotTiled.Tests/Serialization/TestData/Map/map-with-many-layers/map-with-many-layers.cs @@ -1,9 +1,4 @@ using System.Numerics; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Layers.Objects; -using DotTiled.Model.Properties; -using DotTiled.Model.Tilesets; namespace DotTiled.Tests; @@ -99,10 +94,9 @@ public partial class TestData new Vector2(35.6667f, 32.3333f) ], Template = fileExt == "tmx" ? "poly.tx" : "poly.tj", - Properties = new Dictionary - { - ["templateprop"] = new StringProperty { Name = "templateprop", Value = "helo there" } - } + Properties = [ + new StringProperty { Name = "templateprop", Value = "helo there" } + ] }, new TileObject { diff --git a/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs b/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs index c862203..a896a48 100644 --- a/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs +++ b/src/DotTiled.Tests/Serialization/Tmj/TmjMapReaderTests.cs @@ -1,6 +1,3 @@ -using DotTiled.Model; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; using DotTiled.Serialization.Tmj; namespace DotTiled.Tests; @@ -13,7 +10,7 @@ public partial class TmjMapReaderTests public void TmxMapReaderReadMap_ValidTmjExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected( string testDataFile, Func expectedMap, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Arrange testDataFile += ".tmj"; @@ -22,16 +19,20 @@ public partial class TmjMapReaderTests Template ResolveTemplate(string source) { var templateJson = TestData.GetRawStringFor($"{fileDir}/{source}"); - using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, customTypeDefinitions); + using var templateReader = new TjTemplateReader(templateJson, ResolveTileset, ResolveTemplate, ResolveCustomType); return templateReader.ReadTemplate(); } Tileset ResolveTileset(string source) { var tilesetJson = TestData.GetRawStringFor($"{fileDir}/{source}"); - using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTemplate, customTypeDefinitions); + using var tilesetReader = new TsjTilesetReader(tilesetJson, ResolveTileset, ResolveTemplate, ResolveCustomType); return tilesetReader.ReadTileset(); } - using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, customTypeDefinitions); + ICustomTypeDefinition ResolveCustomType(string name) + { + return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; + } + using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, ResolveCustomType); // Act var map = mapReader.ReadMap(); diff --git a/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs b/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs index ab48769..b6e5813 100644 --- a/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs +++ b/src/DotTiled.Tests/Serialization/Tmx/TmxMapReaderTests.cs @@ -1,7 +1,3 @@ -using System.Xml; -using DotTiled.Model; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; using DotTiled.Serialization.Tmx; namespace DotTiled.Tests; @@ -14,7 +10,7 @@ public partial class TmxMapReaderTests public void TmxMapReaderReadMap_ValidXmlExternalTilesetsAndTemplates_ReturnsMapThatEqualsExpected( string testDataFile, Func expectedMap, - IReadOnlyCollection customTypeDefinitions) + IReadOnlyCollection customTypeDefinitions) { // Arrange testDataFile += ".tmx"; @@ -23,16 +19,20 @@ public partial class TmxMapReaderTests Template ResolveTemplate(string source) { using var xmlTemplateReader = TestData.GetXmlReaderFor($"{fileDir}/{source}"); - using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, customTypeDefinitions); + using var templateReader = new TxTemplateReader(xmlTemplateReader, ResolveTileset, ResolveTemplate, ResolveCustomType); return templateReader.ReadTemplate(); } Tileset ResolveTileset(string source) { using var xmlTilesetReader = TestData.GetXmlReaderFor($"{fileDir}/{source}"); - using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTemplate, customTypeDefinitions); + using var tilesetReader = new TsxTilesetReader(xmlTilesetReader, ResolveTileset, ResolveTemplate, ResolveCustomType); return tilesetReader.ReadTileset(); } - using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, customTypeDefinitions); + ICustomTypeDefinition ResolveCustomType(string name) + { + return customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == name)!; + } + using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, ResolveCustomType); // Act var map = mapReader.ReadMap(); diff --git a/src/DotTiled/Model/Color.cs b/src/DotTiled/Color.cs similarity index 50% rename from src/DotTiled/Model/Color.cs rename to src/DotTiled/Color.cs index 4700e0c..367c942 100644 --- a/src/DotTiled/Model/Color.cs +++ b/src/DotTiled/Color.cs @@ -2,21 +2,55 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; -namespace DotTiled.Model; +namespace DotTiled; +/// +/// Represents a Tiled color. +/// public class Color : IParsable, IEquatable { + /// + /// The red component of the color. + /// public required byte R { get; set; } + + /// + /// The green component of the color. + /// public required byte G { get; set; } + + /// + /// The blue component of the color. + /// public required byte B { get; set; } + + /// + /// The alpha component of the color. + /// public byte A { get; set; } = 255; + /// + /// Attempts to parse the specified string into a . Expects strings in the format #RRGGBB or #AARRGGBB. + /// The leading # is optional. + /// + /// A string value to parse into a + /// An object that supplies culture-specific information about the format of s. + /// The parsed + /// Thrown in case the provided string is not in a valid format. public static Color Parse(string s, IFormatProvider? provider) { - TryParse(s, provider, out var result); + _ = TryParse(s, provider, out var result); return result ?? throw new FormatException($"Invalid format for TiledColor: {s}"); } + /// + /// Attempts to parse the specified string into a . Expects strings in the format #RRGGBB or #AARRGGBB. + /// The leading # is optional. + /// + /// A string value to parse into a + /// An object that supplies culture-specific information about the format of s. + /// When this method returns, contains the parsed or null on failure. + /// true if was successfully parsed; otherwise, false. public static bool TryParse( [NotNullWhen(true)] string? s, IFormatProvider? provider, @@ -26,7 +60,7 @@ public class Color : IParsable, IEquatable return TryParse($"#{s}", provider, out result); // Format: #RRGGBB or #AARRGGBB - if (s is null || s.Length != 7 && s.Length != 9 || s[0] != '#') + if (s is null || (s.Length != 7 && s.Length != 9) || s[0] != '#') { result = default; return false; @@ -55,6 +89,7 @@ public class Color : IParsable, IEquatable return true; } + /// public bool Equals(Color? other) { if (other is null) @@ -63,9 +98,12 @@ public class Color : IParsable, IEquatable return R == other.R && G == other.G && B == other.B && A == other.A; } + /// public override bool Equals(object? obj) => obj is Color other && Equals(other); + /// public override int GetHashCode() => HashCode.Combine(R, G, B, A); + /// public override string ToString() => $"#{A:x2}{R:x2}{G:x2}{B:x2}"; } diff --git a/src/DotTiled/DotTiled.csproj b/src/DotTiled/DotTiled.csproj index 0102e6e..f5114f5 100644 --- a/src/DotTiled/DotTiled.csproj +++ b/src/DotTiled/DotTiled.csproj @@ -3,6 +3,28 @@ net8.0 enable + true + + DotTiled + dcronqvist + DotTiled + DotTiled is a general-purpose Tiled map parser for all your .NET needs. + git + https://github.com/dcronqvist/DotTiled + README.md + gamedev;window;parser;tiled;mapeditor + https://github.com/dcronqvist/DotTiled + true + Copyright © 2024 dcronqvist + LICENSE + 0.1.0 + + + + + + + diff --git a/src/DotTiled/Layers/BaseLayer.cs b/src/DotTiled/Layers/BaseLayer.cs new file mode 100644 index 0000000..f00cc01 --- /dev/null +++ b/src/DotTiled/Layers/BaseLayer.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; + +namespace DotTiled; + +/// +/// Base class for all layer types in a map. +/// To check the type of a layer, use C# pattern matching, +/// or some other mechanism to determine the type of the layer at runtime. +/// +public abstract class BaseLayer : HasPropertiesBase +{ + /// + /// Unique ID of the layer. Each layer that is added to a map gets a unique ID. Even if a layer is deleted, no layer ever gets the same ID. + /// + public required uint ID { get; set; } + + /// + /// The name of the layer. + /// + public string Name { get; set; } = ""; + + /// + /// The class of the layer. + /// + public string Class { get; set; } = ""; + + /// + /// The opacity of the layer as a value from 0 (fully transparent) to 1 (fully opaque). + /// + public float Opacity { get; set; } = 1.0f; + + /// + /// Whether the layer is shown (true) or hidden (false). + /// + public bool Visible { get; set; } = true; + + /// + /// A tint color that is multiplied with any tiles drawn by this layer. + /// + public Color? TintColor { get; set; } + + /// + /// Horizontal offset for this layer in pixels. + /// + public float OffsetX { get; set; } = 0.0f; + + /// + /// Vertical offset for this layer in pixels. + /// + public float OffsetY { get; set; } = 0.0f; + + /// + /// Horizontal parallax factor for this layer. + /// + public float ParallaxX { get; set; } = 1.0f; + + /// + /// Vertical parallax factor for this layer. + /// + public float ParallaxY { get; set; } = 1.0f; + + /// + /// Layer properties. + /// + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; +} diff --git a/src/DotTiled/Layers/Data.cs b/src/DotTiled/Layers/Data.cs new file mode 100644 index 0000000..6c74edf --- /dev/null +++ b/src/DotTiled/Layers/Data.cs @@ -0,0 +1,144 @@ +using System; + +namespace DotTiled; + +/// +/// Specifies the encoding used to encode the tile layer data. +/// +public enum DataEncoding +{ + /// + /// The data is stored as comma-separated values. + /// + Csv, + + /// + /// The data is stored as base64-encoded binary data. + /// + Base64 +} + +/// +/// Specifies the compression algorithm used to compress the tile layer data. +/// +public enum DataCompression +{ + /// + /// GZip compression. + /// + GZip, + + /// + /// ZLib compression. + /// + ZLib, + + /// + /// ZStandard compression. Currently not supported by DotTiled and will throw an exception if encountered. + /// + ZStd +} + +/// +/// The flipping flags for a tile. These can be used to check how a tile is flipped or rotated. Uses the +/// FlagsAttribute, for which there is plenty of documentation. +/// +[Flags] +public enum FlippingFlags : uint +{ + /// + /// No flipping. + /// + None = 0, + + /// + /// The tile is flipped horizontally. + /// + FlippedHorizontally = 0x80000000u, + + /// + /// The tile is flipped vertically. + /// + FlippedVertically = 0x40000000u, + + /// + /// The tile is flipped diagonally. + /// + FlippedDiagonally = 0x20000000u, + + /// + /// In hexagonal maps, the tile is rotated 120 degrees clockwise. + /// + RotatedHexagonal120 = 0x10000000u +} + +/// +/// Represents part of a tile layer of a map that is infinite. +/// +public class Chunk +{ + /// + /// The X coordinate of the chunk in tiles. + /// + public required int X { get; set; } + + /// + /// The Y coordinate of the chunk in tiles. + /// + public required int Y { get; set; } + + /// + /// The width of the chunk in tiles. + /// + public required uint Width { get; set; } + + /// + /// The height of the chunk in tiles. + /// + public required uint Height { get; set; } + + /// + /// The parsed chunk data, as a list of tile GIDs. + /// To get an actual tile ID, you map it to a local tile ID using the correct tileset. Please refer to + /// the documentation on how to do this. + /// + public required uint[] GlobalTileIDs { get; set; } + + /// + /// The parsed flipping flags for each tile in the chunk. Appear in the same order as the tiles in the layer in . + /// + public required FlippingFlags[] FlippingFlags { get; set; } +} + +/// +/// Represents the data of a tile layer. +/// +public class Data +{ + /// + /// The encoding used to encode the tile layer data. + /// + public DataEncoding? Encoding { get; set; } + + /// + /// The compression method used to compress the tile layer data. + /// + public DataCompression? Compression { get; set; } + + /// + /// The parsed tile layer data, as a list of tile GIDs. + /// To get an actual tile ID, you map it to a local tile ID using the correct tileset. Please refer to + /// the documentation on how to do this. + /// + public uint[]? GlobalTileIDs { get; set; } + + /// + /// The parsed flipping flags for each tile in the layer. Appear in the same order as the tiles in the layer in . + /// + public FlippingFlags[]? FlippingFlags { get; set; } + + /// + /// If the map is infinite, it will instead contain a list of chunks. + /// + public Chunk[]? Chunks { get; set; } +} diff --git a/src/DotTiled/Layers/Group.cs b/src/DotTiled/Layers/Group.cs new file mode 100644 index 0000000..703e938 --- /dev/null +++ b/src/DotTiled/Layers/Group.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace DotTiled; + +/// +/// Represents a group of layers, to form a hierarchy. +/// +public class Group : BaseLayer +{ + /// + /// The contained sub-layers in the group. + /// + public List Layers { get; set; } = []; +} diff --git a/src/DotTiled/Layers/ImageLayer.cs b/src/DotTiled/Layers/ImageLayer.cs new file mode 100644 index 0000000..11c157a --- /dev/null +++ b/src/DotTiled/Layers/ImageLayer.cs @@ -0,0 +1,32 @@ +namespace DotTiled; + +/// +/// Represents an image layer in a map. +/// +public class ImageLayer : BaseLayer +{ + /// + /// The X position of the image layer in pixels. + /// + public uint X { get; set; } = 0; + + /// + /// The Y position of the image layer in pixels. + /// + public uint Y { get; set; } = 0; + + /// + /// Whether the image drawn by this layer is repeated along the X axis. + /// + public bool RepeatX { get; set; } = false; + + /// + /// Whether the image drawn by this layer is repeated along the Y axis. + /// + public bool RepeatY { get; set; } = false; + + /// + /// The image to be drawn by this image layer. + /// + public Image? Image { get; set; } +} diff --git a/src/DotTiled/Layers/ObjectLayer.cs b/src/DotTiled/Layers/ObjectLayer.cs new file mode 100644 index 0000000..c39b445 --- /dev/null +++ b/src/DotTiled/Layers/ObjectLayer.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +namespace DotTiled; + +/// +/// Represents the order in which objects can be drawn. +/// +public enum DrawOrder +{ + /// + /// Objects are drawn sorted by their Y coordinate. + /// + TopDown, + + /// + /// Objects are drawn in the order of appearance in the object layer. + /// + Index +} + +/// +/// Represents an object layer in a map. In Tiled documentation, it is often called an "object group". +/// +public class ObjectLayer : BaseLayer +{ + /// + /// The X coordinate of the object layer in tiles. + /// + public uint X { get; set; } = 0; + + /// + /// The Y coordinate of the object layer in tiles. + /// + public uint Y { get; set; } = 0; + + /// + /// The width of the object layer in tiles. Meaningless. + /// + public uint? Width { get; set; } + + /// + /// The height of the object layer in tiles. Meaningless. + /// + public uint? Height { get; set; } + + /// + /// A color that is multiplied with any tile objects drawn by this layer. + /// + public Color? Color { get; set; } + + /// + /// Whether the objects are drawn according to the order of appearance () or sorted by their Y coordinate (). + /// + public DrawOrder DrawOrder { get; set; } = DrawOrder.TopDown; + + /// + /// The objects in the object layer. + /// + public required List Objects { get; set; } +} diff --git a/src/DotTiled/Layers/Objects/EllipseObject.cs b/src/DotTiled/Layers/Objects/EllipseObject.cs new file mode 100644 index 0000000..75db631 --- /dev/null +++ b/src/DotTiled/Layers/Objects/EllipseObject.cs @@ -0,0 +1,7 @@ +namespace DotTiled; + +/// +/// An ellipse object in a map. The existing , , , +/// and properties are used to determine the size of the ellipse. +/// +public class EllipseObject : Object { } diff --git a/src/DotTiled/Layers/Objects/Object.cs b/src/DotTiled/Layers/Objects/Object.cs new file mode 100644 index 0000000..fe97131 --- /dev/null +++ b/src/DotTiled/Layers/Objects/Object.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; + +namespace DotTiled; + +/// +/// Base class for objects in object layers. +/// +public abstract class Object : HasPropertiesBase +{ + /// + /// Unique ID of the objects. Each object that is placed on a map gets a unique ID. Even if an object was deleted, no object gets the same ID. + /// + public uint? ID { get; set; } + + /// + /// The name of the object. An arbitrary string. + /// + public string Name { get; set; } = ""; + + /// + /// The class of the object. An arbitrary string. + /// + public string Type { get; set; } = ""; + + /// + /// The X coordinate of the object in pixels. + /// + public float X { get; set; } = 0f; + + /// + /// The Y coordinate of the object in pixels. + /// + public float Y { get; set; } = 0f; + + /// + /// The width of the object in pixels. + /// + public float Width { get; set; } = 0f; + + /// + /// The height of the object in pixels. + /// + public float Height { get; set; } = 0f; + + /// + /// The rotation of the object in degrees clockwise around (X, Y). + /// + public float Rotation { get; set; } = 0f; + + /// + /// Whether the object is shown (true) or hidden (false). + /// + public bool Visible { get; set; } = true; + + /// + /// A reference to a template file. + /// + public string? Template { get; set; } + + /// + /// Object properties. + /// + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; +} diff --git a/src/DotTiled/Layers/Objects/PointObject.cs b/src/DotTiled/Layers/Objects/PointObject.cs new file mode 100644 index 0000000..0c53e1b --- /dev/null +++ b/src/DotTiled/Layers/Objects/PointObject.cs @@ -0,0 +1,7 @@ +namespace DotTiled; + +/// +/// A point object in a map. The existing and properties are used to +/// determine the position of the point. +/// +public class PointObject : Object { } diff --git a/src/DotTiled/Layers/Objects/PolygonObject.cs b/src/DotTiled/Layers/Objects/PolygonObject.cs new file mode 100644 index 0000000..2cf3895 --- /dev/null +++ b/src/DotTiled/Layers/Objects/PolygonObject.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace DotTiled; + +/// +/// A polygon object in a map. The existing and properties are used as +/// the origin of the polygon. +/// +public class PolygonObject : Object +{ + /// + /// The points that make up the polygon. + /// and are used as the origin of the polygon. + /// + public required List Points { get; set; } +} diff --git a/src/DotTiled/Layers/Objects/PolylineObject.cs b/src/DotTiled/Layers/Objects/PolylineObject.cs new file mode 100644 index 0000000..d755521 --- /dev/null +++ b/src/DotTiled/Layers/Objects/PolylineObject.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace DotTiled; + +/// +/// A polyline object in a map. The existing and properties are used as +/// the origin of the polyline. +/// +public class PolylineObject : Object +{ + /// + /// The points that make up the polyline. and are used as the origin of the polyline. + /// + public required List Points { get; set; } +} diff --git a/src/DotTiled/Layers/Objects/RectangleObject.cs b/src/DotTiled/Layers/Objects/RectangleObject.cs new file mode 100644 index 0000000..8e71ee8 --- /dev/null +++ b/src/DotTiled/Layers/Objects/RectangleObject.cs @@ -0,0 +1,7 @@ +namespace DotTiled; + +/// +/// A rectangle object in a map. The existing , , , +/// and properties are used to determine the size of the rectangle. +/// +public class RectangleObject : Object { } diff --git a/src/DotTiled/Layers/Objects/TextObject.cs b/src/DotTiled/Layers/Objects/TextObject.cs new file mode 100644 index 0000000..48a095c --- /dev/null +++ b/src/DotTiled/Layers/Objects/TextObject.cs @@ -0,0 +1,116 @@ +using System.Globalization; + +namespace DotTiled; + +/// +/// The horizontal alignment of text. +/// +public enum TextHorizontalAlignment +{ + /// + /// The text is aligned to the left. + /// + Left, + + /// + /// The text is aligned to the center. + /// + Center, + + /// + /// The text is aligned to the right. + /// + Right, + + /// + /// The text is justified. + /// + Justify +} + +/// +/// The vertical alignment of text. +/// +public enum TextVerticalAlignment +{ + /// + /// The text is aligned to the top. + /// + Top, + + /// + /// The text is aligned to the center. + /// + Center, + + /// + /// The text is aligned to the bottom. + /// + Bottom +} + +/// +/// A text object in a map. +/// +public class TextObject : Object +{ + /// + /// The font family used for the text. + /// + public string FontFamily { get; set; } = "sans-serif"; + + /// + /// The size of the font in pixels. + /// + public int PixelSize { get; set; } = 16; + + /// + /// Whether word wrapping is enabled. + /// + public bool Wrap { get; set; } = false; + + /// + /// The color of the text. + /// + public Color Color { get; set; } = Color.Parse("#000000", CultureInfo.InvariantCulture); + + /// + /// Whether the text is bold. + /// + public bool Bold { get; set; } = false; + + /// + /// Whether the text is italic. + /// + public bool Italic { get; set; } = false; + + /// + /// Whether a line should be drawn below the text. + /// + public bool Underline { get; set; } = false; + + /// + /// Whether a line should be drawn through the text. + /// + public bool Strikeout { get; set; } = false; + + /// + /// Whether kerning should be used while rendering the text. + /// + public bool Kerning { get; set; } = true; + + /// + /// The horizontal alignment of the text. + /// + public TextHorizontalAlignment HorizontalAlignment { get; set; } = TextHorizontalAlignment.Left; + + /// + /// The vertical alignment of the text. + /// + public TextVerticalAlignment VerticalAlignment { get; set; } = TextVerticalAlignment.Top; + + /// + /// The text to be displayed. + /// + public string Text { get; set; } = ""; +} diff --git a/src/DotTiled/Layers/Objects/TileObject.cs b/src/DotTiled/Layers/Objects/TileObject.cs new file mode 100644 index 0000000..ab00628 --- /dev/null +++ b/src/DotTiled/Layers/Objects/TileObject.cs @@ -0,0 +1,12 @@ +namespace DotTiled; + +/// +/// A tile object in a map. +/// +public class TileObject : Object +{ + /// + /// A reference to a tile. + /// + public uint GID { get; set; } +} diff --git a/src/DotTiled/Layers/TileLayer.cs b/src/DotTiled/Layers/TileLayer.cs new file mode 100644 index 0000000..8207f10 --- /dev/null +++ b/src/DotTiled/Layers/TileLayer.cs @@ -0,0 +1,32 @@ +namespace DotTiled; + +/// +/// Represents a tile layer in a map. +/// +public class TileLayer : BaseLayer +{ + /// + /// The X coordinate of the layer in tiles. + /// + public uint X { get; set; } = 0; + + /// + /// The Y coordinate of the layer in tiles. + /// + public uint Y { get; set; } = 0; + + /// + /// The width of the layer in tiles. Always the same as the map width for fixed-size maps. + /// + public required uint Width { get; set; } + + /// + /// The height of the layer in tiles. Always the same as the map height for fixed-size maps. + /// + public required uint Height { get; set; } + + /// + /// The tile layer data. + /// + public Data? Data { get; set; } +} diff --git a/src/DotTiled/Map.cs b/src/DotTiled/Map.cs new file mode 100644 index 0000000..0db1205 --- /dev/null +++ b/src/DotTiled/Map.cs @@ -0,0 +1,208 @@ +using System.Collections.Generic; +using System.Globalization; + +namespace DotTiled; + +/// +/// Map orientation enumeration. The map orientation determines the alignment of the tiles in the map. +/// +public enum MapOrientation +{ + /// + /// Orthogonal orientation. This is the typical top-down grid-based layout. + /// + Orthogonal, + + /// + /// Isometric orientation. This is a type of axonometric projection where the tiles are shown as rhombuses, as seen from a side-on view. + /// + Isometric, + + /// + /// Staggered orientation. This is an isometric projection with a side-on view where the tiles are arranged in a staggered grid. + /// + Staggered, + + /// + /// Hexagonal orientation. This is a type of axial projection where the tiles are shown as hexagons, as seen from a top-down view. + /// + Hexagonal +} + +/// +/// Render order enumeration. The order in which tiles on tile layers are rendered. +/// +public enum RenderOrder +{ + /// + /// Right-down render order. Starts at top-left and proceeds right then down. + /// + RightDown, + + /// + /// Right-up render order. Starts at bottom-left and proceeds right then up. + /// + RightUp, + + /// + /// Left-down render order. Starts at top-right and proceeds left then down. + /// + LeftDown, + + /// + /// Left-up render order. Starts at bottom-right and proceeds left then up. + /// + LeftUp +} + +/// +/// Stagger axis enumeration. For staggered and hexagonal maps, determines which axis (X or Y) is staggered. +/// +public enum StaggerAxis +{ + /// + /// X stagger axis. + /// + X, + + /// + /// Y stagger axis. + /// + Y +} + +/// +/// Stagger index enumeration. For staggered and hexagonal maps, determines whether the "even" or "odd" indexes along the staggered axis are shifted. +/// +public enum StaggerIndex +{ + /// + /// Even stagger index. + /// + Odd, + + /// + /// Odd stagger index. + /// + Even +} + +/// +/// Represents a Tiled map. +/// +public class Map : HasPropertiesBase +{ + /// + /// The TMX format version. Is incremented to match minor Tiled releases. + /// + public required string Version { get; set; } + + /// + /// The Tiled version used to save the file. + /// + public required string TiledVersion { get; set; } + + /// + /// The class of this map. + /// + public string Class { get; set; } = ""; + + /// + /// Map orientation. + /// + public required MapOrientation Orientation { get; set; } + + /// + /// The order in which tiles on tile layers are rendered. + /// + public RenderOrder RenderOrder { get; set; } = RenderOrder.RightDown; + + /// + /// The compression level to use for tile layer data (defaults to -1, which means to use the algorithm default). + /// Typically only useful for parsing, but may be interesting for certain use cases. + /// + public int CompressionLevel { get; set; } = -1; + + /// + /// The width of the map in tiles. + /// + public required uint Width { get; set; } + + /// + /// The height of the map in tiles. + /// + public required uint Height { get; set; } + + /// + /// The width of a tile. + /// + public required uint TileWidth { get; set; } + + /// + /// The height of a tile. + /// + public required uint TileHeight { get; set; } + + /// + /// Only for hexagonal maps. Determines the width or height (depending on the staggered axis) of the tile's edge, in pixels. + /// + public uint? HexSideLength { get; set; } + + /// + /// For staggered and hexagonal maps, determines which axis (X or Y) is staggered. + /// + public StaggerAxis? StaggerAxis { get; set; } + + /// + /// For staggered and hexagonal maps, determines whether the "even" or "odd" indexes along the staggered axis are shifted. + /// + public StaggerIndex? StaggerIndex { get; set; } + + /// + /// X coordinate of the parallax origin in pixels. + /// + public float ParallaxOriginX { get; set; } = 0.0f; + + /// + /// Y coordinate of the parallax origin in pixels. + /// + public float ParallaxOriginY { get; set; } = 0.0f; + + /// + /// The background color of the map. + /// + public Color BackgroundColor { get; set; } = Color.Parse("#00000000", CultureInfo.InvariantCulture); + + /// + /// Stores the next available ID for new layers. This number is used to prevent reuse of the same ID after layers have been removed. + /// + public required uint NextLayerID { get; set; } + + /// + /// Stores the next available ID for new objects. This number is used to prevent reuse of the same ID after objects have been removed. + /// + public required uint NextObjectID { get; set; } + + /// + /// Whether this map is infinite. An infinite map has no fixed size and can grow in all directions. Its layer data is stored in chunks. + /// + public bool Infinite { get; set; } = false; + + /// + /// Map properties. + /// + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; + + /// + /// List of tilesets used by the map. + /// + public List Tilesets { get; set; } = []; + + /// + /// Hierarchical list of layers. is a layer type which can contain sub-layers to create a hierarchy. + /// + public List Layers { get; set; } = []; +} diff --git a/src/DotTiled/Model/Layers/BaseLayer.cs b/src/DotTiled/Model/Layers/BaseLayer.cs deleted file mode 100644 index adeee69..0000000 --- a/src/DotTiled/Model/Layers/BaseLayer.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using DotTiled.Model.Properties; - -namespace DotTiled.Model.Layers; - -public abstract class BaseLayer -{ - // Attributes - public required uint ID { get; set; } - public string Name { get; set; } = ""; - public string Class { get; set; } = ""; - public float Opacity { get; set; } = 1.0f; - public bool Visible { get; set; } = true; - public Color? TintColor { get; set; } - public float OffsetX { get; set; } = 0.0f; - public float OffsetY { get; set; } = 0.0f; - public float ParallaxX { get; set; } = 1.0f; - public float ParallaxY { get; set; } = 1.0f; - - // At most one of - public Dictionary? Properties { get; set; } -} diff --git a/src/DotTiled/Model/Layers/Data.cs b/src/DotTiled/Model/Layers/Data.cs deleted file mode 100644 index 2d54020..0000000 --- a/src/DotTiled/Model/Layers/Data.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; - -namespace DotTiled.Model.Layers; - -public enum DataEncoding -{ - Csv, - Base64 -} - -public enum DataCompression -{ - GZip, - ZLib, - ZStd -} - -[Flags] -public enum FlippingFlags : uint -{ - None = 0, - FlippedHorizontally = 0x80000000u, - FlippedVertically = 0x40000000u, - FlippedDiagonally = 0x20000000u, - RotatedHexagonal120 = 0x10000000u -} - -public class Chunk -{ - // Attributes - public required int X { get; set; } - public required int Y { get; set; } - public required uint Width { get; set; } - public required uint Height { get; set; } - - // Data - public required uint[] GlobalTileIDs { get; set; } - public required FlippingFlags[] FlippingFlags { get; set; } -} - -public class Data -{ - // Attributes - public DataEncoding? Encoding { get; set; } - public DataCompression? Compression { get; set; } - - // Data - public uint[]? GlobalTileIDs { get; set; } - public FlippingFlags[]? FlippingFlags { get; set; } - public Chunk[]? Chunks { get; set; } -} diff --git a/src/DotTiled/Model/Layers/Group.cs b/src/DotTiled/Model/Layers/Group.cs deleted file mode 100644 index fd89f61..0000000 --- a/src/DotTiled/Model/Layers/Group.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; - -namespace DotTiled.Model.Layers; - -public class Group : BaseLayer -{ - // Uses same attributes as BaseLayer - - // Any number of - public List Layers { get; set; } = []; -} diff --git a/src/DotTiled/Model/Layers/ImageLayer.cs b/src/DotTiled/Model/Layers/ImageLayer.cs deleted file mode 100644 index cd35a41..0000000 --- a/src/DotTiled/Model/Layers/ImageLayer.cs +++ /dev/null @@ -1,15 +0,0 @@ -using DotTiled.Model.Tilesets; - -namespace DotTiled.Model.Layers; - -public class ImageLayer : BaseLayer -{ - // Attributes - public uint X { get; set; } = 0; - public uint Y { get; set; } = 0; - public bool RepeatX { get; set; } = false; - public bool RepeatY { get; set; } = false; - - // At most one of - public Image? Image { get; set; } -} diff --git a/src/DotTiled/Model/Layers/ObjectLayer.cs b/src/DotTiled/Model/Layers/ObjectLayer.cs deleted file mode 100644 index 817e95e..0000000 --- a/src/DotTiled/Model/Layers/ObjectLayer.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using DotTiled.Model.Layers.Objects; - -namespace DotTiled.Model.Layers; - -public enum DrawOrder -{ - TopDown, - Index -} - -public class ObjectLayer : BaseLayer -{ - // Attributes - public uint X { get; set; } = 0; - public uint Y { get; set; } = 0; - public uint? Width { get; set; } - public uint? Height { get; set; } - public Color? Color { get; set; } - public DrawOrder DrawOrder { get; set; } = DrawOrder.TopDown; - - // Elements - public required List Objects { get; set; } -} diff --git a/src/DotTiled/Model/Layers/Objects/EllipseObject.cs b/src/DotTiled/Model/Layers/Objects/EllipseObject.cs deleted file mode 100644 index fe57573..0000000 --- a/src/DotTiled/Model/Layers/Objects/EllipseObject.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace DotTiled.Model.Layers.Objects; - -public class EllipseObject : Object { } diff --git a/src/DotTiled/Model/Layers/Objects/Object.cs b/src/DotTiled/Model/Layers/Objects/Object.cs deleted file mode 100644 index 82254e5..0000000 --- a/src/DotTiled/Model/Layers/Objects/Object.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using DotTiled.Model.Properties; - -namespace DotTiled.Model.Layers.Objects; - -public abstract class Object -{ - // Attributes - public uint? ID { get; set; } - public string Name { get; set; } = ""; - public string Type { get; set; } = ""; - public float X { get; set; } = 0f; - public float Y { get; set; } = 0f; - public float Width { get; set; } = 0f; - public float Height { get; set; } = 0f; - public float Rotation { get; set; } = 0f; - public bool Visible { get; set; } = true; - public string? Template { get; set; } - - // Elements - public Dictionary? Properties { get; set; } -} diff --git a/src/DotTiled/Model/Layers/Objects/PointObject.cs b/src/DotTiled/Model/Layers/Objects/PointObject.cs deleted file mode 100644 index f2949a2..0000000 --- a/src/DotTiled/Model/Layers/Objects/PointObject.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace DotTiled.Model.Layers.Objects; - -public class PointObject : Object { } diff --git a/src/DotTiled/Model/Layers/Objects/PolygonObject.cs b/src/DotTiled/Model/Layers/Objects/PolygonObject.cs deleted file mode 100644 index 57e3f06..0000000 --- a/src/DotTiled/Model/Layers/Objects/PolygonObject.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using System.Numerics; - -namespace DotTiled.Model.Layers.Objects; - -public class PolygonObject : Object -{ - // Attributes - public required List Points { get; set; } -} diff --git a/src/DotTiled/Model/Layers/Objects/PolylineObject.cs b/src/DotTiled/Model/Layers/Objects/PolylineObject.cs deleted file mode 100644 index 03cd6b4..0000000 --- a/src/DotTiled/Model/Layers/Objects/PolylineObject.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using System.Numerics; - -namespace DotTiled.Model.Layers.Objects; - -public class PolylineObject : Object -{ - // Attributes - public required List Points { get; set; } -} diff --git a/src/DotTiled/Model/Layers/Objects/RectangleObject.cs b/src/DotTiled/Model/Layers/Objects/RectangleObject.cs deleted file mode 100644 index ff64a76..0000000 --- a/src/DotTiled/Model/Layers/Objects/RectangleObject.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace DotTiled.Model.Layers.Objects; - -public class RectangleObject : Object { } diff --git a/src/DotTiled/Model/Layers/Objects/TextObject.cs b/src/DotTiled/Model/Layers/Objects/TextObject.cs deleted file mode 100644 index f39f84a..0000000 --- a/src/DotTiled/Model/Layers/Objects/TextObject.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Globalization; - -namespace DotTiled.Model.Layers.Objects; - - -public enum TextHorizontalAlignment -{ - Left, - Center, - Right, - Justify -} - -public enum TextVerticalAlignment -{ - Top, - Center, - Bottom -} - -public class TextObject : Object -{ - // Attributes - public string FontFamily { get; set; } = "sans-serif"; - public int PixelSize { get; set; } = 16; - public bool Wrap { get; set; } = false; - public Color Color { get; set; } = Color.Parse("#000000", CultureInfo.InvariantCulture); - public bool Bold { get; set; } = false; - public bool Italic { get; set; } = false; - public bool Underline { get; set; } = false; - public bool Strikeout { get; set; } = false; - public bool Kerning { get; set; } = true; - public TextHorizontalAlignment HorizontalAlignment { get; set; } = TextHorizontalAlignment.Left; - public TextVerticalAlignment VerticalAlignment { get; set; } = TextVerticalAlignment.Top; - - // Elements - public string Text { get; set; } = ""; -} diff --git a/src/DotTiled/Model/Layers/Objects/TileObject.cs b/src/DotTiled/Model/Layers/Objects/TileObject.cs deleted file mode 100644 index 992ac3c..0000000 --- a/src/DotTiled/Model/Layers/Objects/TileObject.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DotTiled.Model.Layers.Objects; - -public class TileObject : Object -{ - public uint GID { get; set; } -} diff --git a/src/DotTiled/Model/Layers/TileLayer.cs b/src/DotTiled/Model/Layers/TileLayer.cs deleted file mode 100644 index 9002397..0000000 --- a/src/DotTiled/Model/Layers/TileLayer.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DotTiled.Model.Layers; - -public class TileLayer : BaseLayer -{ - // Attributes - public uint X { get; set; } = 0; - public uint Y { get; set; } = 0; - public required uint Width { get; set; } - public required uint Height { get; set; } - - // At most one of - public Data? Data { get; set; } -} diff --git a/src/DotTiled/Model/Map.cs b/src/DotTiled/Model/Map.cs deleted file mode 100644 index 3102567..0000000 --- a/src/DotTiled/Model/Map.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Tilesets; - -namespace DotTiled.Model; - -public enum MapOrientation -{ - Orthogonal, - Isometric, - Staggered, - Hexagonal -} - -public enum RenderOrder -{ - RightDown, - RightUp, - LeftDown, - LeftUp -} - -public enum StaggerAxis -{ - X, - Y -} - -public enum StaggerIndex -{ - Odd, - Even -} - -public class Map -{ - // Attributes - public required string Version { get; set; } - public required string TiledVersion { get; set; } - public string Class { get; set; } = ""; - public required MapOrientation Orientation { get; set; } - public RenderOrder RenderOrder { get; set; } = RenderOrder.RightDown; - public int CompressionLevel { get; set; } = -1; - public required uint Width { get; set; } - public required uint Height { get; set; } - public required uint TileWidth { get; set; } - public required uint TileHeight { get; set; } - public uint? HexSideLength { get; set; } - public StaggerAxis? StaggerAxis { get; set; } - public StaggerIndex? StaggerIndex { get; set; } - public float ParallaxOriginX { get; set; } = 0.0f; - public float ParallaxOriginY { get; set; } = 0.0f; - public Color BackgroundColor { get; set; } = Color.Parse("#00000000", CultureInfo.InvariantCulture); - public required uint NextLayerID { get; set; } - public required uint NextObjectID { get; set; } - public bool Infinite { get; set; } = false; - - // At most one of - public Dictionary? Properties { get; set; } - - // Any number of - public List Tilesets { get; set; } = []; - public List Layers { get; set; } = []; -} diff --git a/src/DotTiled/Model/Properties/BoolProperty.cs b/src/DotTiled/Model/Properties/BoolProperty.cs deleted file mode 100644 index 6360a3a..0000000 --- a/src/DotTiled/Model/Properties/BoolProperty.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DotTiled.Model.Properties; - -public class BoolProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.Bool; - public required bool Value { get; set; } - - public IProperty Clone() => new BoolProperty - { - Name = Name, - Value = Value - }; -} diff --git a/src/DotTiled/Model/Properties/ClassProperty.cs b/src/DotTiled/Model/Properties/ClassProperty.cs deleted file mode 100644 index e46389f..0000000 --- a/src/DotTiled/Model/Properties/ClassProperty.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace DotTiled.Model.Properties; - -public class ClassProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => Model.Properties.PropertyType.Class; - public required string PropertyType { get; set; } - public required Dictionary Properties { get; set; } - - public IProperty Clone() => new ClassProperty - { - Name = Name, - PropertyType = PropertyType, - Properties = Properties.ToDictionary(p => p.Key, p => p.Value.Clone()) - }; -} diff --git a/src/DotTiled/Model/Properties/ColorProperty.cs b/src/DotTiled/Model/Properties/ColorProperty.cs deleted file mode 100644 index 06d8fc1..0000000 --- a/src/DotTiled/Model/Properties/ColorProperty.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DotTiled.Model.Properties; - -public class ColorProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.Color; - public required Color Value { get; set; } - - public IProperty Clone() => new ColorProperty - { - Name = Name, - Value = Value - }; -} diff --git a/src/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs b/src/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs deleted file mode 100644 index c080e2a..0000000 --- a/src/DotTiled/Model/Properties/CustomTypes/CustomClassDefinition.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace DotTiled.Model.Properties.CustomTypes; - -[Flags] -public enum CustomClassUseAs -{ - Property, - Map, - Layer, - Object, - Tile, - Tileset, - WangColor, - Wangset, - Project, - All = Property | Map | Layer | Object | Tile | Tileset | WangColor | Wangset | Project -} - -public class CustomClassDefinition : CustomTypeDefinition -{ - public Color? Color { get; set; } - public bool DrawFill { get; set; } - public CustomClassUseAs UseAs { get; set; } - public List Members { get; set; } = []; -} diff --git a/src/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs b/src/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs deleted file mode 100644 index 9d15b7d..0000000 --- a/src/DotTiled/Model/Properties/CustomTypes/CustomEnumDefinition.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; - -namespace DotTiled.Model.Properties.CustomTypes; - -public enum CustomEnumStorageType -{ - Int, - String -} - -public class CustomEnumDefinition : CustomTypeDefinition -{ - public CustomEnumStorageType StorageType { get; set; } - public List Values { get; set; } = []; - public bool ValueAsFlags { get; set; } -} diff --git a/src/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs b/src/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs deleted file mode 100644 index f535215..0000000 --- a/src/DotTiled/Model/Properties/CustomTypes/CustomTypeDefinition.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace DotTiled.Model.Properties.CustomTypes; - -public abstract class CustomTypeDefinition -{ - public uint ID { get; set; } - public string Name { get; set; } = ""; -} diff --git a/src/DotTiled/Model/Properties/FileProperty.cs b/src/DotTiled/Model/Properties/FileProperty.cs deleted file mode 100644 index 42b9d15..0000000 --- a/src/DotTiled/Model/Properties/FileProperty.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DotTiled.Model.Properties; - -public class FileProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.File; - public required string Value { get; set; } - - public IProperty Clone() => new FileProperty - { - Name = Name, - Value = Value - }; -} diff --git a/src/DotTiled/Model/Properties/FloatProperty.cs b/src/DotTiled/Model/Properties/FloatProperty.cs deleted file mode 100644 index ccb18ae..0000000 --- a/src/DotTiled/Model/Properties/FloatProperty.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DotTiled.Model.Properties; - -public class FloatProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.Float; - public required float Value { get; set; } - - public IProperty Clone() => new FloatProperty - { - Name = Name, - Value = Value - }; -} diff --git a/src/DotTiled/Model/Properties/IProperty.cs b/src/DotTiled/Model/Properties/IProperty.cs deleted file mode 100644 index 0414c3c..0000000 --- a/src/DotTiled/Model/Properties/IProperty.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace DotTiled.Model.Properties; - -public interface IProperty -{ - public string Name { get; set; } - public PropertyType Type { get; } - - IProperty Clone(); -} diff --git a/src/DotTiled/Model/Properties/IntProperty.cs b/src/DotTiled/Model/Properties/IntProperty.cs deleted file mode 100644 index cfaf7d0..0000000 --- a/src/DotTiled/Model/Properties/IntProperty.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DotTiled.Model.Properties; - -public class IntProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.Int; - public required int Value { get; set; } - - public IProperty Clone() => new IntProperty - { - Name = Name, - Value = Value - }; -} diff --git a/src/DotTiled/Model/Properties/ObjectProperty.cs b/src/DotTiled/Model/Properties/ObjectProperty.cs deleted file mode 100644 index 5f37607..0000000 --- a/src/DotTiled/Model/Properties/ObjectProperty.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DotTiled.Model.Properties; - -public class ObjectProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.Object; - public required uint Value { get; set; } - - public IProperty Clone() => new ObjectProperty - { - Name = Name, - Value = Value - }; -} diff --git a/src/DotTiled/Model/Properties/PropertyType.cs b/src/DotTiled/Model/Properties/PropertyType.cs deleted file mode 100644 index bb01960..0000000 --- a/src/DotTiled/Model/Properties/PropertyType.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DotTiled.Model.Properties; - -public enum PropertyType -{ - String, - Int, - Float, - Bool, - Color, - File, - Object, - Class -} diff --git a/src/DotTiled/Model/Properties/StringProperty.cs b/src/DotTiled/Model/Properties/StringProperty.cs deleted file mode 100644 index 9842ca4..0000000 --- a/src/DotTiled/Model/Properties/StringProperty.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace DotTiled.Model.Properties; - -public class StringProperty : IProperty -{ - public required string Name { get; set; } - public PropertyType Type => PropertyType.String; - public required string Value { get; set; } - - public IProperty Clone() => new StringProperty - { - Name = Name, - Value = Value - }; -} diff --git a/src/DotTiled/Model/Template.cs b/src/DotTiled/Model/Template.cs deleted file mode 100644 index 834f8d8..0000000 --- a/src/DotTiled/Model/Template.cs +++ /dev/null @@ -1,11 +0,0 @@ -using DotTiled.Model.Layers.Objects; -using DotTiled.Model.Tilesets; - -namespace DotTiled.Model; - -public class Template -{ - // At most one of (if the template is a tile object) - public Tileset? Tileset { get; set; } - public required Object Object { get; set; } -} diff --git a/src/DotTiled/Model/Tilesets/Frame.cs b/src/DotTiled/Model/Tilesets/Frame.cs deleted file mode 100644 index 8762423..0000000 --- a/src/DotTiled/Model/Tilesets/Frame.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DotTiled.Model.Tilesets; - -public class Frame -{ - // Attributes - public required uint TileID { get; set; } - public required uint Duration { get; set; } -} diff --git a/src/DotTiled/Model/Tilesets/Grid.cs b/src/DotTiled/Model/Tilesets/Grid.cs deleted file mode 100644 index 81d5e06..0000000 --- a/src/DotTiled/Model/Tilesets/Grid.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace DotTiled.Model.Tilesets; - -public enum GridOrientation -{ - Orthogonal, - Isometric -} - -public class Grid -{ - // Attributes - public GridOrientation Orientation { get; set; } = GridOrientation.Orthogonal; - public required uint Width { get; set; } - public required uint Height { get; set; } -} diff --git a/src/DotTiled/Model/Tilesets/Image.cs b/src/DotTiled/Model/Tilesets/Image.cs deleted file mode 100644 index e5ea154..0000000 --- a/src/DotTiled/Model/Tilesets/Image.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace DotTiled.Model.Tilesets; - -public enum ImageFormat -{ - Png, - Gif, - Jpg, - Bmp -} - -public class Image -{ - // Attributes - public ImageFormat? Format { get; set; } - public string? Source { get; set; } - public Color? TransparentColor { get; set; } - public uint? Width { get; set; } - public uint? Height { get; set; } -} diff --git a/src/DotTiled/Model/Tilesets/Tile.cs b/src/DotTiled/Model/Tilesets/Tile.cs deleted file mode 100644 index 55669ee..0000000 --- a/src/DotTiled/Model/Tilesets/Tile.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; - -namespace DotTiled.Model.Tilesets; - -public class Tile -{ - // Attributes - public required uint ID { get; set; } - public string Type { get; set; } = ""; - public float Probability { get; set; } = 0f; - public uint X { get; set; } = 0; - public uint Y { get; set; } = 0; - public required uint Width { get; set; } - public required uint Height { get; set; } - - // Elements - public Dictionary? Properties { get; set; } - public Image? Image { get; set; } - public ObjectLayer? ObjectLayer { get; set; } - public List? Animation { get; set; } -} diff --git a/src/DotTiled/Model/Tilesets/TileOffset.cs b/src/DotTiled/Model/Tilesets/TileOffset.cs deleted file mode 100644 index cbe9111..0000000 --- a/src/DotTiled/Model/Tilesets/TileOffset.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DotTiled.Model.Tilesets; - -public class TileOffset -{ - // Attributes - public float X { get; set; } = 0f; - public float Y { get; set; } = 0f; -} diff --git a/src/DotTiled/Model/Tilesets/Tileset.cs b/src/DotTiled/Model/Tilesets/Tileset.cs deleted file mode 100644 index 15bd56b..0000000 --- a/src/DotTiled/Model/Tilesets/Tileset.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Collections.Generic; -using DotTiled.Model.Properties; - -namespace DotTiled.Model.Tilesets; - -public enum ObjectAlignment -{ - Unspecified, - TopLeft, - Top, - TopRight, - Left, - Center, - Right, - BottomLeft, - Bottom, - BottomRight -} - -public enum TileRenderSize -{ - Tile, - Grid -} - -public enum FillMode -{ - Stretch, - PreserveAspectFit -} - -public class Tileset -{ - // Attributes - public string? Version { get; set; } - public string? TiledVersion { get; set; } - public uint? FirstGID { get; set; } - public string? Source { get; set; } - public string? Name { get; set; } - public string Class { get; set; } = ""; - public uint? TileWidth { get; set; } - public uint? TileHeight { get; set; } - public float? Spacing { get; set; } = 0f; - public float? Margin { get; set; } = 0f; - public uint? TileCount { get; set; } - public uint? Columns { get; set; } - public ObjectAlignment ObjectAlignment { get; set; } = ObjectAlignment.Unspecified; - public TileRenderSize RenderSize { get; set; } = TileRenderSize.Tile; - public FillMode FillMode { get; set; } = FillMode.Stretch; - - // At most one of - public Image? Image { get; set; } - public TileOffset? TileOffset { get; set; } - public Grid? Grid { get; set; } - public Dictionary? Properties { get; set; } - // public List? TerrainTypes { get; set; } TODO: Implement Terrain -> Wangset conversion during deserialization - public List? Wangsets { get; set; } - public Transformations? Transformations { get; set; } - - // Any number of - public List Tiles { get; set; } = []; -} diff --git a/src/DotTiled/Model/Tilesets/Transformations.cs b/src/DotTiled/Model/Tilesets/Transformations.cs deleted file mode 100644 index 83187a7..0000000 --- a/src/DotTiled/Model/Tilesets/Transformations.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DotTiled.Model.Tilesets; - -public class Transformations -{ - // Attributes - public bool HFlip { get; set; } = false; - public bool VFlip { get; set; } = false; - public bool Rotate { get; set; } = false; - public bool PreferUntransformed { get; set; } = false; -} diff --git a/src/DotTiled/Model/Tilesets/WangColor.cs b/src/DotTiled/Model/Tilesets/WangColor.cs deleted file mode 100644 index c13b0da..0000000 --- a/src/DotTiled/Model/Tilesets/WangColor.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using DotTiled.Model.Properties; - -namespace DotTiled.Model.Tilesets; - -public class WangColor -{ - // Attributes - public required string Name { get; set; } - public string Class { get; set; } = ""; - public required Color Color { get; set; } - public required int Tile { get; set; } - public float Probability { get; set; } = 0f; - - // Elements - public Dictionary? Properties { get; set; } -} diff --git a/src/DotTiled/Model/Tilesets/WangTile.cs b/src/DotTiled/Model/Tilesets/WangTile.cs deleted file mode 100644 index 488a12a..0000000 --- a/src/DotTiled/Model/Tilesets/WangTile.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DotTiled.Model.Tilesets; - -public class WangTile -{ - // Attributes - public required uint TileID { get; set; } - public required byte[] WangID { get; set; } -} diff --git a/src/DotTiled/Model/Tilesets/Wangset.cs b/src/DotTiled/Model/Tilesets/Wangset.cs deleted file mode 100644 index 6101347..0000000 --- a/src/DotTiled/Model/Tilesets/Wangset.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using DotTiled.Model.Properties; - -namespace DotTiled.Model.Tilesets; - -public class Wangset -{ - // Attributes - public required string Name { get; set; } - public string Class { get; set; } = ""; - public required int Tile { get; set; } - - // Elements - // At most one of - public Dictionary? Properties { get; set; } - - // Up to 254 Wang colors - public List? WangColors { get; set; } = []; - - // Any number of - public List WangTiles { get; set; } = []; -} diff --git a/src/DotTiled/Properties/BoolProperty.cs b/src/DotTiled/Properties/BoolProperty.cs new file mode 100644 index 0000000..e401d38 --- /dev/null +++ b/src/DotTiled/Properties/BoolProperty.cs @@ -0,0 +1,25 @@ +namespace DotTiled; + +/// +/// Represents a boolean property. +/// +public class BoolProperty : IProperty +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => PropertyType.Bool; + + /// + /// The boolean value of the property. + /// + public required bool Value { get; set; } + + /// + public IProperty Clone() => new BoolProperty + { + Name = Name, + Value = Value + }; +} diff --git a/src/DotTiled/Properties/ClassProperty.cs b/src/DotTiled/Properties/ClassProperty.cs new file mode 100644 index 0000000..933660b --- /dev/null +++ b/src/DotTiled/Properties/ClassProperty.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace DotTiled; + +/// +/// Represents a class property. +/// +public class ClassProperty : IHasProperties, IProperty> +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => DotTiled.PropertyType.Class; + + /// + /// The type of the class property. This will be the name of a custom defined + /// type in Tiled. + /// + public required string PropertyType { get; set; } + + /// + /// The properties of the class property. + /// + public required IList Value { get; set; } + + /// + public IProperty Clone() => new ClassProperty + { + Name = Name, + PropertyType = PropertyType, + Value = Value.Select(property => property.Clone()).ToList() + }; + + /// + public IList GetProperties() => Value; + + /// + public T GetProperty(string name) where T : IProperty + { + var property = Value.FirstOrDefault(_properties => _properties.Name == name) ?? throw new InvalidOperationException($"Property '{name}' not found."); + if (property is T prop) + { + return prop; + } + + throw new InvalidOperationException($"Property '{name}' is not of type '{typeof(T).Name}'."); + } + + /// + public bool TryGetProperty(string name, [NotNullWhen(true)] out T? property) where T : IProperty + { + if (Value.FirstOrDefault(_properties => _properties.Name == name) is T prop) + { + property = prop; + return true; + } + + property = default; + return false; + } +} diff --git a/src/DotTiled/Properties/ColorProperty.cs b/src/DotTiled/Properties/ColorProperty.cs new file mode 100644 index 0000000..0fff029 --- /dev/null +++ b/src/DotTiled/Properties/ColorProperty.cs @@ -0,0 +1,25 @@ +namespace DotTiled; + +/// +/// Represents a color property. +/// +public class ColorProperty : IProperty +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => PropertyType.Color; + + /// + /// The color value of the property. + /// + public required Color Value { get; set; } + + /// + public IProperty Clone() => new ColorProperty + { + Name = Name, + Value = Value + }; +} diff --git a/src/DotTiled/Properties/CustomTypes/CustomClassDefinition.cs b/src/DotTiled/Properties/CustomTypes/CustomClassDefinition.cs new file mode 100644 index 0000000..457a17d --- /dev/null +++ b/src/DotTiled/Properties/CustomTypes/CustomClassDefinition.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; + +namespace DotTiled; + +/// +/// Represents the types of objects that can use a custom class. +/// Uses the FlagsAttribute, for which there is plenty of documentation. +/// +[Flags] +public enum CustomClassUseAs +{ + /// + /// Any property on any kind of object. + /// + Property, + + /// + /// A map. + /// + Map, + + /// + /// A layer. + /// + Layer, + + /// + /// An object. + /// + Object, + + /// + /// A tile. + /// + Tile, + + /// + /// A tileset. + /// + Tileset, + + /// + /// A Wang color. + /// + WangColor, + + /// + /// A Wangset. + /// + Wangset, + + /// + /// A project. + /// + Project, + + /// + /// All types. + /// + All = Property | Map | Layer | Object | Tile | Tileset | WangColor | Wangset | Project +} + +/// +/// Represents a custom class definition in Tiled. Refer to the +/// documentation of custom types to understand how they work. +/// +public class CustomClassDefinition : HasPropertiesBase, ICustomTypeDefinition +{ + /// + public uint ID { get; set; } + + /// + public required string Name { get; set; } + + /// + /// The color of the custom class inside the Tiled editor. + /// + public Color? Color { get; set; } + + /// + /// Whether the custom class should be drawn with a fill color. + /// + public bool DrawFill { get; set; } + + /// + /// What the custom class can be used as, or rather, what types of objects can use it. + /// + public CustomClassUseAs UseAs { get; set; } + + /// + /// The members of the custom class, with their names, types and default values. + /// + public List Members { get; set; } = []; + + /// + public override IList GetProperties() => Members; +} diff --git a/src/DotTiled/Properties/CustomTypes/CustomEnumDefinition.cs b/src/DotTiled/Properties/CustomTypes/CustomEnumDefinition.cs new file mode 100644 index 0000000..e155b38 --- /dev/null +++ b/src/DotTiled/Properties/CustomTypes/CustomEnumDefinition.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; + +namespace DotTiled; + +/// +/// Represents the storage type of a custom enum. +/// +public enum CustomEnumStorageType +{ + /// + /// The backing value is an integer. + /// + Int, + + /// + /// The backing value is a string. + /// + String +} + +/// +/// Represents a custom enum definition in Tiled. Refer to the +/// documentation of custom types to understand how they work. +/// +public class CustomEnumDefinition : ICustomTypeDefinition +{ + /// + public uint ID { get; set; } + + /// + public required string Name { get; set; } + + /// + /// The storage type of the custom enum. + /// + public CustomEnumStorageType StorageType { get; set; } + + /// + /// The values of the custom enum. + /// + public List Values { get; set; } = []; + + /// + /// Whether the value should be treated as flags. + /// + public bool ValueAsFlags { get; set; } +} diff --git a/src/DotTiled/Properties/CustomTypes/CustomTypeDefinition.cs b/src/DotTiled/Properties/CustomTypes/CustomTypeDefinition.cs new file mode 100644 index 0000000..583bc40 --- /dev/null +++ b/src/DotTiled/Properties/CustomTypes/CustomTypeDefinition.cs @@ -0,0 +1,17 @@ +namespace DotTiled; + +/// +/// Base class for custom type definitions. +/// +public interface ICustomTypeDefinition +{ + /// + /// The ID of the custom type. + /// + public uint ID { get; set; } + + /// + /// The name of the custom type. + /// + public string Name { get; set; } +} diff --git a/src/DotTiled/Properties/EnumProperty.cs b/src/DotTiled/Properties/EnumProperty.cs new file mode 100644 index 0000000..a123f9a --- /dev/null +++ b/src/DotTiled/Properties/EnumProperty.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; + +namespace DotTiled; + +/// +/// Represents an enum property. +/// +public class EnumProperty : IProperty> +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => DotTiled.PropertyType.Enum; + + /// + /// The type of the class property. This will be the name of a custom defined + /// type in Tiled. + /// + public required string PropertyType { get; set; } + + /// + /// The value of the enum property. + /// + public required ISet Value { get; set; } + + /// + public IProperty Clone() => new EnumProperty + { + Name = Name, + PropertyType = PropertyType, + Value = Value.ToHashSet() + }; + + /// + /// Determines whether the enum property is equal to the specified value. + /// For enums which have multiple values (e.g. flag enums), this method will only return true if it is the only value. + /// + /// The value to check. + /// True if the enum property is equal to the specified value; otherwise, false. + public bool IsValue(string value) => Value.Contains(value) && Value.Count == 1; + + /// + /// Determines whether the enum property has the specified value. This method is very similar to the common method. + /// + /// The value to check. + /// True if the enum property has the specified value as one of its values; otherwise, false. + public bool HasValue(string value) => Value.Contains(value); +} diff --git a/src/DotTiled/Properties/FileProperty.cs b/src/DotTiled/Properties/FileProperty.cs new file mode 100644 index 0000000..01d4a08 --- /dev/null +++ b/src/DotTiled/Properties/FileProperty.cs @@ -0,0 +1,25 @@ +namespace DotTiled; + +/// +/// Represents a file property. +/// +public class FileProperty : IProperty +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => PropertyType.File; + + /// + /// The value of the property. + /// + public required string Value { get; set; } + + /// + public IProperty Clone() => new FileProperty + { + Name = Name, + Value = Value + }; +} diff --git a/src/DotTiled/Properties/FloatProperty.cs b/src/DotTiled/Properties/FloatProperty.cs new file mode 100644 index 0000000..1652e20 --- /dev/null +++ b/src/DotTiled/Properties/FloatProperty.cs @@ -0,0 +1,25 @@ +namespace DotTiled; + +/// +/// Represents a float property. +/// +public class FloatProperty : IProperty +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => PropertyType.Float; + + /// + /// The float value of the property. + /// + public required float Value { get; set; } + + /// + public IProperty Clone() => new FloatProperty + { + Name = Name, + Value = Value + }; +} diff --git a/src/DotTiled/Properties/IHasProperties.cs b/src/DotTiled/Properties/IHasProperties.cs new file mode 100644 index 0000000..21bf0bb --- /dev/null +++ b/src/DotTiled/Properties/IHasProperties.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace DotTiled; + +/// +/// Interface for objects that have properties attached to them. +/// +public interface IHasProperties +{ + /// + /// The properties attached to the object. + /// + IList GetProperties(); + + /// + /// Tries to get a property of the specified type with the specified name. + /// + /// The type of the property to get. + /// The name of the property to get. + /// The property with the specified name, if found. + /// True if a property with the specified name was found; otherwise, false. + bool TryGetProperty(string name, out T? property) where T : IProperty; + + /// + /// Gets a property of the specified type with the specified name. + /// + /// The type of the property to get. + /// The name of the property to get. + /// The property with the specified name. + T GetProperty(string name) where T : IProperty; +} + +/// +/// Interface for objects that have properties attached to them. +/// +public abstract class HasPropertiesBase : IHasProperties +{ + /// + public abstract IList GetProperties(); + + /// + /// Thrown when a property with the specified name is not found. + /// Thrown when a property with the specified name is not of the specified type. + public T GetProperty(string name) where T : IProperty + { + var properties = GetProperties(); + var property = properties.FirstOrDefault(_properties => _properties.Name == name) ?? throw new KeyNotFoundException($"Property '{name}' not found."); + if (property is T prop) + { + return prop; + } + + throw new InvalidCastException($"Property '{name}' is not of type '{typeof(T).Name}'."); + } + + /// + public bool TryGetProperty(string name, [NotNullWhen(true)] out T? property) where T : IProperty + { + var properties = GetProperties(); + if (properties.FirstOrDefault(_properties => _properties.Name == name) is T prop) + { + property = prop; + return true; + } + + property = default; + return false; + } +} diff --git a/src/DotTiled/Properties/IProperty.cs b/src/DotTiled/Properties/IProperty.cs new file mode 100644 index 0000000..de9eebf --- /dev/null +++ b/src/DotTiled/Properties/IProperty.cs @@ -0,0 +1,36 @@ +namespace DotTiled; + +/// +/// Interface for properties that can be attached to objects, tiles, tilesets, maps etc. +/// +public interface IProperty +{ + /// + /// The name of the property. + /// + public string Name { get; set; } + + /// + /// The type of the property. + /// + public PropertyType Type { get; } + + /// + /// Clones the property, only used for copying properties when performing overriding + /// with templates. + /// + /// An identical, but non-reference-equal, instance of the same property. + IProperty Clone(); +} + +/// +/// Interface for properties that can be attached to objects, tiles, tilesets, maps etc. +/// +/// The type of the property value. +public interface IProperty : IProperty +{ + /// + /// The value of the property. + /// + public T Value { get; set; } +} diff --git a/src/DotTiled/Properties/IntProperty.cs b/src/DotTiled/Properties/IntProperty.cs new file mode 100644 index 0000000..2765038 --- /dev/null +++ b/src/DotTiled/Properties/IntProperty.cs @@ -0,0 +1,25 @@ +namespace DotTiled; + +/// +/// Represents an integer property. +/// +public class IntProperty : IProperty +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => PropertyType.Int; + + /// + /// The integer value of the property. + /// + public required int Value { get; set; } + + /// + public IProperty Clone() => new IntProperty + { + Name = Name, + Value = Value + }; +} diff --git a/src/DotTiled/Properties/ObjectProperty.cs b/src/DotTiled/Properties/ObjectProperty.cs new file mode 100644 index 0000000..a33e401 --- /dev/null +++ b/src/DotTiled/Properties/ObjectProperty.cs @@ -0,0 +1,25 @@ +namespace DotTiled; + +/// +/// Represents an object property. +/// +public class ObjectProperty : IProperty +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => PropertyType.Object; + + /// + /// The object identifier referenced by the property. + /// + public required uint Value { get; set; } + + /// + public IProperty Clone() => new ObjectProperty + { + Name = Name, + Value = Value + }; +} diff --git a/src/DotTiled/Properties/PropertyType.cs b/src/DotTiled/Properties/PropertyType.cs new file mode 100644 index 0000000..a6a31db --- /dev/null +++ b/src/DotTiled/Properties/PropertyType.cs @@ -0,0 +1,52 @@ +namespace DotTiled; + +/// +/// Represents the type of a property. +/// +public enum PropertyType +{ + /// + /// A string property. + /// + String, + + /// + /// An integer property. + /// + Int, + + /// + /// A float property. + /// + Float, + + /// + /// A boolean property. + /// + Bool, + + /// + /// A color property. + /// + Color, + + /// + /// A file property. + /// + File, + + /// + /// An object property. + /// + Object, + + /// + /// A class property. + /// + Class, + + /// + /// An enum property. + /// + Enum +} diff --git a/src/DotTiled/Properties/StringProperty.cs b/src/DotTiled/Properties/StringProperty.cs new file mode 100644 index 0000000..c0c8722 --- /dev/null +++ b/src/DotTiled/Properties/StringProperty.cs @@ -0,0 +1,25 @@ +namespace DotTiled; + +/// +/// Represents a string property. +/// +public class StringProperty : IProperty +{ + /// + public required string Name { get; set; } + + /// + public PropertyType Type => PropertyType.String; + + /// + /// The string value of the property. + /// + public required string Value { get; set; } + + /// + public IProperty Clone() => new StringProperty + { + Name = Name, + Value = Value + }; +} diff --git a/src/DotTiled/Serialization/Helpers.cs b/src/DotTiled/Serialization/Helpers.cs index abfba09..bd839eb 100644 --- a/src/DotTiled/Serialization/Helpers.cs +++ b/src/DotTiled/Serialization/Helpers.cs @@ -3,9 +3,6 @@ using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Tilesets; namespace DotTiled.Serialization; @@ -75,31 +72,52 @@ internal static partial class Helpers }; } - internal static Dictionary MergeProperties(Dictionary? baseProperties, Dictionary? overrideProperties) + internal static List CreateInstanceOfCustomClass( + CustomClassDefinition customClassDefinition, + Func customTypeResolver) + { + return customClassDefinition.Members.Select(x => + { + if (x is ClassProperty cp) + { + return new ClassProperty + { + Name = cp.Name, + PropertyType = cp.PropertyType, + Value = CreateInstanceOfCustomClass((CustomClassDefinition)customTypeResolver(cp.PropertyType), customTypeResolver) + }; + } + + return x.Clone(); + }).ToList(); + } + + internal static IList MergeProperties(IList? baseProperties, IList? overrideProperties) { if (baseProperties is null) - return overrideProperties ?? new Dictionary(); + return overrideProperties ?? []; if (overrideProperties is null) return baseProperties; - var result = baseProperties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Clone()); - foreach (var (key, value) in overrideProperties) + var result = baseProperties.Select(x => x.Clone()).ToList(); + foreach (var overrideProp in overrideProperties) { - if (!result.TryGetValue(key, out var baseProp)) + if (!result.Any(x => x.Name == overrideProp.Name)) { - result[key] = value; + result.Add(overrideProp); continue; } else { - if (value is ClassProperty classProp) + var existingProp = result.First(x => x.Name == overrideProp.Name); + if (existingProp is ClassProperty classProp) { - ((ClassProperty)baseProp).Properties = MergeProperties(((ClassProperty)baseProp).Properties, classProp.Properties); + classProp.Value = MergeProperties(classProp.Value, ((ClassProperty)overrideProp).Value); } else { - result[key] = value; + ReplacePropertyInList(result, overrideProp); } } } @@ -107,6 +125,15 @@ internal static partial class Helpers return result; } + internal static void ReplacePropertyInList(List properties, IProperty property) + { + var index = properties.FindIndex(p => p.Name == property.Name); + if (index == -1) + properties.Add(property); + else + properties[index] = property; + } + internal static void SetAtMostOnce(ref T? field, T value, string fieldName) { if (field is not null) @@ -123,4 +150,6 @@ internal static partial class Helpers field = value; counter++; } + + internal static bool StringIsXml(string s) => s.StartsWith(" +/// Interface for reading a map from some source. Used by the different file format parsers to read a map. +/// public interface IMapReader : IDisposable { + /// + /// Reads a map from the source. + /// + /// The parsed map. Map ReadMap(); } diff --git a/src/DotTiled/Serialization/ITemplateReader.cs b/src/DotTiled/Serialization/ITemplateReader.cs index 45832fa..98901fc 100644 --- a/src/DotTiled/Serialization/ITemplateReader.cs +++ b/src/DotTiled/Serialization/ITemplateReader.cs @@ -1,9 +1,15 @@ using System; -using DotTiled.Model; namespace DotTiled.Serialization; +/// +/// Interface for reading a template from some source. Used by the different file format parsers to read a template. +/// public interface ITemplateReader : IDisposable { + /// + /// Reads a template from the source. + /// + /// The parsed template. Template ReadTemplate(); } diff --git a/src/DotTiled/Serialization/ITilesetReader.cs b/src/DotTiled/Serialization/ITilesetReader.cs index fa4a94c..73fe815 100644 --- a/src/DotTiled/Serialization/ITilesetReader.cs +++ b/src/DotTiled/Serialization/ITilesetReader.cs @@ -1,9 +1,15 @@ using System; -using DotTiled.Model.Tilesets; namespace DotTiled.Serialization; +/// +/// Interface for reading a tileset from some source. Used by the different file format parsers to read a tileset. +/// public interface ITilesetReader : IDisposable { + /// + /// Reads a tileset from the source. + /// + /// The parsed tileset. Tileset ReadTileset(); } diff --git a/src/DotTiled/Serialization/MapReader.cs b/src/DotTiled/Serialization/MapReader.cs new file mode 100644 index 0000000..35341f6 --- /dev/null +++ b/src/DotTiled/Serialization/MapReader.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using System.Xml; +using DotTiled.Serialization.Tmj; +using DotTiled.Serialization.Tmx; + +namespace DotTiled.Serialization; + +/// +/// Reads a map from a string, regardless of format. +/// +public class MapReader : IMapReader +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + private readonly Func _customTypeResolver; + + private readonly StringReader? _mapStringReader; + private readonly XmlReader? _xmlReader; + private readonly IMapReader _mapReader; + private bool disposedValue; + + /// + /// Constructs a new , capable of reading a map from a string, regardless of format. + /// + /// The string containing the map data. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A function that resolves custom types given their source. + /// Thrown when any of the arguments are null. + public MapReader( + string map, + Func externalTilesetResolver, + Func externalTemplateResolver, + Func customTypeResolver) + { + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeResolver = customTypeResolver ?? throw new ArgumentNullException(nameof(customTypeResolver)); + + // Prepare reader + if (Helpers.StringIsXml(map)) + { + _mapStringReader = new StringReader(map); + _xmlReader = XmlReader.Create(_mapStringReader); + _mapReader = new TmxMapReader(_xmlReader, _externalTilesetResolver, _externalTemplateResolver, _customTypeResolver); + } + else + { + _mapReader = new TmjMapReader(map, _externalTilesetResolver, _externalTemplateResolver, _customTypeResolver); + } + } + + /// + public Map ReadMap() => _mapReader.ReadMap(); + + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _mapStringReader?.Dispose(); + _xmlReader?.Dispose(); + _mapReader.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~MapReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/DotTiled/Serialization/TemplateReader.cs b/src/DotTiled/Serialization/TemplateReader.cs new file mode 100644 index 0000000..2fe92ec --- /dev/null +++ b/src/DotTiled/Serialization/TemplateReader.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using System.Xml; +using DotTiled.Serialization.Tmj; +using DotTiled.Serialization.Tmx; + +namespace DotTiled.Serialization; + +/// +/// Reads a template from a string, regardless of format. +/// +public class TemplateReader : ITemplateReader +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + private readonly Func _customTypeResolver; + + private readonly StringReader? _templateStringReader; + private readonly XmlReader? _xmlReader; + private readonly ITemplateReader _templateReader; + private bool disposedValue; + + /// + /// Constructs a new , capable of reading a template from a string, regardless of format. + /// + /// The string containing the template data. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A function that resolves custom types given their source. + /// Thrown when any of the arguments are null. + public TemplateReader( + string template, + Func externalTilesetResolver, + Func externalTemplateResolver, + Func customTypeResolver) + { + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeResolver = customTypeResolver ?? throw new ArgumentNullException(nameof(customTypeResolver)); + + // Prepare reader + if (Helpers.StringIsXml(template)) + { + _templateStringReader = new StringReader(template); + _xmlReader = XmlReader.Create(_templateStringReader); + _templateReader = new TxTemplateReader(_xmlReader, _externalTilesetResolver, _externalTemplateResolver, _customTypeResolver); + } + else + { + _templateReader = new TjTemplateReader(template, _externalTilesetResolver, _externalTemplateResolver, _customTypeResolver); + } + } + + /// + public Template ReadTemplate() => _templateReader.ReadTemplate(); + + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _templateStringReader?.Dispose(); + _xmlReader?.Dispose(); + _templateReader.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~MapReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/DotTiled/Serialization/TilesetReader.cs b/src/DotTiled/Serialization/TilesetReader.cs new file mode 100644 index 0000000..b8ba69b --- /dev/null +++ b/src/DotTiled/Serialization/TilesetReader.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using System.Xml; +using DotTiled.Serialization.Tmj; +using DotTiled.Serialization.Tmx; + +namespace DotTiled.Serialization; + +/// +/// Reads a tileset from a string, regardless of format. +/// +public class TilesetReader : ITilesetReader +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + private readonly Func _customTypeResolver; + + private readonly StringReader? _tilesetStringReader; + private readonly XmlReader? _xmlReader; + private readonly ITilesetReader _tilesetReader; + private bool disposedValue; + + /// + /// Constructs a new , capable of reading a tileset from a string, regardless of format. + /// + /// The string containing the tileset data. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A function that resolves custom types given their source. + /// Thrown when any of the arguments are null. + public TilesetReader( + string tileset, + Func externalTilesetResolver, + Func externalTemplateResolver, + Func customTypeResolver) + { + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeResolver = customTypeResolver ?? throw new ArgumentNullException(nameof(customTypeResolver)); + + // Prepare reader + if (Helpers.StringIsXml(tileset)) + { + _tilesetStringReader = new StringReader(tileset); + _xmlReader = XmlReader.Create(_tilesetStringReader); + _tilesetReader = new TsxTilesetReader(_xmlReader, _externalTilesetResolver, _externalTemplateResolver, _customTypeResolver); + } + else + { + _tilesetReader = new TsjTilesetReader(tileset, _externalTilesetResolver, _externalTemplateResolver, _customTypeResolver); + } + } + + /// + public Tileset ReadTileset() => _tilesetReader.ReadTileset(); + + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _tilesetStringReader?.Dispose(); + _xmlReader?.Dispose(); + _tilesetReader.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~MapReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs b/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs index 69747bb..792bb73 100644 --- a/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs +++ b/src/DotTiled/Serialization/Tmj/TjTemplateReader.cs @@ -1,67 +1,28 @@ using System; -using System.Collections.Generic; -using DotTiled.Model; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; namespace DotTiled.Serialization.Tmj; -public class TjTemplateReader : ITemplateReader +/// +/// A template reader for reading Tiled JSON templates. +/// +public class TjTemplateReader : TmjReaderBase, ITemplateReader { - // External resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly string _jsonString; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - + /// + /// Constructs a new . + /// + /// A string containing a Tiled map in the Tiled JSON format. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A function that resolves custom types given their name. + /// Thrown when any of the arguments are null. public TjTemplateReader( string jsonString, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); - _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); - } + Func customTypeResolver) : base( + jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } - public Template ReadTemplate() - { - var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString); - var rootElement = jsonDoc.RootElement; - return Tmj.ReadTemplate(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~TjTemplateReader() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } + /// + public Template ReadTemplate() => ReadTemplate(RootElement); } diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs b/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs deleted file mode 100644 index d81ce17..0000000 --- a/src/DotTiled/Serialization/Tmj/Tmj.Layer.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Numerics; -using System.Text.Json; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties.CustomTypes; - -namespace DotTiled.Serialization.Tmj; - -internal partial class Tmj -{ - internal static BaseLayer ReadLayer( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - var type = element.GetRequiredProperty("type"); - - return type switch - { - "tilelayer" => ReadTileLayer(element, customTypeDefinitions), - "objectgroup" => ReadObjectLayer(element, externalTemplateResolver, customTypeDefinitions), - "imagelayer" => ReadImageLayer(element, customTypeDefinitions), - "group" => ReadGroup(element, externalTemplateResolver, customTypeDefinitions), - _ => throw new JsonException($"Unsupported layer type '{type}'.") - }; - } -} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs b/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs deleted file mode 100644 index 1ef4944..0000000 --- a/src/DotTiled/Serialization/Tmj/Tmj.Properties.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text.Json; -using DotTiled.Model; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; - -namespace DotTiled.Serialization.Tmj; - -internal partial class Tmj -{ - internal static Dictionary ReadProperties( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) => - element.GetValueAsList(e => - { - var name = e.GetRequiredProperty("name"); - var type = e.GetOptionalPropertyParseable("type", s => s switch - { - "string" => PropertyType.String, - "int" => PropertyType.Int, - "float" => PropertyType.Float, - "bool" => PropertyType.Bool, - "color" => PropertyType.Color, - "file" => PropertyType.File, - "object" => PropertyType.Object, - "class" => PropertyType.Class, - _ => throw new JsonException("Invalid property type") - }, PropertyType.String); - - IProperty property = type switch - { - PropertyType.String => new StringProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable("value") }, - PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty("value") }, - PropertyType.Class => ReadClassProperty(e, customTypeDefinitions), - _ => throw new JsonException("Invalid property type") - }; - - return property!; - }).ToDictionary(p => p.Name); - - internal static ClassProperty ReadClassProperty( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) - { - var name = element.GetRequiredProperty("name"); - var propertyType = element.GetRequiredProperty("propertytype"); - - var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType); - - if (customTypeDef is CustomClassDefinition ccd) - { - var propsInType = CreateInstanceOfCustomClass(ccd); - var props = element.GetOptionalPropertyCustom>("value", el => ReadCustomClassProperties(el, ccd, customTypeDefinitions), []); - - var mergedProps = Helpers.MergeProperties(propsInType, props); - - return new ClassProperty - { - Name = name, - PropertyType = propertyType, - Properties = mergedProps - }; - } - - throw new JsonException($"Unknown custom class '{propertyType}'."); - } - - internal static Dictionary ReadCustomClassProperties( - JsonElement element, - CustomClassDefinition customClassDefinition, - IReadOnlyCollection customTypeDefinitions) - { - Dictionary resultingProps = []; - - foreach (var prop in customClassDefinition.Members) - { - if (!element.TryGetProperty(prop.Name, out var propElement)) - continue; // Property not present in element, therefore will use default value - - IProperty property = prop.Type switch - { - PropertyType.String => new StringProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - PropertyType.Int => new IntProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - PropertyType.Float => new FloatProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - PropertyType.Bool => new BoolProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - PropertyType.Color => new ColorProperty { Name = prop.Name, Value = Color.Parse(propElement.GetValueAs(), CultureInfo.InvariantCulture) }, - PropertyType.File => new FileProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - PropertyType.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs() }, - PropertyType.Class => ReadClassProperty(propElement, customTypeDefinitions), - _ => throw new JsonException("Invalid property type") - }; - - resultingProps[prop.Name] = property; - } - - return resultingProps; - } - - internal static Dictionary CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) - { - return customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone()); - } -} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Template.cs b/src/DotTiled/Serialization/Tmj/Tmj.Template.cs deleted file mode 100644 index 65d6f87..0000000 --- a/src/DotTiled/Serialization/Tmj/Tmj.Template.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Numerics; -using System.Text.Json; -using DotTiled.Model; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; - -namespace DotTiled.Serialization.Tmj; - -internal partial class Tmj -{ - internal static Template ReadTemplate( - JsonElement element, - Func externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - var type = element.GetRequiredProperty("type"); - var tileset = element.GetOptionalPropertyCustom("tileset", el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), null); - var @object = element.GetRequiredPropertyCustom("object", el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)); - - return new Template - { - Tileset = tileset, - Object = @object - }; - } -} diff --git a/src/DotTiled/Serialization/Tmj/TmjMapReader.cs b/src/DotTiled/Serialization/Tmj/TmjMapReader.cs index 3eb1df0..8ceb211 100644 --- a/src/DotTiled/Serialization/Tmj/TmjMapReader.cs +++ b/src/DotTiled/Serialization/Tmj/TmjMapReader.cs @@ -1,70 +1,28 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Text.Json; -using DotTiled.Model; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; namespace DotTiled.Serialization.Tmj; -public class TmjMapReader : IMapReader +/// +/// A map reader for reading Tiled JSON maps. +/// +public class TmjMapReader : TmjReaderBase, IMapReader { - // External resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private string _jsonString; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - + /// + /// Constructs a new . + /// + /// A string containing a Tiled map in the Tiled JSON format. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A function that resolves custom types given their name. + /// Thrown when any of the arguments are null. public TmjMapReader( string jsonString, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); - _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); - } + Func customTypeResolver) : base( + jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } - public Map ReadMap() - { - var jsonDoc = JsonDocument.Parse(_jsonString); - var rootElement = jsonDoc.RootElement; - return Tmj.ReadMap(rootElement, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~TmjMapReader() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - System.GC.SuppressFinalize(this); - } + /// + public Map ReadMap() => ReadMap(RootElement); } diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Data.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Data.cs similarity index 91% rename from src/DotTiled/Serialization/Tmj/Tmj.Data.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Data.cs index ac3d30b..76945ca 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Data.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Data.cs @@ -1,15 +1,10 @@ using System; -using System.Collections.Generic; -using System.Globalization; using System.IO; -using System.IO.Compression; -using System.Linq; using System.Text.Json; -using DotTiled.Model.Layers; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { internal static Data ReadDataAsChunks(JsonElement element, DataCompression? compression, DataEncoding encoding) { @@ -69,7 +64,8 @@ internal partial class Tmj { DataCompression.GZip => Helpers.DecompressGZip(stream), DataCompression.ZLib => Helpers.DecompressZLib(stream), - _ => throw new JsonException($"Unsupported compression '{compression}'.") + DataCompression.ZStd => throw new NotSupportedException("ZStd compression is not supported."), + _ => throw new InvalidOperationException($"Unsupported compression '{compression}'.") }; { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Group.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Group.cs similarity index 66% rename from src/DotTiled/Serialization/Tmj/Tmj.Group.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Group.cs index 159d892..ca08df8 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Group.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Group.cs @@ -1,21 +1,12 @@ -using System; using System.Collections.Generic; using System.Globalization; -using System.Numerics; using System.Text.Json; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static Group ReadGroup( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Group ReadGroup(JsonElement element) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); @@ -27,8 +18,8 @@ internal partial class Tmj var offsetY = element.GetOptionalProperty("offsety", 0.0f); var parallaxX = element.GetOptionalProperty("parallaxx", 1.0f); var parallaxY = element.GetOptionalProperty("parallaxy", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); - var layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); + var layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(ReadLayer), []); return new Group { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ImageLayer.cs similarity index 77% rename from src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.ImageLayer.cs index e3d6112..f115a52 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.ImageLayer.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ImageLayer.cs @@ -1,21 +1,11 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Numerics; using System.Text.Json; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static ImageLayer ReadImageLayer( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal ImageLayer ReadImageLayer(JsonElement element) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); @@ -27,7 +17,7 @@ internal partial class Tmj var offsetY = element.GetOptionalProperty("offsety", 0.0f); var parallaxX = element.GetOptionalProperty("parallaxx", 1.0f); var parallaxY = element.GetOptionalProperty("parallaxy", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var image = element.GetRequiredProperty("image"); var repeatX = element.GetOptionalProperty("repeatx", false); diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.Layer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Layer.cs new file mode 100644 index 0000000..33c42fa --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Layer.cs @@ -0,0 +1,20 @@ +using System.Text.Json; + +namespace DotTiled.Serialization.Tmj; + +public abstract partial class TmjReaderBase +{ + internal BaseLayer ReadLayer(JsonElement element) + { + var type = element.GetRequiredProperty("type"); + + return type switch + { + "tilelayer" => ReadTileLayer(element), + "objectgroup" => ReadObjectLayer(element), + "imagelayer" => ReadImageLayer(element), + "group" => ReadGroup(element), + _ => throw new JsonException($"Unsupported layer type '{type}'.") + }; + } +} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Map.cs similarity index 82% rename from src/DotTiled/Serialization/Tmj/Tmj.Map.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Map.cs index e27dfed..ec45b8d 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Map.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Map.cs @@ -1,24 +1,12 @@ -using System; using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.Linq; using System.Text.Json; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static Map ReadMap( - JsonElement element, - Func? externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Map ReadMap(JsonElement element) { var version = element.GetRequiredProperty("version"); var tiledVersion = element.GetRequiredProperty("tiledversion"); @@ -64,10 +52,10 @@ internal partial class Tmj var nextObjectID = element.GetRequiredProperty("nextobjectid"); var infinite = element.GetOptionalProperty("infinite", false); - var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); - List layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el, externalTemplateResolver, customTypeDefinitions)), []); - List tilesets = element.GetOptionalPropertyCustom>("tilesets", e => e.GetValueAsList(el => ReadTileset(el, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), []); + List layers = element.GetOptionalPropertyCustom>("layers", e => e.GetValueAsList(el => ReadLayer(el)), []); + List tilesets = element.GetOptionalPropertyCustom>("tilesets", e => e.GetValueAsList(el => ReadTileset(el)), []); return new Map { diff --git a/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ObjectLayer.cs similarity index 84% rename from src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.ObjectLayer.cs index 0662fc9..036e8ff 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.ObjectLayer.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.ObjectLayer.cs @@ -1,22 +1,13 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.Numerics; using System.Text.Json; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Layers.Objects; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static ObjectLayer ReadObjectLayer( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal ObjectLayer ReadObjectLayer(JsonElement element) { var id = element.GetRequiredProperty("id"); var name = element.GetRequiredProperty("name"); @@ -28,7 +19,7 @@ internal partial class Tmj var offsetY = element.GetOptionalProperty("offsety", 0.0f); var parallaxX = element.GetOptionalProperty("parallaxx", 1.0f); var parallaxY = element.GetOptionalProperty("parallaxy", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var x = element.GetOptionalProperty("x", 0); var y = element.GetOptionalProperty("y", 0); @@ -42,7 +33,7 @@ internal partial class Tmj _ => throw new JsonException($"Unknown draw order '{s}'.") }, DrawOrder.TopDown); - var objects = element.GetOptionalPropertyCustom>("objects", e => e.GetValueAsList(el => ReadObject(el, externalTemplateResolver, customTypeDefinitions)), []); + var objects = element.GetOptionalPropertyCustom>("objects", e => e.GetValueAsList(el => ReadObject(el)), []); return new ObjectLayer { @@ -67,10 +58,7 @@ internal partial class Tmj }; } - internal static Model.Layers.Objects.Object ReadObject( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal DotTiled.Object ReadObject(JsonElement element) { uint? idDefault = null; string nameDefault = ""; @@ -86,12 +74,12 @@ internal partial class Tmj bool pointDefault = false; List? polygonDefault = null; List? polylineDefault = null; - Dictionary? propertiesDefault = null; + List propertiesDefault = []; var template = element.GetOptionalProperty("template", null); if (template is not null) { - var resolvedTemplate = externalTemplateResolver(template); + var resolvedTemplate = _externalTemplateResolver(template); var templObj = resolvedTemplate.Object; idDefault = templObj.ID; @@ -116,9 +104,9 @@ internal partial class Tmj var id = element.GetOptionalProperty("id", idDefault); var name = element.GetOptionalProperty("name", nameDefault); var point = element.GetOptionalProperty("point", pointDefault); - var polygon = element.GetOptionalPropertyCustom?>("polygon", e => ReadPoints(e), polygonDefault); - var polyline = element.GetOptionalPropertyCustom?>("polyline", e => ReadPoints(e), polylineDefault); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), propertiesDefault); + var polygon = element.GetOptionalPropertyCustom?>("polygon", ReadPoints, polygonDefault); + var polyline = element.GetOptionalPropertyCustom?>("polyline", ReadPoints, polylineDefault); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, propertiesDefault); var rotation = element.GetOptionalProperty("rotation", rotationDefault); var text = element.GetOptionalPropertyCustom("text", ReadText, null); var type = element.GetOptionalProperty("type", typeDefault); @@ -253,12 +241,12 @@ internal partial class Tmj } internal static List ReadPoints(JsonElement element) => - element.GetValueAsList(e => - { - var x = e.GetRequiredProperty("x"); - var y = e.GetRequiredProperty("y"); - return new Vector2(x, y); - }); + element.GetValueAsList(e => + { + var x = e.GetRequiredProperty("x"); + var y = e.GetRequiredProperty("y"); + return new Vector2(x, y); + }); internal static TextObject ReadText(JsonElement element) { diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs new file mode 100644 index 0000000..c8683b2 --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Properties.cs @@ -0,0 +1,173 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json; + +namespace DotTiled.Serialization.Tmj; + +public abstract partial class TmjReaderBase +{ + internal List ReadProperties(JsonElement element) => + element.GetValueAsList(e => + { + var name = e.GetRequiredProperty("name"); + var type = e.GetOptionalPropertyParseable("type", s => s switch + { + "string" => PropertyType.String, + "int" => PropertyType.Int, + "float" => PropertyType.Float, + "bool" => PropertyType.Bool, + "color" => PropertyType.Color, + "file" => PropertyType.File, + "object" => PropertyType.Object, + "class" => PropertyType.Class, + _ => throw new JsonException("Invalid property type") + }, PropertyType.String); + var propertyType = e.GetOptionalProperty("propertytype", null); + if (propertyType is not null) + { + return ReadPropertyWithCustomType(e); + } + + IProperty property = type switch + { + PropertyType.String => new StringProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Int => new IntProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Float => new FloatProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Bool => new BoolProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Color => new ColorProperty { Name = name, Value = e.GetRequiredPropertyParseable("value") }, + PropertyType.File => new FileProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Object => new ObjectProperty { Name = name, Value = e.GetRequiredProperty("value") }, + PropertyType.Class => throw new JsonException("Class property must have a property type"), + PropertyType.Enum => throw new JsonException("Enum property must have a property type"), + _ => throw new JsonException("Invalid property type") + }; + + return property!; + }); + + internal IProperty ReadPropertyWithCustomType(JsonElement element) + { + var isClass = element.GetOptionalProperty("type", null) == "class"; + if (isClass) + { + return ReadClassProperty(element); + } + + return ReadEnumProperty(element); + } + + internal ClassProperty ReadClassProperty(JsonElement element) + { + var name = element.GetRequiredProperty("name"); + var propertyType = element.GetRequiredProperty("propertytype"); + var customTypeDef = _customTypeResolver(propertyType); + + if (customTypeDef is CustomClassDefinition ccd) + { + var propsInType = Helpers.CreateInstanceOfCustomClass(ccd, _customTypeResolver); + var props = element.GetOptionalPropertyCustom>("value", e => ReadPropertiesInsideClass(e, ccd), []); + var mergedProps = Helpers.MergeProperties(propsInType, props); + + return new ClassProperty + { + Name = name, + PropertyType = propertyType, + Value = mergedProps + }; + } + + throw new JsonException($"Unknown custom class '{propertyType}'."); + } + + internal List ReadPropertiesInsideClass( + JsonElement element, + CustomClassDefinition customClassDefinition) + { + List resultingProps = []; + + foreach (var prop in customClassDefinition.Members) + { + if (!element.TryGetProperty(prop.Name, out var propElement)) + continue; + + IProperty property = prop.Type switch + { + PropertyType.String => new StringProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Int => new IntProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Float => new FloatProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Bool => new BoolProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Color => new ColorProperty { Name = prop.Name, Value = Color.Parse(propElement.GetValueAs(), CultureInfo.InvariantCulture) }, + PropertyType.File => new FileProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Object => new ObjectProperty { Name = prop.Name, Value = propElement.GetValueAs() }, + PropertyType.Class => new ClassProperty { Name = prop.Name, PropertyType = ((ClassProperty)prop).PropertyType, Value = ReadPropertiesInsideClass(propElement, (CustomClassDefinition)_customTypeResolver(((ClassProperty)prop).PropertyType)) }, + PropertyType.Enum => ReadEnumProperty(propElement), + _ => throw new JsonException("Invalid property type") + }; + + resultingProps.Add(property); + } + + return resultingProps; + } + + internal EnumProperty ReadEnumProperty(JsonElement element) + { + var name = element.GetRequiredProperty("name"); + var propertyType = element.GetRequiredProperty("propertytype"); + var typeInXml = element.GetOptionalPropertyParseable("type", (s) => s switch + { + "string" => PropertyType.String, + "int" => PropertyType.Int, + _ => throw new JsonException("Invalid property type") + }, PropertyType.String); + var customTypeDef = _customTypeResolver(propertyType); + + if (customTypeDef is not CustomEnumDefinition ced) + throw new JsonException($"Unknown custom enum '{propertyType}'. Enums must be defined"); + + if (ced.StorageType == CustomEnumStorageType.String) + { + var value = element.GetRequiredProperty("value"); + if (value.Contains(',') && !ced.ValueAsFlags) + throw new JsonException("Enum value must not contain ',' if not ValueAsFlags is set to true."); + + if (ced.ValueAsFlags) + { + var values = value.Split(',').Select(v => v.Trim()).ToHashSet(); + return new EnumProperty { Name = name, PropertyType = propertyType, Value = values }; + } + else + { + return new EnumProperty { Name = name, PropertyType = propertyType, Value = new HashSet { value } }; + } + } + else if (ced.StorageType == CustomEnumStorageType.Int) + { + var value = element.GetRequiredProperty("value"); + if (ced.ValueAsFlags) + { + var allValues = ced.Values; + var enumValues = new HashSet(); + for (var i = 0; i < allValues.Count; i++) + { + var mask = 1 << i; + if ((value & mask) == mask) + { + var enumValue = allValues[i]; + _ = enumValues.Add(enumValue); + } + } + return new EnumProperty { Name = name, PropertyType = propertyType, Value = enumValues }; + } + else + { + var allValues = ced.Values; + var enumValue = allValues[value]; + return new EnumProperty { Name = name, PropertyType = propertyType, Value = new HashSet { enumValue } }; + } + } + + throw new JsonException($"Unknown custom enum '{propertyType}'. Enums must be defined"); + } +} diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.Template.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Template.cs new file mode 100644 index 0000000..88bcf09 --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Template.cs @@ -0,0 +1,19 @@ +using System.Text.Json; + +namespace DotTiled.Serialization.Tmj; + +public abstract partial class TmjReaderBase +{ + internal Template ReadTemplate(JsonElement element) + { + var type = element.GetRequiredProperty("type"); + var tileset = element.GetOptionalPropertyCustom("tileset", ReadTileset, null); + var @object = element.GetRequiredPropertyCustom("object", ReadObject); + + return new Template + { + Tileset = tileset, + Object = @object + }; + } +} diff --git a/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.TileLayer.cs similarity index 84% rename from src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.TileLayer.cs index d2fd53d..e3ccc9a 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.TileLayer.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.TileLayer.cs @@ -1,20 +1,11 @@ -using System; -using System.Collections.Generic; using System.Globalization; -using System.Numerics; using System.Text.Json; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static TileLayer ReadTileLayer( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal TileLayer ReadTileLayer(JsonElement element) { var compression = element.GetOptionalPropertyParseable("compression", s => s switch { @@ -40,7 +31,7 @@ internal partial class Tmj var opacity = element.GetOptionalProperty("opacity", 1.0f); var parallaxx = element.GetOptionalProperty("parallaxx", 1.0f); var parallaxy = element.GetOptionalProperty("parallaxy", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var repeatX = element.GetOptionalProperty("repeatx", false); var repeatY = element.GetOptionalProperty("repeaty", false); var startX = element.GetOptionalProperty("startx", 0); diff --git a/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Tileset.cs similarity index 81% rename from src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs rename to src/DotTiled/Serialization/Tmj/TmjReaderBase.Tileset.cs index 8b206c9..c19e3f6 100644 --- a/src/DotTiled/Serialization/Tmj/Tmj.Tileset.cs +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.Tileset.cs @@ -1,25 +1,12 @@ -using System; using System.Collections.Generic; using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Linq; using System.Text.Json; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; namespace DotTiled.Serialization.Tmj; -internal partial class Tmj +public abstract partial class TmjReaderBase { - internal static Tileset ReadTileset( - JsonElement element, - Func? externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) + internal Tileset ReadTileset(JsonElement element) { var backgroundColor = element.GetOptionalPropertyParseable("backgroundcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var @class = element.GetOptionalProperty("class", ""); @@ -51,7 +38,7 @@ internal partial class Tmj "bottomright" => ObjectAlignment.BottomRight, _ => throw new JsonException($"Unknown object alignment '{s}'") }, ObjectAlignment.Unspecified); - var properties = element.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var source = element.GetOptionalProperty("source", null); var spacing = element.GetOptionalProperty("spacing", null); var tileCount = element.GetOptionalProperty("tilecount", null); @@ -64,20 +51,17 @@ internal partial class Tmj "grid" => TileRenderSize.Grid, _ => throw new JsonException($"Unknown tile render size '{s}'") }, TileRenderSize.Tile); - var tiles = element.GetOptionalPropertyCustom>("tiles", el => ReadTiles(el, externalTemplateResolver, customTypeDefinitions), []); + var tiles = element.GetOptionalPropertyCustom>("tiles", ReadTiles, []); var tileWidth = element.GetOptionalProperty("tilewidth", null); var transparentColor = element.GetOptionalPropertyParseable("transparentcolor", s => Color.Parse(s, CultureInfo.InvariantCulture), null); var type = element.GetOptionalProperty("type", null); var version = element.GetOptionalProperty("version", null); var transformations = element.GetOptionalPropertyCustom("transformations", ReadTransformations, null); - var wangsets = element.GetOptionalPropertyCustom?>("wangsets", el => el.GetValueAsList(e => ReadWangset(e, customTypeDefinitions)), null); + var wangsets = element.GetOptionalPropertyCustom?>("wangsets", el => el.GetValueAsList(e => ReadWangset(e)), null); if (source is not null) { - if (externalTilesetResolver is null) - throw new JsonException("External tileset resolver is required to resolve external tilesets."); - - var resolvedTileset = externalTilesetResolver(source); + var resolvedTileset = _externalTilesetResolver(source); resolvedTileset.FirstGID = firstGID; resolvedTileset.Source = source; return resolvedTileset; @@ -166,10 +150,7 @@ internal partial class Tmj }; } - internal static List ReadTiles( - JsonElement element, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) => + internal List ReadTiles(JsonElement element) => element.GetValueAsList(e => { var animation = e.GetOptionalPropertyCustom?>("animation", e => e.GetValueAsList(ReadFrame), null); @@ -181,9 +162,9 @@ internal partial class Tmj var y = e.GetOptionalProperty("y", 0); var width = e.GetOptionalProperty("width", imageWidth ?? 0); var height = e.GetOptionalProperty("height", imageHeight ?? 0); - var objectGroup = e.GetOptionalPropertyCustom("objectgroup", e => ReadObjectLayer(e, externalTemplateResolver, customTypeDefinitions), null); + var objectGroup = e.GetOptionalPropertyCustom("objectgroup", e => ReadObjectLayer(e), null); var probability = e.GetOptionalProperty("probability", 0.0f); - var properties = e.GetOptionalPropertyCustom?>("properties", el => ReadProperties(el, customTypeDefinitions), null); + var properties = e.GetOptionalPropertyCustom("properties", ReadProperties, []); // var terrain, replaced by wangsets var type = e.GetOptionalProperty("type", ""); @@ -223,14 +204,12 @@ internal partial class Tmj }; } - internal static Wangset ReadWangset( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal Wangset ReadWangset(JsonElement element) { var @clalss = element.GetOptionalProperty("class", ""); - var colors = element.GetOptionalPropertyCustom>("colors", e => e.GetValueAsList(el => ReadWangColor(el, customTypeDefinitions)), []); + var colors = element.GetOptionalPropertyCustom>("colors", e => e.GetValueAsList(el => ReadWangColor(el)), []); var name = element.GetRequiredProperty("name"); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var tile = element.GetOptionalProperty("tile", 0); var type = element.GetOptionalProperty("type", ""); var wangTiles = element.GetOptionalPropertyCustom>("wangtiles", e => e.GetValueAsList(ReadWangTile), []); @@ -246,15 +225,13 @@ internal partial class Tmj }; } - internal static WangColor ReadWangColor( - JsonElement element, - IReadOnlyCollection customTypeDefinitions) + internal WangColor ReadWangColor(JsonElement element) { var @class = element.GetOptionalProperty("class", ""); var color = element.GetRequiredPropertyParseable("color", s => Color.Parse(s, CultureInfo.InvariantCulture)); var name = element.GetRequiredProperty("name"); var probability = element.GetOptionalProperty("probability", 1.0f); - var properties = element.GetOptionalPropertyCustom?>("properties", e => ReadProperties(e, customTypeDefinitions), null); + var properties = element.GetOptionalPropertyCustom("properties", ReadProperties, []); var tile = element.GetOptionalProperty("tile", 0); return new WangColor diff --git a/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs b/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs new file mode 100644 index 0000000..f36ca88 --- /dev/null +++ b/src/DotTiled/Serialization/Tmj/TmjReaderBase.cs @@ -0,0 +1,73 @@ +using System; +using System.Text.Json; + +namespace DotTiled.Serialization.Tmj; + +/// +/// Base class for Tiled JSON format readers. +/// +public abstract partial class TmjReaderBase : IDisposable +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + private readonly Func _customTypeResolver; + + /// + /// The root element of the JSON document being read. + /// + protected JsonElement RootElement { get; private set; } + + private bool disposedValue; + + /// + /// Constructs a new . + /// + /// A string containing a Tiled map in the Tiled JSON format. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A collection of custom type definitions that can be used to resolve custom types when encountering . + /// Thrown when any of the arguments are null. + protected TmjReaderBase( + string jsonString, + Func externalTilesetResolver, + Func externalTemplateResolver, + Func customTypeResolver) + { + RootElement = JsonDocument.Parse(jsonString ?? throw new ArgumentNullException(nameof(jsonString))).RootElement; + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeResolver = customTypeResolver ?? throw new ArgumentNullException(nameof(customTypeResolver)); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TmjMapReader() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs b/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs index a41adfe..5816c8a 100644 --- a/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs +++ b/src/DotTiled/Serialization/Tmj/TsjTilesetReader.cs @@ -1,68 +1,28 @@ using System; -using System.Collections.Generic; -using DotTiled.Model; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; namespace DotTiled.Serialization.Tmj; -public class TsjTilesetReader : ITilesetReader +/// +/// A tileset reader for the Tiled JSON format. +/// +public class TsjTilesetReader : TmjReaderBase, ITilesetReader { - // External resolvers - private readonly Func _externalTemplateResolver; - - private readonly string _jsonString; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - + /// + /// Constructs a new . + /// + /// A string containing a Tiled map in the Tiled JSON format. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A function that resolves custom types given their name. + /// Thrown when any of the arguments are null. public TsjTilesetReader( string jsonString, + Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _jsonString = jsonString ?? throw new ArgumentNullException(nameof(jsonString)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); - } + Func customTypeResolver) : base( + jsonString, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } - public Tileset ReadTileset() - { - var jsonDoc = System.Text.Json.JsonDocument.Parse(_jsonString); - var rootElement = jsonDoc.RootElement; - return Tmj.ReadTileset( - rootElement, - _ => throw new NotSupportedException("External tilesets cannot refer to other external tilesets."), - _externalTemplateResolver, - _customTypeDefinitions); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~TsjTilesetReader() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - System.GC.SuppressFinalize(this); - } + /// + public Tileset ReadTileset() => ReadTileset(RootElement); } diff --git a/src/DotTiled/Serialization/Tmx/ExtensionsXmlReader.cs b/src/DotTiled/Serialization/Tmx/ExtensionsXmlReader.cs index ca5f836..9a8f1d9 100644 --- a/src/DotTiled/Serialization/Tmx/ExtensionsXmlReader.cs +++ b/src/DotTiled/Serialization/Tmx/ExtensionsXmlReader.cs @@ -31,10 +31,8 @@ internal static class ExtensionsXmlReader return enumParser(value); } - internal static string? GetOptionalAttribute(this XmlReader reader, string attribute, string? defaultValue = default) - { - return reader.GetAttribute(attribute) ?? defaultValue; - } + internal static string? GetOptionalAttribute(this XmlReader reader, string attribute, string? defaultValue = default) => + reader.GetAttribute(attribute) ?? defaultValue; internal static T? GetOptionalAttributeParseable(this XmlReader reader, string attribute) where T : struct, IParsable { @@ -84,7 +82,7 @@ internal static class ExtensionsXmlReader if (reader.NodeType == XmlNodeType.EndElement) continue; // At end of list, no need to read again - reader.Read(); + _ = reader.Read(); } reader.ReadEndElement(); @@ -135,6 +133,6 @@ internal static class ExtensionsXmlReader internal static void SkipXmlDeclaration(this XmlReader reader) { if (reader.NodeType == XmlNodeType.XmlDeclaration) - reader.Read(); + _ = reader.Read(); } } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs b/src/DotTiled/Serialization/Tmx/Tmx.Map.cs deleted file mode 100644 index 2152852..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Map.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Xml; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static Map ReadMap( - XmlReader reader, - Func externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var version = reader.GetRequiredAttribute("version"); - var tiledVersion = reader.GetRequiredAttribute("tiledversion"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var orientation = reader.GetRequiredAttributeEnum("orientation", s => s switch - { - "orthogonal" => MapOrientation.Orthogonal, - "isometric" => MapOrientation.Isometric, - "staggered" => MapOrientation.Staggered, - "hexagonal" => MapOrientation.Hexagonal, - _ => throw new Exception($"Unknown orientation '{s}'") - }); - var renderOrder = reader.GetOptionalAttributeEnum("renderorder", s => s switch - { - "right-down" => RenderOrder.RightDown, - "right-up" => RenderOrder.RightUp, - "left-down" => RenderOrder.LeftDown, - "left-up" => RenderOrder.LeftUp, - _ => throw new Exception($"Unknown render order '{s}'") - }) ?? RenderOrder.RightDown; - var compressionLevel = reader.GetOptionalAttributeParseable("compressionlevel") ?? -1; - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); - var tileWidth = reader.GetRequiredAttributeParseable("tilewidth"); - var tileHeight = reader.GetRequiredAttributeParseable("tileheight"); - var hexSideLength = reader.GetOptionalAttributeParseable("hexsidelength"); - var staggerAxis = reader.GetOptionalAttributeEnum("staggeraxis", s => s switch - { - "x" => StaggerAxis.X, - "y" => StaggerAxis.Y, - _ => throw new Exception($"Unknown stagger axis '{s}'") - }); - var staggerIndex = reader.GetOptionalAttributeEnum("staggerindex", s => s switch - { - "odd" => StaggerIndex.Odd, - "even" => StaggerIndex.Even, - _ => throw new Exception($"Unknown stagger index '{s}'") - }); - var parallaxOriginX = reader.GetOptionalAttributeParseable("parallaxoriginx") ?? 0.0f; - var parallaxOriginY = reader.GetOptionalAttributeParseable("parallaxoriginy") ?? 0.0f; - var backgroundColor = reader.GetOptionalAttributeClass("backgroundcolor") ?? Color.Parse("#00000000", CultureInfo.InvariantCulture); - var nextLayerID = reader.GetRequiredAttributeParseable("nextlayerid"); - var nextObjectID = reader.GetRequiredAttributeParseable("nextobjectid"); - var infinite = (reader.GetOptionalAttributeParseable("infinite") ?? 0) == 1; - - // At most one of - Dictionary? properties = null; - - // Any number of - List layers = []; - List tilesets = []; - - reader.ProcessChildren("map", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "tileset" => () => tilesets.Add(ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions)), - "layer" => () => layers.Add(ReadTileLayer(r, infinite, customTypeDefinitions)), - "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)), - "imagelayer" => () => layers.Add(ReadImageLayer(r, customTypeDefinitions)), - "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver, customTypeDefinitions)), - _ => r.Skip - }); - - return new Map - { - Version = version, - TiledVersion = tiledVersion, - Class = @class, - Orientation = orientation, - RenderOrder = renderOrder, - CompressionLevel = compressionLevel, - Width = width, - Height = height, - TileWidth = tileWidth, - TileHeight = tileHeight, - HexSideLength = hexSideLength, - StaggerAxis = staggerAxis, - StaggerIndex = staggerIndex, - ParallaxOriginX = parallaxOriginX, - ParallaxOriginY = parallaxOriginY, - BackgroundColor = backgroundColor, - NextLayerID = nextLayerID, - NextObjectID = nextObjectID, - Infinite = infinite, - Properties = properties, - Tilesets = tilesets, - Layers = layers - }; - } -} diff --git a/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs b/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs deleted file mode 100644 index 5f5bda0..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.ObjectLayer.cs +++ /dev/null @@ -1,342 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Numerics; -using System.Xml; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Layers.Objects; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static ObjectLayer ReadObjectLayer( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var id = reader.GetRequiredAttributeParseable("id"); - var name = reader.GetOptionalAttribute("name") ?? ""; - var @class = reader.GetOptionalAttribute("class") ?? ""; - var x = reader.GetOptionalAttributeParseable("x") ?? 0; - var y = reader.GetOptionalAttributeParseable("y") ?? 0; - var width = reader.GetOptionalAttributeParseable("width"); - var height = reader.GetOptionalAttributeParseable("height"); - var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; - var visible = reader.GetOptionalAttributeParseable("visible") ?? true; - var tintColor = reader.GetOptionalAttributeClass("tintcolor"); - var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; - var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; - var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; - var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - var color = reader.GetOptionalAttributeClass("color"); - var drawOrder = reader.GetOptionalAttributeEnum("draworder", s => s switch - { - "topdown" => DrawOrder.TopDown, - "index" => DrawOrder.Index, - _ => throw new Exception($"Unknown draw order '{s}'") - }) ?? DrawOrder.TopDown; - - // Elements - Dictionary? properties = null; - List objects = []; - - reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "object" => () => objects.Add(ReadObject(r, externalTemplateResolver, customTypeDefinitions)), - _ => r.Skip - }); - - return new ObjectLayer - { - ID = id, - Name = name, - Class = @class, - X = x, - Y = y, - Width = width, - Height = height, - Opacity = opacity, - Visible = visible, - TintColor = tintColor, - OffsetX = offsetX, - OffsetY = offsetY, - ParallaxX = parallaxX, - ParallaxY = parallaxY, - Color = color, - Properties = properties, - DrawOrder = drawOrder, - Objects = objects - }; - } - - internal static Model.Layers.Objects.Object ReadObject( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var template = reader.GetOptionalAttribute("template"); - Model.Layers.Objects.Object? obj = null; - if (template is not null) - obj = externalTemplateResolver(template).Object; - - uint? idDefault = obj?.ID ?? null; - string nameDefault = obj?.Name ?? ""; - string typeDefault = obj?.Type ?? ""; - float xDefault = obj?.X ?? 0f; - float yDefault = obj?.Y ?? 0f; - float widthDefault = obj?.Width ?? 0f; - float heightDefault = obj?.Height ?? 0f; - float rotationDefault = obj?.Rotation ?? 0f; - uint? gidDefault = obj is TileObject tileObj ? tileObj.GID : null; - bool visibleDefault = obj?.Visible ?? true; - Dictionary? propertiesDefault = obj?.Properties ?? null; - - var id = reader.GetOptionalAttributeParseable("id") ?? idDefault; - var name = reader.GetOptionalAttribute("name") ?? nameDefault; - var type = reader.GetOptionalAttribute("type") ?? typeDefault; - var x = reader.GetOptionalAttributeParseable("x") ?? xDefault; - var y = reader.GetOptionalAttributeParseable("y") ?? yDefault; - var width = reader.GetOptionalAttributeParseable("width") ?? widthDefault; - var height = reader.GetOptionalAttributeParseable("height") ?? heightDefault; - var rotation = reader.GetOptionalAttributeParseable("rotation") ?? rotationDefault; - var gid = reader.GetOptionalAttributeParseable("gid") ?? gidDefault; - var visible = reader.GetOptionalAttributeParseable("visible") ?? visibleDefault; - - // Elements - Model.Layers.Objects.Object? foundObject = null; - int propertiesCounter = 0; - Dictionary? properties = propertiesDefault; - - reader.ProcessChildren("object", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties(r, customTypeDefinitions)), "Properties", ref propertiesCounter), - "ellipse" => () => Helpers.SetAtMostOnce(ref foundObject, ReadEllipseObject(r), "Object marker"), - "point" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPointObject(r), "Object marker"), - "polygon" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolygonObject(r), "Object marker"), - "polyline" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolylineObject(r), "Object marker"), - "text" => () => Helpers.SetAtMostOnce(ref foundObject, ReadTextObject(r), "Object marker"), - _ => throw new Exception($"Unknown object marker '{elementName}'") - }); - - if (foundObject is null) - { - if (gid is not null) - foundObject = new TileObject { ID = id, GID = gid.Value }; - else - foundObject = new RectangleObject { ID = id }; - } - - foundObject.ID = id; - foundObject.Name = name; - foundObject.Type = type; - foundObject.X = x; - foundObject.Y = y; - foundObject.Width = width; - foundObject.Height = height; - foundObject.Rotation = rotation; - foundObject.Visible = visible; - foundObject.Properties = properties; - foundObject.Template = template; - - return OverrideObject(obj, foundObject); - } - - internal static Model.Layers.Objects.Object OverrideObject(Model.Layers.Objects.Object? obj, Model.Layers.Objects.Object foundObject) - { - if (obj is null) - return foundObject; - - 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); - obj.Template = foundObject.Template; - return obj; - } - - return OverrideObject((dynamic)obj, (dynamic)foundObject); - } - - internal static EllipseObject ReadEllipseObject(XmlReader reader) - { - reader.Skip(); - return new EllipseObject { }; - } - - internal static EllipseObject OverrideObject(EllipseObject obj, EllipseObject foundObject) => obj; - - internal static PointObject ReadPointObject(XmlReader reader) - { - reader.Skip(); - return new PointObject { }; - } - - internal static PointObject OverrideObject(PointObject obj, PointObject foundObject) => obj; - - internal static PolygonObject ReadPolygonObject(XmlReader reader) - { - // Attributes - var points = reader.GetRequiredAttributeParseable>("points", s => - { - // Takes on format "x1,y1 x2,y2 x3,y3 ..." - var coords = s.Split(' '); - return coords.Select(c => - { - var xy = c.Split(','); - return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture)); - }).ToList(); - }); - - reader.ReadStartElement("polygon"); - return new PolygonObject { Points = points }; - } - - internal static PolygonObject OverrideObject(PolygonObject obj, PolygonObject foundObject) - { - obj.Points = foundObject.Points; - return obj; - } - - internal static PolylineObject ReadPolylineObject(XmlReader reader) - { - // Attributes - var points = reader.GetRequiredAttributeParseable>("points", s => - { - // Takes on format "x1,y1 x2,y2 x3,y3 ..." - var coords = s.Split(' '); - return coords.Select(c => - { - var xy = c.Split(','); - return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture)); - }).ToList(); - }); - - reader.ReadStartElement("polyline"); - return new PolylineObject { Points = points }; - } - - internal static PolylineObject OverrideObject(PolylineObject obj, PolylineObject foundObject) - { - obj.Points = foundObject.Points; - return obj; - } - - internal static TextObject ReadTextObject(XmlReader reader) - { - // Attributes - var fontFamily = reader.GetOptionalAttribute("fontfamily") ?? "sans-serif"; - var pixelSize = reader.GetOptionalAttributeParseable("pixelsize") ?? 16; - var wrap = reader.GetOptionalAttributeParseable("wrap") ?? false; - var color = reader.GetOptionalAttributeClass("color") ?? Color.Parse("#000000", CultureInfo.InvariantCulture); - var bold = reader.GetOptionalAttributeParseable("bold") ?? false; - var italic = reader.GetOptionalAttributeParseable("italic") ?? false; - var underline = reader.GetOptionalAttributeParseable("underline") ?? false; - var strikeout = reader.GetOptionalAttributeParseable("strikeout") ?? false; - var kerning = reader.GetOptionalAttributeParseable("kerning") ?? true; - var hAlign = reader.GetOptionalAttributeEnum("halign", s => s switch - { - "left" => TextHorizontalAlignment.Left, - "center" => TextHorizontalAlignment.Center, - "right" => TextHorizontalAlignment.Right, - "justify" => TextHorizontalAlignment.Justify, - _ => throw new Exception($"Unknown horizontal alignment '{s}'") - }) ?? TextHorizontalAlignment.Left; - var vAlign = reader.GetOptionalAttributeEnum("valign", s => s switch - { - "top" => TextVerticalAlignment.Top, - "center" => TextVerticalAlignment.Center, - "bottom" => TextVerticalAlignment.Bottom, - _ => throw new Exception($"Unknown vertical alignment '{s}'") - }) ?? TextVerticalAlignment.Top; - - // Elements - var text = reader.ReadElementContentAsString("text", ""); - - return new TextObject - { - FontFamily = fontFamily, - PixelSize = pixelSize, - Wrap = wrap, - Color = color, - Bold = bold, - Italic = italic, - Underline = underline, - Strikeout = strikeout, - Kerning = kerning, - HorizontalAlignment = hAlign, - VerticalAlignment = vAlign, - Text = text - }; - } - - internal static TextObject OverrideObject(TextObject obj, TextObject foundObject) - { - obj.FontFamily = foundObject.FontFamily; - obj.PixelSize = foundObject.PixelSize; - obj.Wrap = foundObject.Wrap; - obj.Color = foundObject.Color; - obj.Bold = foundObject.Bold; - obj.Italic = foundObject.Italic; - obj.Underline = foundObject.Underline; - obj.Strikeout = foundObject.Strikeout; - obj.Kerning = foundObject.Kerning; - obj.HorizontalAlignment = foundObject.HorizontalAlignment; - obj.VerticalAlignment = foundObject.VerticalAlignment; - obj.Text = foundObject.Text; - return obj; - } - - internal static TileObject OverrideObject(TileObject obj, TileObject foundObject) - { - obj.GID = foundObject.GID; - return obj; - } - - internal static Template ReadTemplate( - XmlReader reader, - Func externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // No attributes - - // At most one of - Tileset? tileset = null; - - // Should contain exactly one of - Model.Layers.Objects.Object? obj = null; - - reader.ProcessChildren("template", (r, elementName) => elementName switch - { - "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(r, externalTilesetResolver, externalTemplateResolver, customTypeDefinitions), "Tileset"), - "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(r, externalTemplateResolver, customTypeDefinitions), "Object"), - _ => r.Skip - }); - - if (obj is null) - throw new NotSupportedException("Template must contain exactly one object"); - - return new Template - { - Tileset = tileset, - Object = obj - }; - } -} diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs b/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs deleted file mode 100644 index 7f62358..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Properties.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Xml; -using DotTiled.Model; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static Dictionary ReadProperties( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - return reader.ReadList("properties", "property", (r) => - { - var name = r.GetRequiredAttribute("name"); - var type = r.GetOptionalAttributeEnum("type", (s) => s switch - { - "string" => PropertyType.String, - "int" => PropertyType.Int, - "float" => PropertyType.Float, - "bool" => PropertyType.Bool, - "color" => PropertyType.Color, - "file" => PropertyType.File, - "object" => PropertyType.Object, - "class" => PropertyType.Class, - _ => throw new XmlException("Invalid property type") - }) ?? PropertyType.String; - - IProperty property = type switch - { - PropertyType.String => new StringProperty { Name = name, Value = r.GetRequiredAttribute("value") }, - PropertyType.Int => new IntProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") }, - PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, - PropertyType.Class => ReadClassProperty(r, customTypeDefinitions), - _ => throw new XmlException("Invalid property type") - }; - return (name, property); - }).ToDictionary(x => x.name, x => x.property); - } - - internal static ClassProperty ReadClassProperty( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - var name = reader.GetRequiredAttribute("name"); - var propertyType = reader.GetRequiredAttribute("propertytype"); - - var customTypeDef = customTypeDefinitions.FirstOrDefault(ctd => ctd.Name == propertyType); - if (customTypeDef is CustomClassDefinition ccd) - { - reader.ReadStartElement("property"); - var propsInType = CreateInstanceOfCustomClass(ccd); - var props = ReadProperties(reader, customTypeDefinitions); - - var mergedProps = Helpers.MergeProperties(propsInType, props); - - reader.ReadEndElement(); - return new ClassProperty { Name = name, PropertyType = propertyType, Properties = mergedProps }; - } - - throw new XmlException($"Unkonwn custom class definition: {propertyType}"); - } - - internal static Dictionary CreateInstanceOfCustomClass(CustomClassDefinition customClassDefinition) - { - return customClassDefinition.Members.ToDictionary(m => m.Name, m => m.Clone()); - } -} diff --git a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs b/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs deleted file mode 100644 index 41e720b..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.TileLayer.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Xml; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static TileLayer ReadTileLayer( - XmlReader reader, - bool dataUsesChunks, - IReadOnlyCollection customTypeDefinitions) - { - var id = reader.GetRequiredAttributeParseable("id"); - var name = reader.GetOptionalAttribute("name") ?? ""; - var @class = reader.GetOptionalAttribute("class") ?? ""; - var x = reader.GetOptionalAttributeParseable("x") ?? 0; - var y = reader.GetOptionalAttributeParseable("y") ?? 0; - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); - var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; - var visible = reader.GetOptionalAttributeParseable("visible") ?? true; - var tintColor = reader.GetOptionalAttributeClass("tintcolor"); - var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; - var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; - var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; - var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - - Dictionary? properties = null; - Data? data = null; - - reader.ProcessChildren("layer", (r, elementName) => elementName switch - { - "data" => () => Helpers.SetAtMostOnce(ref data, ReadData(r, dataUsesChunks), "Data"), - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - _ => r.Skip - }); - - return new TileLayer - { - ID = id, - Name = name, - Class = @class, - X = x, - Y = y, - Width = width, - Height = height, - Opacity = opacity, - Visible = visible, - TintColor = tintColor, - OffsetX = offsetX, - OffsetY = offsetY, - ParallaxX = parallaxX, - ParallaxY = parallaxY, - Data = data, - Properties = properties - }; - } - - internal static ImageLayer ReadImageLayer( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - var id = reader.GetRequiredAttributeParseable("id"); - var name = reader.GetOptionalAttribute("name") ?? ""; - var @class = reader.GetOptionalAttribute("class") ?? ""; - var x = reader.GetOptionalAttributeParseable("x") ?? 0; - var y = reader.GetOptionalAttributeParseable("y") ?? 0; - var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; - var visible = reader.GetOptionalAttributeParseable("visible") ?? true; - var tintColor = reader.GetOptionalAttributeClass("tintcolor"); - var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; - var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; - var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; - var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - var repeatX = (reader.GetOptionalAttributeParseable("repeatx") ?? 0) == 1; - var repeatY = (reader.GetOptionalAttributeParseable("repeaty") ?? 0) == 1; - - Dictionary? properties = null; - Image? image = null; - - reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch - { - "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - _ => r.Skip - }); - - return new ImageLayer - { - ID = id, - Name = name, - Class = @class, - X = x, - Y = y, - Opacity = opacity, - Visible = visible, - TintColor = tintColor, - OffsetX = offsetX, - OffsetY = offsetY, - ParallaxX = parallaxX, - ParallaxY = parallaxY, - Properties = properties, - Image = image, - RepeatX = repeatX, - RepeatY = repeatY - }; - } - - internal static Group ReadGroup( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - var id = reader.GetRequiredAttributeParseable("id"); - var name = reader.GetOptionalAttribute("name") ?? ""; - var @class = reader.GetOptionalAttribute("class") ?? ""; - var opacity = reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; - var visible = reader.GetOptionalAttributeParseable("visible") ?? true; - var tintColor = reader.GetOptionalAttributeClass("tintcolor"); - var offsetX = reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; - var offsetY = reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; - var parallaxX = reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; - var parallaxY = reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; - - Dictionary? properties = null; - List layers = []; - - reader.ProcessChildren("group", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "layer" => () => layers.Add(ReadTileLayer(r, false, customTypeDefinitions)), - "objectgroup" => () => layers.Add(ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions)), - "imagelayer" => () => layers.Add(ReadImageLayer(r, customTypeDefinitions)), - "group" => () => layers.Add(ReadGroup(r, externalTemplateResolver, customTypeDefinitions)), - _ => r.Skip - }); - - return new Group - { - ID = id, - Name = name, - Class = @class, - Opacity = opacity, - Visible = visible, - TintColor = tintColor, - OffsetX = offsetX, - OffsetY = offsetY, - ParallaxX = parallaxX, - ParallaxY = parallaxY, - Properties = properties, - Layers = layers - }; - } -} diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs b/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs deleted file mode 100644 index 5d8aac6..0000000 --- a/src/DotTiled/Serialization/Tmx/Tmx.Tileset.cs +++ /dev/null @@ -1,355 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Xml; -using DotTiled.Model; -using DotTiled.Model.Layers; -using DotTiled.Model.Properties; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; - -namespace DotTiled.Serialization.Tmx; - -internal partial class Tmx -{ - internal static Tileset ReadTileset( - XmlReader reader, - Func? externalTilesetResolver, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var version = reader.GetOptionalAttribute("version"); - var tiledVersion = reader.GetOptionalAttribute("tiledversion"); - var firstGID = reader.GetOptionalAttributeParseable("firstgid"); - var source = reader.GetOptionalAttribute("source"); - var name = reader.GetOptionalAttribute("name"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var tileWidth = reader.GetOptionalAttributeParseable("tilewidth"); - var tileHeight = reader.GetOptionalAttributeParseable("tileheight"); - var spacing = reader.GetOptionalAttributeParseable("spacing") ?? 0; - var margin = reader.GetOptionalAttributeParseable("margin") ?? 0; - var tileCount = reader.GetOptionalAttributeParseable("tilecount"); - var columns = reader.GetOptionalAttributeParseable("columns"); - var objectAlignment = reader.GetOptionalAttributeEnum("objectalignment", s => s switch - { - "unspecified" => ObjectAlignment.Unspecified, - "topleft" => ObjectAlignment.TopLeft, - "top" => ObjectAlignment.Top, - "topright" => ObjectAlignment.TopRight, - "left" => ObjectAlignment.Left, - "center" => ObjectAlignment.Center, - "right" => ObjectAlignment.Right, - "bottomleft" => ObjectAlignment.BottomLeft, - "bottom" => ObjectAlignment.Bottom, - "bottomright" => ObjectAlignment.BottomRight, - _ => throw new Exception($"Unknown object alignment '{s}'") - }) ?? ObjectAlignment.Unspecified; - var renderSize = reader.GetOptionalAttributeEnum("rendersize", s => s switch - { - "tile" => TileRenderSize.Tile, - "grid" => TileRenderSize.Grid, - _ => throw new Exception($"Unknown render size '{s}'") - }) ?? TileRenderSize.Tile; - var fillMode = reader.GetOptionalAttributeEnum("fillmode", s => s switch - { - "stretch" => FillMode.Stretch, - "preserve-aspect-fit" => FillMode.PreserveAspectFit, - _ => throw new Exception($"Unknown fill mode '{s}'") - }) ?? FillMode.Stretch; - - // Elements - Image? image = null; - TileOffset? tileOffset = null; - Grid? grid = null; - Dictionary? properties = null; - List? wangsets = null; - Transformations? transformations = null; - List tiles = []; - - reader.ProcessChildren("tileset", (r, elementName) => elementName switch - { - "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), - "tileoffset" => () => Helpers.SetAtMostOnce(ref tileOffset, ReadTileOffset(r), "TileOffset"), - "grid" => () => Helpers.SetAtMostOnce(ref grid, ReadGrid(r), "Grid"), - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(r, customTypeDefinitions), "Wangsets"), - "transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(r), "Transformations"), - "tile" => () => tiles.Add(ReadTile(r, externalTemplateResolver, customTypeDefinitions)), - _ => r.Skip - }); - - // Check if tileset is referring to external file - if (source is not null) - { - if (externalTilesetResolver is null) - throw new InvalidOperationException("External tileset resolver is required to resolve external tilesets."); - - var resolvedTileset = externalTilesetResolver(source); - resolvedTileset.FirstGID = firstGID; - resolvedTileset.Source = source; - return resolvedTileset; - } - - return new Tileset - { - Version = version, - TiledVersion = tiledVersion, - FirstGID = firstGID, - Source = source, - Name = name, - Class = @class, - TileWidth = tileWidth, - TileHeight = tileHeight, - Spacing = spacing, - Margin = margin, - TileCount = tileCount, - Columns = columns, - ObjectAlignment = objectAlignment, - RenderSize = renderSize, - FillMode = fillMode, - Image = image, - TileOffset = tileOffset, - Grid = grid, - Properties = properties, - Wangsets = wangsets, - Transformations = transformations, - Tiles = tiles - }; - } - - internal static Image ReadImage(XmlReader reader) - { - // Attributes - var format = reader.GetOptionalAttributeEnum("format", s => s switch - { - "png" => ImageFormat.Png, - "jpg" => ImageFormat.Jpg, - "bmp" => ImageFormat.Bmp, - "gif" => ImageFormat.Gif, - _ => throw new Exception($"Unknown image format '{s}'") - }); - var source = reader.GetOptionalAttribute("source"); - var transparentColor = reader.GetOptionalAttributeClass("trans"); - var width = reader.GetOptionalAttributeParseable("width"); - var height = reader.GetOptionalAttributeParseable("height"); - - reader.ProcessChildren("image", (r, elementName) => elementName switch - { - "data" => throw new NotSupportedException("Embedded image data is not supported."), - _ => r.Skip - }); - - if (format is null && source is not null) - format = ParseImageFormatFromSource(source); - - return new Image - { - Format = format, - Source = source, - TransparentColor = transparentColor, - Width = width, - Height = height, - }; - } - - - private static ImageFormat ParseImageFormatFromSource(string source) - { - var extension = Path.GetExtension(source).ToLowerInvariant(); - return extension switch - { - ".png" => ImageFormat.Png, - ".gif" => ImageFormat.Gif, - ".jpg" => ImageFormat.Jpg, - ".jpeg" => ImageFormat.Jpg, - ".bmp" => ImageFormat.Bmp, - _ => throw new XmlException($"Unsupported image format '{extension}'") - }; - } - - internal static TileOffset ReadTileOffset(XmlReader reader) - { - // Attributes - var x = reader.GetOptionalAttributeParseable("x") ?? 0f; - var y = reader.GetOptionalAttributeParseable("y") ?? 0f; - - reader.ReadStartElement("tileoffset"); - return new TileOffset { X = x, Y = y }; - } - - internal static Grid ReadGrid(XmlReader reader) - { - // Attributes - var orientation = reader.GetOptionalAttributeEnum("orientation", s => s switch - { - "orthogonal" => GridOrientation.Orthogonal, - "isometric" => GridOrientation.Isometric, - _ => throw new Exception($"Unknown orientation '{s}'") - }) ?? GridOrientation.Orthogonal; - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); - - reader.ReadStartElement("grid"); - return new Grid { Orientation = orientation, Width = width, Height = height }; - } - - internal static Transformations ReadTransformations(XmlReader reader) - { - // Attributes - var hFlip = (reader.GetOptionalAttributeParseable("hflip") ?? 0) == 1; - var vFlip = (reader.GetOptionalAttributeParseable("vflip") ?? 0) == 1; - var rotate = (reader.GetOptionalAttributeParseable("rotate") ?? 0) == 1; - var preferUntransformed = (reader.GetOptionalAttributeParseable("preferuntransformed") ?? 0) == 1; - - reader.ReadStartElement("transformations"); - return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed }; - } - - internal static Tile ReadTile( - XmlReader reader, - Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var id = reader.GetRequiredAttributeParseable("id"); - var type = reader.GetOptionalAttribute("type") ?? ""; - var probability = reader.GetOptionalAttributeParseable("probability") ?? 0f; - var x = reader.GetOptionalAttributeParseable("x") ?? 0; - var y = reader.GetOptionalAttributeParseable("y") ?? 0; - var width = reader.GetOptionalAttributeParseable("width"); - var height = reader.GetOptionalAttributeParseable("height"); - - // Elements - Dictionary? properties = null; - Image? image = null; - ObjectLayer? objectLayer = null; - List? animation = null; - - reader.ProcessChildren("tile", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(r), "Image"), - "objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(r, externalTemplateResolver, customTypeDefinitions), "ObjectLayer"), - "animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList("animation", "frame", (ar) => - { - var tileID = ar.GetRequiredAttributeParseable("tileid"); - var duration = ar.GetRequiredAttributeParseable("duration"); - return new Frame { TileID = tileID, Duration = duration }; - }), "Animation"), - _ => r.Skip - }); - - return new Tile - { - ID = id, - Type = type, - Probability = probability, - X = x, - Y = y, - Width = width ?? image?.Width ?? 0, - Height = height ?? image?.Height ?? 0, - Properties = properties, - Image = image, - ObjectLayer = objectLayer, - Animation = animation - }; - } - - internal static List ReadWangsets( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - return reader.ReadList("wangsets", "wangset", r => ReadWangset(r, customTypeDefinitions)); - } - - internal static Wangset ReadWangset( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var name = reader.GetRequiredAttribute("name"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var tile = reader.GetRequiredAttributeParseable("tile"); - - // Elements - Dictionary? properties = null; - List wangColors = []; - List wangTiles = []; - - reader.ProcessChildren("wangset", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - "wangcolor" => () => wangColors.Add(ReadWangColor(r, customTypeDefinitions)), - "wangtile" => () => wangTiles.Add(ReadWangTile(r)), - _ => r.Skip - }); - - if (wangColors.Count > 254) - throw new ArgumentException("Wangset can have at most 254 Wang colors."); - - return new Wangset - { - Name = name, - Class = @class, - Tile = tile, - Properties = properties, - WangColors = wangColors, - WangTiles = wangTiles - }; - } - - internal static WangColor ReadWangColor( - XmlReader reader, - IReadOnlyCollection customTypeDefinitions) - { - // Attributes - var name = reader.GetRequiredAttribute("name"); - var @class = reader.GetOptionalAttribute("class") ?? ""; - var color = reader.GetRequiredAttributeParseable("color"); - var tile = reader.GetRequiredAttributeParseable("tile"); - var probability = reader.GetOptionalAttributeParseable("probability") ?? 0f; - - // Elements - Dictionary? properties = null; - - reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch - { - "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(r, customTypeDefinitions), "Properties"), - _ => r.Skip - }); - - return new WangColor - { - Name = name, - Class = @class, - Color = color, - Tile = tile, - Probability = probability, - Properties = properties - }; - } - - internal static WangTile ReadWangTile(XmlReader reader) - { - // Attributes - var tileID = reader.GetRequiredAttributeParseable("tileid"); - var wangID = reader.GetRequiredAttributeParseable("wangid", s => - { - // Comma-separated list of indices (0-254) - var indices = s.Split(',').Select(i => byte.Parse(i)).ToArray(); - if (indices.Length > 8) - throw new ArgumentException("Wang ID can have at most 8 indices."); - return indices; - }); - - reader.ReadStartElement("wangtile"); - - return new WangTile - { - TileID = tileID, - WangID = wangID - }; - } -} diff --git a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs index f9228b4..6029481 100644 --- a/src/DotTiled/Serialization/Tmx/TmxMapReader.cs +++ b/src/DotTiled/Serialization/Tmx/TmxMapReader.cs @@ -1,70 +1,25 @@ using System; -using System.Collections.Generic; using System.Xml; -using DotTiled.Model; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; namespace DotTiled.Serialization.Tmx; -public class TmxMapReader : IMapReader +/// +/// A map reader for the Tiled XML format. +/// +public class TmxMapReader : TmxReaderBase, IMapReader { - // External resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - + /// + /// Constructs a new . + /// + /// public TmxMapReader( XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _reader = reader ?? throw new ArgumentNullException(nameof(reader)); - _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } - // Prepare reader - _reader.MoveToContent(); - } - - public Map ReadMap() - { - return Tmx.ReadMap(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); - } - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - _reader.Dispose(); - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~TmxTiledMapReader() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - System.GC.SuppressFinalize(this); - } + /// + public new Map ReadMap() => base.ReadMap(); } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs similarity index 57% rename from src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs rename to src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs index d0db01f..4ab5b11 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Chunk.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Chunk.cs @@ -1,27 +1,24 @@ -using System.Xml; -using DotTiled.Model.Layers; - namespace DotTiled.Serialization.Tmx; -internal partial class Tmx +public abstract partial class TmxReaderBase { - internal static Chunk ReadChunk(XmlReader reader, DataEncoding? encoding, DataCompression? compression) + internal Chunk ReadChunk(DataEncoding? encoding, DataCompression? compression) { - var x = reader.GetRequiredAttributeParseable("x"); - var y = reader.GetRequiredAttributeParseable("y"); - var width = reader.GetRequiredAttributeParseable("width"); - var height = reader.GetRequiredAttributeParseable("height"); + var x = _reader.GetRequiredAttributeParseable("x"); + var y = _reader.GetRequiredAttributeParseable("y"); + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); var usesTileChildrenInsteadOfRawData = encoding is null; if (usesTileChildrenInsteadOfRawData) { - var globalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("chunk", reader); + var globalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("chunk", _reader); var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags); return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags }; } else { - var globalTileIDsWithFlippingFlags = ReadRawData(reader, encoding!.Value, compression); + var globalTileIDsWithFlippingFlags = ReadRawData(_reader, encoding!.Value, compression); var (globalTileIDs, flippingFlags) = ReadAndClearFlippingFlagsFromGIDs(globalTileIDsWithFlippingFlags); return new Chunk { X = x, Y = y, Width = width, Height = height, GlobalTileIDs = globalTileIDs, FlippingFlags = flippingFlags }; } diff --git a/src/DotTiled/Serialization/Tmx/Tmx.Data.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs similarity index 84% rename from src/DotTiled/Serialization/Tmx/Tmx.Data.cs rename to src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs index 5d55ba4..fb29250 100644 --- a/src/DotTiled/Serialization/Tmx/Tmx.Data.cs +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Data.cs @@ -4,21 +4,20 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Xml; -using DotTiled.Model.Layers; namespace DotTiled.Serialization.Tmx; -internal partial class Tmx +public abstract partial class TmxReaderBase { - internal static Data ReadData(XmlReader reader, bool usesChunks) + internal Data ReadData(bool usesChunks) { - var encoding = reader.GetOptionalAttributeEnum("encoding", e => e switch + var encoding = _reader.GetOptionalAttributeEnum("encoding", e => e switch { "csv" => DataEncoding.Csv, "base64" => DataEncoding.Base64, _ => throw new XmlException("Invalid encoding") }); - var compression = reader.GetOptionalAttributeEnum("compression", c => c switch + var compression = _reader.GetOptionalAttributeEnum("compression", c => c switch { "gzip" => DataCompression.GZip, "zlib" => DataCompression.ZLib, @@ -28,8 +27,8 @@ internal partial class Tmx if (usesChunks) { - var chunks = reader - .ReadList("data", "chunk", (r) => ReadChunk(r, encoding, compression)) + var chunks = _reader + .ReadList("data", "chunk", (r) => ReadChunk(encoding, compression)) .ToArray(); return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = null, Chunks = chunks }; } @@ -37,12 +36,12 @@ internal partial class Tmx var usesTileChildrenInsteadOfRawData = encoding is null && compression is null; if (usesTileChildrenInsteadOfRawData) { - var tileChildrenGlobalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("data", reader); + var tileChildrenGlobalTileIDsWithFlippingFlags = ReadTileChildrenInWrapper("data", _reader); var (tileChildrenGlobalTileIDs, tileChildrenFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(tileChildrenGlobalTileIDsWithFlippingFlags); return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = tileChildrenGlobalTileIDs, FlippingFlags = tileChildrenFlippingFlags, Chunks = null }; } - var rawDataGlobalTileIDsWithFlippingFlags = ReadRawData(reader, encoding!.Value, compression); + var rawDataGlobalTileIDsWithFlippingFlags = ReadRawData(_reader, encoding!.Value, compression); var (rawDataGlobalTileIDs, rawDataFlippingFlags) = ReadAndClearFlippingFlagsFromGIDs(rawDataGlobalTileIDsWithFlippingFlags); return new Data { Encoding = encoding, Compression = compression, GlobalTileIDs = rawDataGlobalTileIDs, FlippingFlags = rawDataFlippingFlags, Chunks = null }; } @@ -62,10 +61,8 @@ internal partial class Tmx return (clearedGlobalTileIDs, flippingFlags); } - internal static uint[] ReadTileChildrenInWrapper(string wrapper, XmlReader reader) - { - return reader.ReadList(wrapper, "tile", (r) => r.GetOptionalAttributeParseable("gid") ?? 0).ToArray(); - } + internal static uint[] ReadTileChildrenInWrapper(string wrapper, XmlReader reader) => + reader.ReadList(wrapper, "tile", (r) => r.GetOptionalAttributeParseable("gid") ?? 0).ToArray(); internal static uint[] ReadRawData(XmlReader reader, DataEncoding encoding, DataCompression? compression) { diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs new file mode 100644 index 0000000..11d6fc1 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Map.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace DotTiled.Serialization.Tmx; + +/// +/// Base class for Tiled XML format readers. +/// +public abstract partial class TmxReaderBase +{ + internal Map ReadMap() + { + // Attributes + var version = _reader.GetRequiredAttribute("version"); + var tiledVersion = _reader.GetRequiredAttribute("tiledversion"); + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var orientation = _reader.GetRequiredAttributeEnum("orientation", s => s switch + { + "orthogonal" => MapOrientation.Orthogonal, + "isometric" => MapOrientation.Isometric, + "staggered" => MapOrientation.Staggered, + "hexagonal" => MapOrientation.Hexagonal, + _ => throw new InvalidOperationException($"Unknown orientation '{s}'") + }); + var renderOrder = _reader.GetOptionalAttributeEnum("renderorder", s => s switch + { + "right-down" => RenderOrder.RightDown, + "right-up" => RenderOrder.RightUp, + "left-down" => RenderOrder.LeftDown, + "left-up" => RenderOrder.LeftUp, + _ => throw new InvalidOperationException($"Unknown render order '{s}'") + }) ?? RenderOrder.RightDown; + var compressionLevel = _reader.GetOptionalAttributeParseable("compressionlevel") ?? -1; + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); + var tileWidth = _reader.GetRequiredAttributeParseable("tilewidth"); + var tileHeight = _reader.GetRequiredAttributeParseable("tileheight"); + var hexSideLength = _reader.GetOptionalAttributeParseable("hexsidelength"); + var staggerAxis = _reader.GetOptionalAttributeEnum("staggeraxis", s => s switch + { + "x" => StaggerAxis.X, + "y" => StaggerAxis.Y, + _ => throw new InvalidOperationException($"Unknown stagger axis '{s}'") + }); + var staggerIndex = _reader.GetOptionalAttributeEnum("staggerindex", s => s switch + { + "odd" => StaggerIndex.Odd, + "even" => StaggerIndex.Even, + _ => throw new InvalidOperationException($"Unknown stagger index '{s}'") + }); + var parallaxOriginX = _reader.GetOptionalAttributeParseable("parallaxoriginx") ?? 0.0f; + var parallaxOriginY = _reader.GetOptionalAttributeParseable("parallaxoriginy") ?? 0.0f; + var backgroundColor = _reader.GetOptionalAttributeClass("backgroundcolor") ?? Color.Parse("#00000000", CultureInfo.InvariantCulture); + var nextLayerID = _reader.GetRequiredAttributeParseable("nextlayerid"); + var nextObjectID = _reader.GetRequiredAttributeParseable("nextobjectid"); + var infinite = (_reader.GetOptionalAttributeParseable("infinite") ?? 0) == 1; + + // At most one of + List? properties = null; + + // Any number of + List layers = []; + List tilesets = []; + + _reader.ProcessChildren("map", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "tileset" => () => tilesets.Add(ReadTileset()), + "layer" => () => layers.Add(ReadTileLayer(infinite)), + "objectgroup" => () => layers.Add(ReadObjectLayer()), + "imagelayer" => () => layers.Add(ReadImageLayer()), + "group" => () => layers.Add(ReadGroup()), + _ => r.Skip + }); + + return new Map + { + Version = version, + TiledVersion = tiledVersion, + Class = @class, + Orientation = orientation, + RenderOrder = renderOrder, + CompressionLevel = compressionLevel, + Width = width, + Height = height, + TileWidth = tileWidth, + TileHeight = tileHeight, + HexSideLength = hexSideLength, + StaggerAxis = staggerAxis, + StaggerIndex = staggerIndex, + ParallaxOriginX = parallaxOriginX, + ParallaxOriginY = parallaxOriginY, + BackgroundColor = backgroundColor, + NextLayerID = nextLayerID, + NextObjectID = nextObjectID, + Infinite = infinite, + Properties = properties ?? [], + Tilesets = tilesets, + Layers = layers + }; + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs new file mode 100644 index 0000000..0233639 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.ObjectLayer.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Numerics; + +namespace DotTiled.Serialization.Tmx; + +public abstract partial class TmxReaderBase +{ + internal ObjectLayer ReadObjectLayer() + { + // Attributes + var id = _reader.GetRequiredAttributeParseable("id"); + var name = _reader.GetOptionalAttribute("name") ?? ""; + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var x = _reader.GetOptionalAttributeParseable("x") ?? 0; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0; + var width = _reader.GetOptionalAttributeParseable("width"); + var height = _reader.GetOptionalAttributeParseable("height"); + var opacity = _reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = _reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = _reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = _reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = _reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = _reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = _reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + var color = _reader.GetOptionalAttributeClass("color"); + var drawOrder = _reader.GetOptionalAttributeEnum("draworder", s => s switch + { + "topdown" => DrawOrder.TopDown, + "index" => DrawOrder.Index, + _ => throw new InvalidOperationException($"Unknown draw order '{s}'") + }) ?? DrawOrder.TopDown; + + // Elements + List? properties = null; + List objects = []; + + _reader.ProcessChildren("objectgroup", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "object" => () => objects.Add(ReadObject()), + _ => r.Skip + }); + + return new ObjectLayer + { + ID = id, + Name = name, + Class = @class, + X = x, + Y = y, + Width = width, + Height = height, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Color = color, + Properties = properties ?? [], + DrawOrder = drawOrder, + Objects = objects + }; + } + + internal DotTiled.Object ReadObject() + { + // Attributes + var template = _reader.GetOptionalAttribute("template"); + DotTiled.Object? obj = null; + if (template is not null) + obj = _externalTemplateResolver(template).Object; + + uint? idDefault = obj?.ID ?? null; + string nameDefault = obj?.Name ?? ""; + string typeDefault = obj?.Type ?? ""; + float xDefault = obj?.X ?? 0f; + float yDefault = obj?.Y ?? 0f; + float widthDefault = obj?.Width ?? 0f; + float heightDefault = obj?.Height ?? 0f; + float rotationDefault = obj?.Rotation ?? 0f; + uint? gidDefault = obj is TileObject tileObj ? tileObj.GID : null; + bool visibleDefault = obj?.Visible ?? true; + List? propertiesDefault = obj?.Properties ?? null; + + var id = _reader.GetOptionalAttributeParseable("id") ?? idDefault; + var name = _reader.GetOptionalAttribute("name") ?? nameDefault; + var type = _reader.GetOptionalAttribute("type") ?? typeDefault; + var x = _reader.GetOptionalAttributeParseable("x") ?? xDefault; + var y = _reader.GetOptionalAttributeParseable("y") ?? yDefault; + var width = _reader.GetOptionalAttributeParseable("width") ?? widthDefault; + var height = _reader.GetOptionalAttributeParseable("height") ?? heightDefault; + var rotation = _reader.GetOptionalAttributeParseable("rotation") ?? rotationDefault; + var gid = _reader.GetOptionalAttributeParseable("gid") ?? gidDefault; + var visible = _reader.GetOptionalAttributeParseable("visible") ?? visibleDefault; + + // Elements + DotTiled.Object? foundObject = null; + int propertiesCounter = 0; + List? properties = propertiesDefault; + + _reader.ProcessChildren("object", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnceUsingCounter(ref properties, Helpers.MergeProperties(properties, ReadProperties()).ToList(), "Properties", ref propertiesCounter), + "ellipse" => () => Helpers.SetAtMostOnce(ref foundObject, ReadEllipseObject(), "Object marker"), + "point" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPointObject(), "Object marker"), + "polygon" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolygonObject(), "Object marker"), + "polyline" => () => Helpers.SetAtMostOnce(ref foundObject, ReadPolylineObject(), "Object marker"), + "text" => () => Helpers.SetAtMostOnce(ref foundObject, ReadTextObject(), "Object marker"), + _ => throw new InvalidOperationException($"Unknown object marker '{elementName}'") + }); + + if (foundObject is null) + { + if (gid is not null) + foundObject = new TileObject { ID = id, GID = gid.Value }; + else + foundObject = new RectangleObject { ID = id }; + } + + foundObject.ID = id; + foundObject.Name = name; + foundObject.Type = type; + foundObject.X = x; + foundObject.Y = y; + foundObject.Width = width; + foundObject.Height = height; + foundObject.Rotation = rotation; + foundObject.Visible = visible; + foundObject.Properties = properties ?? []; + foundObject.Template = template; + + return OverrideObject(obj, foundObject); + } + + internal static DotTiled.Object OverrideObject(DotTiled.Object? obj, DotTiled.Object foundObject) + { + if (obj is null) + return foundObject; + + 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; + } + + return OverrideObject((dynamic)obj, (dynamic)foundObject); + } + + internal EllipseObject ReadEllipseObject() + { + _reader.Skip(); + return new EllipseObject { }; + } + + internal static EllipseObject OverrideObject(EllipseObject obj, EllipseObject _) => obj; + + internal PointObject ReadPointObject() + { + _reader.Skip(); + return new PointObject { }; + } + + internal static PointObject OverrideObject(PointObject obj, PointObject _) => obj; + + internal PolygonObject ReadPolygonObject() + { + // Attributes + var points = _reader.GetRequiredAttributeParseable>("points", s => + { + // Takes on format "x1,y1 x2,y2 x3,y3 ..." + var coords = s.Split(' '); + return coords.Select(c => + { + var xy = c.Split(','); + return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture)); + }).ToList(); + }); + + _reader.ReadStartElement("polygon"); + return new PolygonObject { Points = points }; + } + + internal static PolygonObject OverrideObject(PolygonObject obj, PolygonObject foundObject) + { + obj.Points = foundObject.Points; + return obj; + } + + internal PolylineObject ReadPolylineObject() + { + // Attributes + var points = _reader.GetRequiredAttributeParseable>("points", s => + { + // Takes on format "x1,y1 x2,y2 x3,y3 ..." + var coords = s.Split(' '); + return coords.Select(c => + { + var xy = c.Split(','); + return new Vector2(float.Parse(xy[0], CultureInfo.InvariantCulture), float.Parse(xy[1], CultureInfo.InvariantCulture)); + }).ToList(); + }); + + _reader.ReadStartElement("polyline"); + return new PolylineObject { Points = points }; + } + + internal static PolylineObject OverrideObject(PolylineObject obj, PolylineObject foundObject) + { + obj.Points = foundObject.Points; + return obj; + } + + internal TextObject ReadTextObject() + { + // Attributes + var fontFamily = _reader.GetOptionalAttribute("fontfamily") ?? "sans-serif"; + var pixelSize = _reader.GetOptionalAttributeParseable("pixelsize") ?? 16; + var wrap = _reader.GetOptionalAttributeParseable("wrap") ?? false; + var color = _reader.GetOptionalAttributeClass("color") ?? Color.Parse("#000000", CultureInfo.InvariantCulture); + var bold = _reader.GetOptionalAttributeParseable("bold") ?? false; + var italic = _reader.GetOptionalAttributeParseable("italic") ?? false; + var underline = _reader.GetOptionalAttributeParseable("underline") ?? false; + var strikeout = _reader.GetOptionalAttributeParseable("strikeout") ?? false; + var kerning = _reader.GetOptionalAttributeParseable("kerning") ?? true; + var hAlign = _reader.GetOptionalAttributeEnum("halign", s => s switch + { + "left" => TextHorizontalAlignment.Left, + "center" => TextHorizontalAlignment.Center, + "right" => TextHorizontalAlignment.Right, + "justify" => TextHorizontalAlignment.Justify, + _ => throw new InvalidOperationException($"Unknown horizontal alignment '{s}'") + }) ?? TextHorizontalAlignment.Left; + var vAlign = _reader.GetOptionalAttributeEnum("valign", s => s switch + { + "top" => TextVerticalAlignment.Top, + "center" => TextVerticalAlignment.Center, + "bottom" => TextVerticalAlignment.Bottom, + _ => throw new InvalidOperationException($"Unknown vertical alignment '{s}'") + }) ?? TextVerticalAlignment.Top; + + // Elements + var text = _reader.ReadElementContentAsString("text", ""); + + return new TextObject + { + FontFamily = fontFamily, + PixelSize = pixelSize, + Wrap = wrap, + Color = color, + Bold = bold, + Italic = italic, + Underline = underline, + Strikeout = strikeout, + Kerning = kerning, + HorizontalAlignment = hAlign, + VerticalAlignment = vAlign, + Text = text + }; + } + + internal static TextObject OverrideObject(TextObject obj, TextObject foundObject) + { + obj.FontFamily = foundObject.FontFamily; + obj.PixelSize = foundObject.PixelSize; + obj.Wrap = foundObject.Wrap; + obj.Color = foundObject.Color; + obj.Bold = foundObject.Bold; + obj.Italic = foundObject.Italic; + obj.Underline = foundObject.Underline; + obj.Strikeout = foundObject.Strikeout; + obj.Kerning = foundObject.Kerning; + obj.HorizontalAlignment = foundObject.HorizontalAlignment; + obj.VerticalAlignment = foundObject.VerticalAlignment; + obj.Text = foundObject.Text; + return obj; + } + + internal static TileObject OverrideObject(TileObject obj, TileObject foundObject) + { + obj.GID = foundObject.GID; + return obj; + } + + internal Template ReadTemplate() + { + // No attributes + + // At most one of + Tileset? tileset = null; + + // Should contain exactly one of + DotTiled.Object? obj = null; + + _reader.ProcessChildren("template", (r, elementName) => elementName switch + { + "tileset" => () => Helpers.SetAtMostOnce(ref tileset, ReadTileset(), "Tileset"), + "object" => () => Helpers.SetAtMostOnce(ref obj, ReadObject(), "Object"), + _ => r.Skip + }); + + if (obj is null) + throw new NotSupportedException("Template must contain exactly one object"); + + return new Template + { + Tileset = tileset, + Object = obj + }; + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs new file mode 100644 index 0000000..de12d57 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Properties.cs @@ -0,0 +1,149 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml; + +namespace DotTiled.Serialization.Tmx; + +public abstract partial class TmxReaderBase +{ + internal List ReadProperties() + { + if (!_reader.IsStartElement("properties")) + return []; + + return _reader.ReadList("properties", "property", (r) => + { + var name = r.GetRequiredAttribute("name"); + var type = r.GetOptionalAttributeEnum("type", (s) => s switch + { + "string" => PropertyType.String, + "int" => PropertyType.Int, + "float" => PropertyType.Float, + "bool" => PropertyType.Bool, + "color" => PropertyType.Color, + "file" => PropertyType.File, + "object" => PropertyType.Object, + "class" => PropertyType.Class, + _ => throw new XmlException("Invalid property type") + }) ?? PropertyType.String; + var propertyType = r.GetOptionalAttribute("propertytype"); + if (propertyType is not null) + { + return ReadPropertyWithCustomType(); + } + + IProperty property = type switch + { + PropertyType.String => new StringProperty { Name = name, Value = r.GetRequiredAttribute("value") }, + PropertyType.Int => new IntProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Float => new FloatProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Bool => new BoolProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Color => new ColorProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.File => new FileProperty { Name = name, Value = r.GetRequiredAttribute("value") }, + PropertyType.Object => new ObjectProperty { Name = name, Value = r.GetRequiredAttributeParseable("value") }, + PropertyType.Class => throw new XmlException("Class property must have a property type"), + PropertyType.Enum => throw new XmlException("Enum property must have a property type"), + _ => throw new XmlException("Invalid property type") + }; + return property; + }); + } + + internal IProperty ReadPropertyWithCustomType() + { + var isClass = _reader.GetOptionalAttribute("type") == "class"; + if (isClass) + { + return ReadClassProperty(); + } + + return ReadEnumProperty(); + } + + internal ClassProperty ReadClassProperty() + { + var name = _reader.GetRequiredAttribute("name"); + var propertyType = _reader.GetRequiredAttribute("propertytype"); + var customTypeDef = _customTypeResolver(propertyType); + + if (customTypeDef is CustomClassDefinition ccd) + { + 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 }; + } + } + + throw new XmlException($"Unkonwn custom class definition: {propertyType}"); + } + + internal EnumProperty ReadEnumProperty() + { + var name = _reader.GetRequiredAttribute("name"); + var propertyType = _reader.GetRequiredAttribute("propertytype"); + var typeInXml = _reader.GetOptionalAttributeEnum("type", (s) => s switch + { + "string" => PropertyType.String, + "int" => PropertyType.Int, + _ => throw new XmlException("Invalid property type") + }) ?? 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 (ced.StorageType == CustomEnumStorageType.String) + { + var value = _reader.GetRequiredAttribute("value"); + if (value.Contains(',') && !ced.ValueAsFlags) + throw new XmlException("Enum value must not contain ',' if not ValueAsFlags is set to true."); + + if (ced.ValueAsFlags) + { + var values = value.Split(',').Select(v => v.Trim()).ToHashSet(); + return new EnumProperty { Name = name, PropertyType = propertyType, Value = values }; + } + else + { + return new EnumProperty { Name = name, PropertyType = propertyType, Value = new HashSet { value } }; + } + } + else if (ced.StorageType == CustomEnumStorageType.Int) + { + var value = _reader.GetRequiredAttributeParseable("value"); + if (ced.ValueAsFlags) + { + var allValues = ced.Values; + var enumValues = new HashSet(); + for (var i = 0; i < allValues.Count; i++) + { + var mask = 1 << i; + if ((value & mask) == mask) + { + var enumValue = allValues[i]; + _ = enumValues.Add(enumValue); + } + } + return new EnumProperty { Name = name, PropertyType = propertyType, Value = enumValues }; + } + else + { + var allValues = ced.Values; + var enumValue = allValues[value]; + return new EnumProperty { Name = name, PropertyType = propertyType, Value = new HashSet { enumValue } }; + } + } + + throw new XmlException($"Unknown custom enum storage type: {ced.StorageType}"); + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs new file mode 100644 index 0000000..c345917 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.TileLayer.cs @@ -0,0 +1,146 @@ +using System.Collections.Generic; +using System.Linq; + +namespace DotTiled.Serialization.Tmx; + +public abstract partial class TmxReaderBase +{ + internal TileLayer ReadTileLayer(bool dataUsesChunks) + { + var id = _reader.GetRequiredAttributeParseable("id"); + var name = _reader.GetOptionalAttribute("name") ?? ""; + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var x = _reader.GetOptionalAttributeParseable("x") ?? 0; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0; + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); + var opacity = _reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = _reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = _reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = _reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = _reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = _reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = _reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + + List? properties = null; + Data? data = null; + + _reader.ProcessChildren("layer", (r, elementName) => elementName switch + { + "data" => () => Helpers.SetAtMostOnce(ref data, ReadData(dataUsesChunks), "Data"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + _ => r.Skip + }); + + return new TileLayer + { + ID = id, + Name = name, + Class = @class, + X = x, + Y = y, + Width = width, + Height = height, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Data = data, + Properties = properties ?? [] + }; + } + + internal ImageLayer ReadImageLayer() + { + var id = _reader.GetRequiredAttributeParseable("id"); + var name = _reader.GetOptionalAttribute("name") ?? ""; + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var x = _reader.GetOptionalAttributeParseable("x") ?? 0; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0; + var opacity = _reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = _reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = _reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = _reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = _reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = _reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = _reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + var repeatX = (_reader.GetOptionalAttributeParseable("repeatx") ?? 0) == 1; + var repeatY = (_reader.GetOptionalAttributeParseable("repeaty") ?? 0) == 1; + + List? properties = null; + Image? image = null; + + _reader.ProcessChildren("imagelayer", (r, elementName) => elementName switch + { + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + _ => r.Skip + }); + + return new ImageLayer + { + ID = id, + Name = name, + Class = @class, + X = x, + Y = y, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties ?? [], + Image = image, + RepeatX = repeatX, + RepeatY = repeatY + }; + } + + internal Group ReadGroup() + { + var id = _reader.GetRequiredAttributeParseable("id"); + var name = _reader.GetOptionalAttribute("name") ?? ""; + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var opacity = _reader.GetOptionalAttributeParseable("opacity") ?? 1.0f; + var visible = _reader.GetOptionalAttributeParseable("visible") ?? true; + var tintColor = _reader.GetOptionalAttributeClass("tintcolor"); + var offsetX = _reader.GetOptionalAttributeParseable("offsetx") ?? 0.0f; + var offsetY = _reader.GetOptionalAttributeParseable("offsety") ?? 0.0f; + var parallaxX = _reader.GetOptionalAttributeParseable("parallaxx") ?? 1.0f; + var parallaxY = _reader.GetOptionalAttributeParseable("parallaxy") ?? 1.0f; + + List? properties = null; + List layers = []; + + _reader.ProcessChildren("group", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "layer" => () => layers.Add(ReadTileLayer(false)), + "objectgroup" => () => layers.Add(ReadObjectLayer()), + "imagelayer" => () => layers.Add(ReadImageLayer()), + "group" => () => layers.Add(ReadGroup()), + _ => r.Skip + }); + + return new Group + { + ID = id, + Name = name, + Class = @class, + Opacity = opacity, + Visible = visible, + TintColor = tintColor, + OffsetX = offsetX, + OffsetY = offsetY, + ParallaxX = parallaxX, + ParallaxY = parallaxY, + Properties = properties ?? [], + Layers = layers + }; + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs new file mode 100644 index 0000000..bffe0bd --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.Tileset.cs @@ -0,0 +1,316 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace DotTiled.Serialization.Tmx; + +public abstract partial class TmxReaderBase +{ + internal Tileset ReadTileset() + { + // Attributes + var version = _reader.GetOptionalAttribute("version"); + var tiledVersion = _reader.GetOptionalAttribute("tiledversion"); + var firstGID = _reader.GetOptionalAttributeParseable("firstgid"); + var source = _reader.GetOptionalAttribute("source"); + var name = _reader.GetOptionalAttribute("name"); + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var tileWidth = _reader.GetOptionalAttributeParseable("tilewidth"); + var tileHeight = _reader.GetOptionalAttributeParseable("tileheight"); + var spacing = _reader.GetOptionalAttributeParseable("spacing") ?? 0; + var margin = _reader.GetOptionalAttributeParseable("margin") ?? 0; + var tileCount = _reader.GetOptionalAttributeParseable("tilecount"); + var columns = _reader.GetOptionalAttributeParseable("columns"); + var objectAlignment = _reader.GetOptionalAttributeEnum("objectalignment", s => s switch + { + "unspecified" => ObjectAlignment.Unspecified, + "topleft" => ObjectAlignment.TopLeft, + "top" => ObjectAlignment.Top, + "topright" => ObjectAlignment.TopRight, + "left" => ObjectAlignment.Left, + "center" => ObjectAlignment.Center, + "right" => ObjectAlignment.Right, + "bottomleft" => ObjectAlignment.BottomLeft, + "bottom" => ObjectAlignment.Bottom, + "bottomright" => ObjectAlignment.BottomRight, + _ => throw new InvalidOperationException($"Unknown object alignment '{s}'") + }) ?? ObjectAlignment.Unspecified; + var renderSize = _reader.GetOptionalAttributeEnum("rendersize", s => s switch + { + "tile" => TileRenderSize.Tile, + "grid" => TileRenderSize.Grid, + _ => throw new InvalidOperationException($"Unknown render size '{s}'") + }) ?? TileRenderSize.Tile; + var fillMode = _reader.GetOptionalAttributeEnum("fillmode", s => s switch + { + "stretch" => FillMode.Stretch, + "preserve-aspect-fit" => FillMode.PreserveAspectFit, + _ => throw new InvalidOperationException($"Unknown fill mode '{s}'") + }) ?? FillMode.Stretch; + + // Elements + Image? image = null; + TileOffset? tileOffset = null; + Grid? grid = null; + List? properties = null; + List? wangsets = null; + Transformations? transformations = null; + List tiles = []; + + _reader.ProcessChildren("tileset", (r, elementName) => elementName switch + { + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"), + "tileoffset" => () => Helpers.SetAtMostOnce(ref tileOffset, ReadTileOffset(), "TileOffset"), + "grid" => () => Helpers.SetAtMostOnce(ref grid, ReadGrid(), "Grid"), + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "wangsets" => () => Helpers.SetAtMostOnce(ref wangsets, ReadWangsets(), "Wangsets"), + "transformations" => () => Helpers.SetAtMostOnce(ref transformations, ReadTransformations(), "Transformations"), + "tile" => () => tiles.Add(ReadTile()), + _ => r.Skip + }); + + // Check if tileset is referring to external file + if (source is not null) + { + var resolvedTileset = _externalTilesetResolver(source); + resolvedTileset.FirstGID = firstGID; + resolvedTileset.Source = source; + return resolvedTileset; + } + + return new Tileset + { + Version = version, + TiledVersion = tiledVersion, + FirstGID = firstGID, + Source = source, + Name = name, + Class = @class, + TileWidth = tileWidth, + TileHeight = tileHeight, + Spacing = spacing, + Margin = margin, + TileCount = tileCount, + Columns = columns, + ObjectAlignment = objectAlignment, + RenderSize = renderSize, + FillMode = fillMode, + Image = image, + TileOffset = tileOffset, + Grid = grid, + Properties = properties ?? [], + Wangsets = wangsets, + Transformations = transformations, + Tiles = tiles + }; + } + + internal Image ReadImage() + { + // Attributes + var format = _reader.GetOptionalAttributeEnum("format", s => s switch + { + "png" => ImageFormat.Png, + "jpg" => ImageFormat.Jpg, + "bmp" => ImageFormat.Bmp, + "gif" => ImageFormat.Gif, + _ => throw new InvalidOperationException($"Unknown image format '{s}'") + }); + var source = _reader.GetOptionalAttribute("source"); + var transparentColor = _reader.GetOptionalAttributeClass("trans"); + var width = _reader.GetOptionalAttributeParseable("width"); + var height = _reader.GetOptionalAttributeParseable("height"); + + _reader.ProcessChildren("image", (r, elementName) => elementName switch + { + "data" => throw new NotSupportedException("Embedded image data is not supported."), + _ => r.Skip + }); + + if (format is null && source is not null) + format = Helpers.ParseImageFormatFromSource(source); + + return new Image + { + Format = format, + Source = source, + TransparentColor = transparentColor, + Width = width, + Height = height, + }; + } + + internal TileOffset ReadTileOffset() + { + // Attributes + var x = _reader.GetOptionalAttributeParseable("x") ?? 0f; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0f; + + _reader.ReadStartElement("tileoffset"); + return new TileOffset { X = x, Y = y }; + } + + internal Grid ReadGrid() + { + // Attributes + var orientation = _reader.GetOptionalAttributeEnum("orientation", s => s switch + { + "orthogonal" => GridOrientation.Orthogonal, + "isometric" => GridOrientation.Isometric, + _ => throw new InvalidOperationException($"Unknown orientation '{s}'") + }) ?? GridOrientation.Orthogonal; + var width = _reader.GetRequiredAttributeParseable("width"); + var height = _reader.GetRequiredAttributeParseable("height"); + + _reader.ReadStartElement("grid"); + return new Grid { Orientation = orientation, Width = width, Height = height }; + } + + internal Transformations ReadTransformations() + { + // Attributes + var hFlip = (_reader.GetOptionalAttributeParseable("hflip") ?? 0) == 1; + var vFlip = (_reader.GetOptionalAttributeParseable("vflip") ?? 0) == 1; + var rotate = (_reader.GetOptionalAttributeParseable("rotate") ?? 0) == 1; + var preferUntransformed = (_reader.GetOptionalAttributeParseable("preferuntransformed") ?? 0) == 1; + + _reader.ReadStartElement("transformations"); + return new Transformations { HFlip = hFlip, VFlip = vFlip, Rotate = rotate, PreferUntransformed = preferUntransformed }; + } + + internal Tile ReadTile() + { + // Attributes + var id = _reader.GetRequiredAttributeParseable("id"); + var type = _reader.GetOptionalAttribute("type") ?? ""; + var probability = _reader.GetOptionalAttributeParseable("probability") ?? 0f; + var x = _reader.GetOptionalAttributeParseable("x") ?? 0; + var y = _reader.GetOptionalAttributeParseable("y") ?? 0; + var width = _reader.GetOptionalAttributeParseable("width"); + var height = _reader.GetOptionalAttributeParseable("height"); + + // Elements + List? properties = null; + Image? image = null; + ObjectLayer? objectLayer = null; + List? animation = null; + + _reader.ProcessChildren("tile", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "image" => () => Helpers.SetAtMostOnce(ref image, ReadImage(), "Image"), + "objectgroup" => () => Helpers.SetAtMostOnce(ref objectLayer, ReadObjectLayer(), "ObjectLayer"), + "animation" => () => Helpers.SetAtMostOnce(ref animation, r.ReadList("animation", "frame", (ar) => + { + var tileID = ar.GetRequiredAttributeParseable("tileid"); + var duration = ar.GetRequiredAttributeParseable("duration"); + return new Frame { TileID = tileID, Duration = duration }; + }), "Animation"), + _ => r.Skip + }); + + return new Tile + { + ID = id, + Type = type, + Probability = probability, + X = x, + Y = y, + Width = width ?? image?.Width ?? 0, + Height = height ?? image?.Height ?? 0, + Properties = properties ?? [], + Image = image, + ObjectLayer = objectLayer, + Animation = animation + }; + } + + internal List ReadWangsets() => + _reader.ReadList("wangsets", "wangset", r => ReadWangset()); + + internal Wangset ReadWangset() + { + // Attributes + var name = _reader.GetRequiredAttribute("name"); + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var tile = _reader.GetRequiredAttributeParseable("tile"); + + // Elements + List? properties = null; + List wangColors = []; + List wangTiles = []; + + _reader.ProcessChildren("wangset", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + "wangcolor" => () => wangColors.Add(ReadWangColor()), + "wangtile" => () => wangTiles.Add(ReadWangTile()), + _ => r.Skip + }); + + if (wangColors.Count > 254) + throw new ArgumentException("Wangset can have at most 254 Wang colors."); + + return new Wangset + { + Name = name, + Class = @class, + Tile = tile, + Properties = properties ?? [], + WangColors = wangColors, + WangTiles = wangTiles + }; + } + + internal WangColor ReadWangColor() + { + // Attributes + var name = _reader.GetRequiredAttribute("name"); + var @class = _reader.GetOptionalAttribute("class") ?? ""; + var color = _reader.GetRequiredAttributeParseable("color"); + var tile = _reader.GetRequiredAttributeParseable("tile"); + var probability = _reader.GetOptionalAttributeParseable("probability") ?? 0f; + + // Elements + List? properties = null; + + _reader.ProcessChildren("wangcolor", (r, elementName) => elementName switch + { + "properties" => () => Helpers.SetAtMostOnce(ref properties, ReadProperties(), "Properties"), + _ => r.Skip + }); + + return new WangColor + { + Name = name, + Class = @class, + Color = color, + Tile = tile, + Probability = probability, + Properties = properties ?? [] + }; + } + + internal WangTile ReadWangTile() + { + // Attributes + var tileID = _reader.GetRequiredAttributeParseable("tileid"); + var wangID = _reader.GetRequiredAttributeParseable("wangid", s => + { + // Comma-separated list of indices (0-254) + var indices = s.Split(',').Select(i => byte.Parse(i, CultureInfo.InvariantCulture)).ToArray(); + if (indices.Length > 8) + throw new ArgumentException("Wang ID can have at most 8 indices."); + return indices; + }); + + _reader.ReadStartElement("wangtile"); + + return new WangTile + { + TileID = tileID, + WangID = wangID + }; + } +} diff --git a/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs b/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs new file mode 100644 index 0000000..e30af82 --- /dev/null +++ b/src/DotTiled/Serialization/Tmx/TmxReaderBase.cs @@ -0,0 +1,73 @@ +using System; +using System.Xml; + +namespace DotTiled.Serialization.Tmx; + +/// +/// Base class for Tiled XML format readers. +/// +public abstract partial class TmxReaderBase : IDisposable +{ + // External resolvers + private readonly Func _externalTilesetResolver; + private readonly Func _externalTemplateResolver; + private readonly Func _customTypeResolver; + + private readonly XmlReader _reader; + private bool disposedValue; + + /// + /// Constructs a new , which is the base class for all Tiled XML format readers. + /// + /// An XML reader for reading a Tiled map in the Tiled XML format. + /// A function that resolves external tilesets given their source. + /// A function that resolves external templates given their source. + /// A function that resolves custom types given their source. + /// Thrown when any of the arguments are null. + protected TmxReaderBase( + XmlReader reader, + Func externalTilesetResolver, + Func externalTemplateResolver, + Func customTypeResolver) + { + _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); + _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); + _customTypeResolver = customTypeResolver ?? throw new ArgumentNullException(nameof(customTypeResolver)); + + // Prepare reader + _ = _reader.MoveToContent(); + } + + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + _reader.Dispose(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + disposedValue = true; + } + } + + // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~TmxReaderBase() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs b/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs index f7a1565..3b442da 100644 --- a/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs +++ b/src/DotTiled/Serialization/Tmx/TsxTilesetReader.cs @@ -1,63 +1,25 @@ using System; -using System.Collections.Generic; using System.Xml; -using DotTiled.Model; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; namespace DotTiled.Serialization.Tmx; -public class TsxTilesetReader : ITilesetReader +/// +/// A tileset reader for the Tiled XML format. +/// +public class TsxTilesetReader : TmxReaderBase, ITilesetReader { - // External resolvers - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - + /// + /// Constructs a new . + /// + /// public TsxTilesetReader( XmlReader reader, + Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _reader = reader ?? throw new ArgumentNullException(nameof(reader)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } - // Prepare reader - _reader.MoveToContent(); - } - - public Tileset ReadTileset() => Tmx.ReadTileset(_reader, null, _externalTemplateResolver, _customTypeDefinitions); - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~TsxTilesetReader() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - System.GC.SuppressFinalize(this); - } + /// + public new Tileset ReadTileset() => base.ReadTileset(); } diff --git a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs index 3fa6c69..72da9dc 100644 --- a/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs +++ b/src/DotTiled/Serialization/Tmx/TxTemplateReader.cs @@ -1,66 +1,25 @@ using System; -using System.Collections.Generic; using System.Xml; -using DotTiled.Model; -using DotTiled.Model.Properties.CustomTypes; -using DotTiled.Model.Tilesets; namespace DotTiled.Serialization.Tmx; -public class TxTemplateReader : ITemplateReader +/// +/// A template reader for the Tiled XML format. +/// +public class TxTemplateReader : TmxReaderBase, ITemplateReader { - // Resolvers - private readonly Func _externalTilesetResolver; - private readonly Func _externalTemplateResolver; - - private readonly XmlReader _reader; - private bool disposedValue; - - private readonly IReadOnlyCollection _customTypeDefinitions; - + /// + /// Constructs a new . + /// + /// public TxTemplateReader( XmlReader reader, Func externalTilesetResolver, Func externalTemplateResolver, - IReadOnlyCollection customTypeDefinitions) - { - _reader = reader ?? throw new ArgumentNullException(nameof(reader)); - _externalTilesetResolver = externalTilesetResolver ?? throw new ArgumentNullException(nameof(externalTilesetResolver)); - _externalTemplateResolver = externalTemplateResolver ?? throw new ArgumentNullException(nameof(externalTemplateResolver)); - _customTypeDefinitions = customTypeDefinitions ?? throw new ArgumentNullException(nameof(customTypeDefinitions)); + Func customTypeResolver) : base( + reader, externalTilesetResolver, externalTemplateResolver, customTypeResolver) + { } - // Prepare reader - _reader.MoveToContent(); - } - - public Template ReadTemplate() => Tmx.ReadTemplate(_reader, _externalTilesetResolver, _externalTemplateResolver, _customTypeDefinitions); - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - { - if (disposing) - { - // TODO: dispose managed state (managed objects) - } - - // TODO: free unmanaged resources (unmanaged objects) and override finalizer - // TODO: set large fields to null - disposedValue = true; - } - } - - // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources - // ~TxTemplateReader() - // { - // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - // Dispose(disposing: false); - // } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - System.GC.SuppressFinalize(this); - } + /// + public new Template ReadTemplate() => base.ReadTemplate(); } diff --git a/src/DotTiled/Template.cs b/src/DotTiled/Template.cs new file mode 100644 index 0000000..56052de --- /dev/null +++ b/src/DotTiled/Template.cs @@ -0,0 +1,17 @@ +namespace DotTiled; + +/// +/// Represents a Tiled template. A template is a reusable object that can be placed in an inside the Tiled editor. +/// +public class Template +{ + /// + /// If the template represents a tile object, this property will contain the tileset that the tile belongs to. + /// + public Tileset? Tileset { get; set; } + + /// + /// The object that this template represents. + /// + public required Object Object { get; set; } +} diff --git a/src/DotTiled/Tilesets/Frame.cs b/src/DotTiled/Tilesets/Frame.cs new file mode 100644 index 0000000..ff37133 --- /dev/null +++ b/src/DotTiled/Tilesets/Frame.cs @@ -0,0 +1,17 @@ +namespace DotTiled; + +/// +/// A single frame of an animated tile. +/// +public class Frame +{ + /// + /// The local tile ID within the parent tileset. + /// + public required uint TileID { get; set; } + + /// + /// How long (in milliseconds) this frame should be displayed before advancing to the next frame. + /// + public required uint Duration { get; set; } +} diff --git a/src/DotTiled/Tilesets/Grid.cs b/src/DotTiled/Tilesets/Grid.cs new file mode 100644 index 0000000..c063dd1 --- /dev/null +++ b/src/DotTiled/Tilesets/Grid.cs @@ -0,0 +1,38 @@ +namespace DotTiled; + +/// +/// Orientation of the grid for the tiles in this tileset. +/// +public enum GridOrientation +{ + /// + /// The grid is orthogonal. + /// + Orthogonal, + + /// + /// The grid is isometric. + /// + Isometric +} + +/// +/// Used to specify how tile overlays for terrain and collision information are rendered in isometric maps. +/// +public class Grid +{ + /// + /// Orientation of the grid for the tiles in this tileset. + /// + public GridOrientation Orientation { get; set; } = GridOrientation.Orthogonal; + + /// + /// Width of a grid cell. + /// + public required uint Width { get; set; } + + /// + /// Height of a grid cell. + /// + public required uint Height { get; set; } +} diff --git a/src/DotTiled/Tilesets/Image.cs b/src/DotTiled/Tilesets/Image.cs new file mode 100644 index 0000000..cee67ff --- /dev/null +++ b/src/DotTiled/Tilesets/Image.cs @@ -0,0 +1,58 @@ +namespace DotTiled; + +/// +/// The format of an image. +/// +public enum ImageFormat +{ + /// + /// Portable Network Graphics. + /// + Png, + + /// + /// Graphics Interchange Format. + /// + Gif, + + /// + /// Joint Photographic Experts Group. + /// + Jpg, + + /// + /// Windows Bitmap. + /// + Bmp +} + +/// +/// Represents an image that is used by a tileset. +/// +public class Image +{ + /// + /// The format of the image. + /// + public ImageFormat? Format { get; set; } + + /// + /// The reference to the image file. + /// + public string? Source { get; set; } + + /// + /// Defines a specific color that is treated as transparent. + /// + public Color? TransparentColor { get; set; } + + /// + /// The image width in pixels, used for tile index correction when the image changes. + /// + public uint? Width { get; set; } + + /// + /// The image height in pixels, used for tile index correction when the image changes. + /// + public uint? Height { get; set; } +} diff --git a/src/DotTiled/Tilesets/Tile.cs b/src/DotTiled/Tilesets/Tile.cs new file mode 100644 index 0000000..835ecac --- /dev/null +++ b/src/DotTiled/Tilesets/Tile.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; + +namespace DotTiled; + +/// +/// Represents a single tile in a tileset, when using a collection of images to represent the tileset. +/// Tiled documentation for Tileset tiles +/// +public class Tile : HasPropertiesBase +{ + /// + /// The local tile ID within its tileset. + /// + public required uint ID { get; set; } + + /// + /// The class of the tile. Is inherited by tile objects + /// + public string Type { get; set; } = ""; + + /// + /// A percentage indicating the probability that this tile is chosen when it competes with others while editing with the terrain tool. + /// + public float Probability { get; set; } = 0f; + + /// + /// The X position of the sub-rectangle representing this tile within the tileset image. + /// + public uint X { get; set; } = 0; + + /// + /// The Y position of the sub-rectangle representing this tile within the tileset image. + /// + public uint Y { get; set; } = 0; + + /// + /// The width of the sub-rectangle representing this tile within the tileset image. + /// + public required uint Width { get; set; } + + /// + /// The height of the sub-rectangle representing this tile within the tileset image. + /// + public required uint Height { get; set; } + + /// + /// Tile properties. + /// + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; + + /// + /// The image representing this tile. Only used for tilesets that composed of a collection of images. + /// + public Image? Image { get; set; } + + /// + /// Unclear what this is for. + /// + public ObjectLayer? ObjectLayer { get; set; } + + /// + /// The animation frames for this tile. + /// + public List? Animation { get; set; } +} diff --git a/src/DotTiled/Tilesets/TileOffset.cs b/src/DotTiled/Tilesets/TileOffset.cs new file mode 100644 index 0000000..b446036 --- /dev/null +++ b/src/DotTiled/Tilesets/TileOffset.cs @@ -0,0 +1,17 @@ +namespace DotTiled; + +/// +/// Is used to specify an offset in pixels in tilesets, to be applied when drawing a tile from the related tileset. +/// +public class TileOffset +{ + /// + /// The horizontal offset in pixels. + /// + public float X { get; set; } = 0f; + + /// + /// The vertical offset in pixels. + /// + public float Y { get; set; } = 0f; +} diff --git a/src/DotTiled/Tilesets/Tileset.cs b/src/DotTiled/Tilesets/Tileset.cs new file mode 100644 index 0000000..0c2863f --- /dev/null +++ b/src/DotTiled/Tilesets/Tileset.cs @@ -0,0 +1,212 @@ +using System.Collections.Generic; + +namespace DotTiled; + +/// +/// The alignment of tile objects. +/// +public enum ObjectAlignment +{ + /// + /// The alignment is unspecified. Tile objects will use in orthogonal maps, and in isometric maps. + /// + Unspecified, + + /// + /// The tile object is aligned to the top left of the tile. + /// + TopLeft, + + /// + /// The tile object is aligned to the top of the tile. + /// + Top, + + /// + /// The tile object is aligned to the top right of the tile. + /// + TopRight, + + /// + /// The tile object is aligned to the left of the tile. + /// + Left, + + /// + /// The tile object is aligned to the center of the tile. + /// + Center, + + /// + /// The tile object is aligned to the right of the tile. + /// + Right, + + /// + /// The tile object is aligned to the bottom left of the tile. + /// + BottomLeft, + + /// + /// The tile object is aligned to the bottom of the tile. + /// + Bottom, + + /// + /// The tile object is aligned to the bottom right of the tile. + /// + BottomRight +} + +/// +/// The size to use when rendering tiles from a tileset on a tile layer. +/// +public enum TileRenderSize +{ + /// + /// The tile is drawn at the size of the tile in the tileset. + /// + Tile, + + /// + /// The tile is drawn at the tile grid size of the map. + /// + Grid +} + +/// +/// Determines how a tile is rendered in a tile set. +/// +public enum FillMode +{ + /// + /// The tile is stretched to fill the tile size, possibly distorting the tile. + /// + Stretch, + + /// + /// The tile's aspect ratio is preserved, and it is scaled to fit within the tile size. + /// + PreserveAspectFit +} + +/// +/// A tileset is a collection of tiles that can be used in a tile layer, or by tile objects. +/// +public class Tileset : HasPropertiesBase +{ + /// + /// The TMX format version. Is incremented to match minor Tiled releases. + /// + public string? Version { get; set; } + + /// + /// The Tiled version used to save the file in case it was loaded from an external tileset file. + /// + public string? TiledVersion { get; set; } + + /// + /// The first global tile ID of this tileset (this global ID maps to the first tile in this tileset). + /// + public uint? FirstGID { get; set; } + + /// + /// If this tileset is stored in an external TSX (Tile Set XML) file, this attribute refers to that file. + /// + public string? Source { get; set; } + + /// + /// The name of this tileset. + /// + public string? Name { get; set; } + + /// + /// The class of this tileset. + /// + public string Class { get; set; } = ""; + + /// + /// The width of the tiles in this tileset, which should be at least 1 (non-zero) except in the case of image collection tilesets (in which case it stores the maximum tile width). + /// + public uint? TileWidth { get; set; } + + /// + /// The height of the tiles in this tileset, which should be at least 1 (non-zero) except in the case of image collection tilesets (in which case it stores the maximum tile height). + /// + public uint? TileHeight { get; set; } + + /// + /// The spacing in pixels between the tiles in this tileset (applies to the tileset image). Irrelevant for image collection tilesets. + /// + public float? Spacing { get; set; } = 0f; + + /// + /// The margin around the tiles in this tileset (applies to the tileset image). Irrelevant for image collection tilesets. + /// + public float? Margin { get; set; } = 0f; + + /// + /// The number of tiles in this tileset. + /// + public uint? TileCount { get; set; } + + /// + /// The number of tile columns in the tileset. + /// + public uint? Columns { get; set; } + + /// + /// Controls the aligntment for tile objects. + /// + public ObjectAlignment ObjectAlignment { get; set; } = ObjectAlignment.Unspecified; + + /// + /// The size to use when rendering tiles from thie tileset on a tile layer. When set to , the tile is drawn at the tile grid size of the map. + /// + public TileRenderSize RenderSize { get; set; } = TileRenderSize.Tile; + + /// + /// The fill mode to use when rendering tiles from this tileset. + /// + public FillMode FillMode { get; set; } = FillMode.Stretch; + + /// + /// If the tileset is based on a single image, which is cut into tiles based on the given attributes of the tileset, then this is that image. + /// + public Image? Image { get; set; } + + /// + /// This is used to specify an offset in pixels, to be applied when drawing a tile from the related tileset. When not present, no offset is applied. + /// + public TileOffset? TileOffset { get; set; } + + /// + /// Ths is only used in case of isometric orientation, and determines how tile overlays for terrain and collision information are rendered. + /// + public Grid? Grid { get; set; } + + /// + /// Tileset properties. + /// + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; + + // public List? TerrainTypes { get; set; } TODO: Implement Terrain -> Wangset conversion during deserialization + + /// + /// Contains the list of Wang sets defined for this tileset. + /// + public List? Wangsets { get; set; } + + /// + /// Used to describe which transformations can be applied to the tiles (e.g. to extend a Wang set by transforming existing tiles). + /// + public Transformations? Transformations { get; set; } + + /// + /// If this tileset is based on a collection of images, then this list of tiles will contain the individual images that make up the tileset. + /// + public List Tiles { get; set; } = []; +} diff --git a/src/DotTiled/Tilesets/Transformations.cs b/src/DotTiled/Tilesets/Transformations.cs new file mode 100644 index 0000000..8ed7c36 --- /dev/null +++ b/src/DotTiled/Tilesets/Transformations.cs @@ -0,0 +1,27 @@ +namespace DotTiled; + +/// +/// Represents which transformations can be applied to a tile in a tileset. +/// +public class Transformations +{ + /// + /// Whether the file in this can set be flipped horizontally. + /// + public bool HFlip { get; set; } = false; + + /// + /// Whether the file in this can set be flipped vertically. + /// + public bool VFlip { get; set; } = false; + + /// + /// Whether the file in this set can be rotated in 90 degree increments. + /// + public bool Rotate { get; set; } = false; + + /// + /// Whether untransformed tiles remain preferred, otherwise transformed tiles are used to produce more vartiations. + /// + public bool PreferUntransformed { get; set; } = false; +} diff --git a/src/DotTiled/Tilesets/WangColor.cs b/src/DotTiled/Tilesets/WangColor.cs new file mode 100644 index 0000000..b5ef482 --- /dev/null +++ b/src/DotTiled/Tilesets/WangColor.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace DotTiled; + +/// +/// Represents a Wang color in a Wang set. +/// +public class WangColor : HasPropertiesBase +{ + /// + /// The name of this color. + /// + public required string Name { get; set; } + + /// + /// The class of the Wang color. + /// + public string Class { get; set; } = ""; + + /// + /// The color of the Wang color. + /// + public required Color Color { get; set; } + + /// + /// The tile ID of the tile representing this color. + /// + public required int Tile { get; set; } + + /// + /// The relative probability that this color is chosen over others in case of multiple options. + /// + public float Probability { get; set; } = 0f; + + /// + /// The Wang color properties. + /// + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; +} diff --git a/src/DotTiled/Tilesets/WangTile.cs b/src/DotTiled/Tilesets/WangTile.cs new file mode 100644 index 0000000..0432cdd --- /dev/null +++ b/src/DotTiled/Tilesets/WangTile.cs @@ -0,0 +1,17 @@ +namespace DotTiled; + +/// +/// Represents a Wang tile in a Wang set. +/// +public class WangTile +{ + /// + /// The tile ID associated with this Wang tile. + /// + public required uint TileID { get; set; } + + /// + /// The Wang ID of this Wang tile. + /// + public required byte[] WangID { get; set; } +} diff --git a/src/DotTiled/Tilesets/Wangset.cs b/src/DotTiled/Tilesets/Wangset.cs new file mode 100644 index 0000000..16ceb56 --- /dev/null +++ b/src/DotTiled/Tilesets/Wangset.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace DotTiled; + +/// +/// Defines a list of colors and any number of Wang tiles using these colors. +/// +public class Wangset : HasPropertiesBase +{ + /// + /// The name of the Wang set. + /// + public required string Name { get; set; } + + /// + /// The class of the Wang set. + /// + public string Class { get; set; } = ""; + + /// + /// The tile ID of the tile representing the Wang set. + /// + public required int Tile { get; set; } + + /// + /// The Wang set properties. + /// + public List Properties { get; set; } = []; + + /// + public override IList GetProperties() => Properties; + + // Up to 254 Wang colors + /// + /// The Wang colors in the Wang set. + /// + public List? WangColors { get; set; } = []; + + /// + /// The Wang tiles in the Wang set. + /// + public List WangTiles { get; set; } = []; +}