Make Optional<T> into readonly struct to avoid heap allocations

This commit is contained in:
Daniel Cronqvist 2024-12-02 22:45:14 +01:00
parent 21470cb056
commit 9aa2c63e0b
35 changed files with 176 additions and 203 deletions

View file

@ -79,6 +79,6 @@ public class Program
[
new CustomClassDefinition() { Name = "a" },
];
return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional<ICustomTypeDefinition>(ctd) : Optional<ICustomTypeDefinition>.Empty;
return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional<ICustomTypeDefinition>(ctd) : Optional.Empty;
}
}

View file

@ -42,10 +42,10 @@ public partial class MapParser : Node2D
private Tileset ResolveTileset(string source)
{
string tilesetString = FileAccess.Open($"res://{source}", FileAccess.ModeFlags.Read).GetAsText();
using TilesetReader tilesetReader =
new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset();
string tilesetString = FileAccess.Open($"res://{source}", FileAccess.ModeFlags.Read).GetAsText();
using TilesetReader tilesetReader =
new TilesetReader(tilesetString, ResolveTileset, ResolveTemplate, ResolveCustomType);
return tilesetReader.ReadTileset();
}
private Template ResolveTemplate(string source)
@ -62,6 +62,6 @@ public partial class MapParser : Node2D
[
new CustomClassDefinition() { Name = "a" },
];
return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional<ICustomTypeDefinition>(ctd) : Optional<ICustomTypeDefinition>.Empty;
return allDefinedTypes.FirstOrDefault(ctd => ctd.Name == name) is ICustomTypeDefinition ctd ? new Optional<ICustomTypeDefinition>(ctd) : Optional.Empty;
}
}

View file

@ -35,8 +35,7 @@ public static partial class DotTiledAssert
AssertEqual(expected.X, actual.X, nameof(TileLayer.X));
AssertEqual(expected.Y, actual.Y, nameof(TileLayer.Y));
Assert.NotNull(actual.Data);
AssertData(expected.Data, actual.Data);
AssertOptionalsEqual(expected.Data, actual.Data, nameof(TileLayer.Data), AssertData);
}
private static void AssertLayer(ObjectLayer expected, ObjectLayer actual)
@ -60,8 +59,7 @@ public static partial class DotTiledAssert
AssertEqual(expected.X, actual.X, nameof(ImageLayer.X));
AssertEqual(expected.Y, actual.Y, nameof(ImageLayer.Y));
Assert.NotNull(actual.Image);
AssertImage(expected.Image, actual.Image);
AssertOptionalsEqual(expected.Image, actual.Image, nameof(ImageLayer.Image), AssertImage);
}
private static void AssertLayer(Group expected, Group actual)

View file

@ -33,12 +33,6 @@ public static partial class DotTiledAssert
string nameof,
Action<T, T> assertEqual)
{
if (expected is null)
{
Assert.Null(actual);
return;
}
if (expected.HasValue)
{
Assert.True(actual.HasValue, $"Expected {nameof} to have a value");
@ -51,12 +45,6 @@ public static partial class DotTiledAssert
internal static void AssertEqual<T>(Optional<T> expected, Optional<T> actual, string nameof)
{
if (expected is null)
{
Assert.Null(actual);
return;
}
if (expected.HasValue)
{
Assert.True(actual.HasValue, $"Expected {nameof} to have a value");

View file

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

View file

@ -59,30 +59,6 @@ public class OptionalTests
Assert.Equal(expectedValue, optional.Value);
}
[Fact]
public void ImplicitConversion_FromOptionalToValue_ShouldReturnValue_WhenHasValueIsTrue()
{
// Arrange
int expectedValue = 10;
var optional = new Optional<int>(expectedValue);
// Act
int value = optional;
// Assert
Assert.Equal(expectedValue, value);
}
[Fact]
public void ImplicitConversion_FromOptionalToValue_ShouldThrowException_WhenHasValueIsFalse()
{
// Arrange
var optional = new Optional<int>();
// Act & Assert
_ = Assert.Throws<InvalidOperationException>(() => { int value = optional; });
}
// ToString Method Tests
[Fact]
@ -237,18 +213,6 @@ public class OptionalTests
_ = Assert.Throws<InvalidOperationException>(() => optional.Value);
}
[Fact]
public void ImplicitConversion_WhenHasValueIsFalse_ShouldThrowInvalidOperationException()
{
// Arrange
var optional = new Optional<int>();
// Act & Assert
_ = Assert.Throws<InvalidOperationException>(() => { int value = optional; });
}
// Edge Cases
[Fact]
public void EmptyOptionalEquality_ShouldReturnTrue()
{

View file

@ -71,8 +71,8 @@ public class LoaderTests
""");
var resourceCache = Substitute.For<IResourceCache>();
resourceCache.GetTileset(Arg.Any<string>()).Returns(Optional<Tileset>.Empty);
resourceCache.GetTemplate(Arg.Any<string>()).Returns(Optional<Template>.Empty);
resourceCache.GetTileset(Arg.Any<string>()).Returns(Optional.Empty);
resourceCache.GetTemplate(Arg.Any<string>()).Returns(Optional.Empty);
var customTypeDefinitions = Enumerable.Empty<ICustomTypeDefinition>();
var loader = new Loader(resourceReader, resourceCache, customTypeDefinitions);
@ -134,8 +134,8 @@ public class LoaderTests
""");
var resourceCache = Substitute.For<IResourceCache>();
resourceCache.GetTileset(Arg.Any<string>()).Returns(Optional<Tileset>.Empty);
resourceCache.GetTemplate(Arg.Any<string>()).Returns(Optional<Template>.Empty);
resourceCache.GetTileset(Arg.Any<string>()).Returns(Optional.Empty);
resourceCache.GetTemplate(Arg.Any<string>()).Returns(Optional.Empty);
var customTypeDefinitions = Enumerable.Empty<ICustomTypeDefinition>();
var loader = new Loader(resourceReader, resourceCache, customTypeDefinitions);
@ -219,7 +219,7 @@ public class LoaderTests
""");
var resourceCache = Substitute.For<IResourceCache>();
resourceCache.GetTileset(Arg.Any<string>()).Returns(Optional<Tileset>.Empty);
resourceCache.GetTileset(Arg.Any<string>()).Returns(Optional.Empty);
resourceCache.GetTemplate("template.tx").Returns(new Optional<Template>(new Template
{
Object = new PolygonObject

View file

@ -39,7 +39,7 @@ public partial class MapReaderTests
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
return Optional.Empty;
}
using var mapReader = new MapReader(mapString, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -35,7 +35,7 @@ public partial class TmjMapReaderTests
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
return Optional.Empty;
}
using var mapReader = new TmjMapReader(json, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -35,7 +35,7 @@ public partial class TmxMapReaderTests
return new Optional<ICustomTypeDefinition>(ctd);
}
return Optional<ICustomTypeDefinition>.Empty;
return Optional.Empty;
}
using var mapReader = new TmxMapReader(reader, ResolveTileset, ResolveTemplate, ResolveCustomType);

View file

@ -37,7 +37,7 @@ public abstract class BaseLayer : HasPropertiesBase
/// <summary>
/// A tint color that is multiplied with any tiles drawn by this layer.
/// </summary>
public Optional<TiledColor> TintColor { get; set; } = Optional<TiledColor>.Empty;
public Optional<TiledColor> TintColor { get; set; } = Optional.Empty;
/// <summary>
/// Horizontal offset for this layer in pixels.

View file

@ -118,27 +118,27 @@ public class Data
/// <summary>
/// The encoding used to encode the tile layer data.
/// </summary>
public Optional<DataEncoding> Encoding { get; set; } = Optional<DataEncoding>.Empty;
public Optional<DataEncoding> Encoding { get; set; } = Optional.Empty;
/// <summary>
/// The compression method used to compress the tile layer data.
/// </summary>
public Optional<DataCompression> Compression { get; set; } = Optional<DataCompression>.Empty;
public Optional<DataCompression> Compression { get; set; } = Optional.Empty;
/// <summary>
/// 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
/// <see href="https://doc.mapeditor.org/en/stable/reference/global-tile-ids/#mapping-a-gid-to-a-local-tile-id">the documentation on how to do this</see>.
/// </summary>
public Optional<uint[]> GlobalTileIDs { get; set; } = Optional<uint[]>.Empty;
public Optional<uint[]> GlobalTileIDs { get; set; } = Optional.Empty;
/// <summary>
/// The parsed flipping flags for each tile in the layer. Appear in the same order as the tiles in the layer in <see cref="GlobalTileIDs"/>.
/// </summary>
public Optional<FlippingFlags[]> FlippingFlags { get; set; } = Optional<FlippingFlags[]>.Empty;
public Optional<FlippingFlags[]> FlippingFlags { get; set; } = Optional.Empty;
/// <summary>
/// If the map is infinite, it will instead contain a list of chunks.
/// </summary>
public Optional<Chunk[]> Chunks { get; set; } = Optional<Chunk[]>.Empty;
public Optional<Chunk[]> Chunks { get; set; } = Optional.Empty;
}

View file

@ -46,7 +46,7 @@ public class ObjectLayer : BaseLayer
/// <summary>
/// A color that is multiplied with any tile objects drawn by this layer.
/// </summary>
public Optional<TiledColor> Color { get; set; } = Optional<TiledColor>.Empty;
public Optional<TiledColor> Color { get; set; } = Optional.Empty;
/// <summary>
/// Whether the objects are drawn according to the order of appearance (<see cref="DrawOrder.Index"/>) or sorted by their Y coordinate (<see cref="DrawOrder.TopDown"/>).

View file

@ -10,7 +10,7 @@ public abstract class Object : HasPropertiesBase
/// <summary>
/// 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.
/// </summary>
public Optional<uint> ID { get; set; } = Optional<uint>.Empty;
public Optional<uint> ID { get; set; } = Optional.Empty;
/// <summary>
/// The name of the object. An arbitrary string.
@ -55,7 +55,7 @@ public abstract class Object : HasPropertiesBase
/// <summary>
/// A reference to a template file.
/// </summary>
public Optional<string> Template { get; set; } = Optional<string>.Empty;
public Optional<string> Template { get; set; } = Optional.Empty;
/// <summary>
/// Object properties.

View file

@ -30,7 +30,7 @@ public class TileLayer : BaseLayer
/// <summary>
/// The tile layer data.
/// </summary>
public Optional<Data> Data { get; set; } = Optional<Data>.Empty;
public Optional<Data> Data { get; set; } = Optional.Empty;
/// <summary>
/// Helper method to retrieve the Global Tile ID at a given coordinate in the layer.

View file

@ -101,7 +101,7 @@ public class Map : HasPropertiesBase
/// <summary>
/// The Tiled version used to save the file.
/// </summary>
public Optional<string> TiledVersion { get; set; } = Optional<string>.Empty;
public Optional<string> TiledVersion { get; set; } = Optional.Empty;
/// <summary>
/// The class of this map.
@ -147,17 +147,17 @@ public class Map : HasPropertiesBase
/// <summary>
/// Only for hexagonal maps. Determines the width or height (depending on the staggered axis) of the tile's edge, in pixels.
/// </summary>
public Optional<int> HexSideLength { get; set; } = Optional<int>.Empty;
public Optional<int> HexSideLength { get; set; } = Optional.Empty;
/// <summary>
/// For staggered and hexagonal maps, determines which axis (X or Y) is staggered.
/// </summary>
public Optional<StaggerAxis> StaggerAxis { get; set; } = Optional<StaggerAxis>.Empty;
public Optional<StaggerAxis> StaggerAxis { get; set; } = Optional.Empty;
/// <summary>
/// For staggered and hexagonal maps, determines whether the "even" or "odd" indexes along the staggered axis are shifted.
/// </summary>
public Optional<StaggerIndex> StaggerIndex { get; set; } = Optional<StaggerIndex>.Empty;
public Optional<StaggerIndex> StaggerIndex { get; set; } = Optional.Empty;
/// <summary>
/// X coordinate of the parallax origin in pixels.

View file

@ -2,62 +2,94 @@ using System;
namespace DotTiled;
/// <summary>
/// Represents an empty value.
/// </summary>
public readonly struct OptionalEmpty;
/// <summary>
/// Represents an empty <see cref="Optional{T}"/> object.
/// </summary>
public static class Optional
{
/// <summary>
/// Represents an empty <see cref="Optional{T}"/> object.
/// </summary>
public static readonly OptionalEmpty Empty = default;
}
/// <summary>
/// Represents a value that may or may not be present.
/// </summary>
/// <typeparam name="T">The type of the optionally present value.</typeparam>
public class Optional<T>
public readonly struct Optional<T>
{
#pragma warning disable IDE0032 // Use auto property
private readonly bool _hasValue;
private readonly T _value;
/// <summary>
/// Gets a value indicating whether the current <see cref="Optional{T}"/> object has a value.
/// Returns <see langword="true"/> if the <see cref="Value"/> will return a meaningful value.
/// </summary>
public bool HasValue { get; }
/// <returns></returns>
public bool HasValue => _hasValue;
#pragma warning restore IDE0032 // Use auto property
/// <summary>
/// Gets the value of the current <see cref="Optional{T}"/> object if it has been set; otherwise, throws an exception.
/// Constructs an <see cref="Optional{T}"/> with a meaningful value.
/// </summary>
/// <param name="value"></param>
public Optional(T value)
{
_hasValue = true;
_value = value;
}
/// <summary>
/// Gets the value of the current object. Not meaningful unless <see cref="HasValue"/> returns <see langword="true"/>.
/// </summary>
/// <remarks>
/// <para>Unlike <see cref="Nullable{T}.Value"/>, this property does not throw an exception when
/// <see cref="HasValue"/> is <see langword="false"/>.</para>
/// </remarks>
/// <returns>
/// <para>The value if <see cref="HasValue"/> is <see langword="true"/>; otherwise, the default value for type
/// <typeparamref name="T"/>.</para>
/// </returns>
public T Value => HasValue ? _value : throw new InvalidOperationException("Value is not set");
/// <summary>
/// Initializes a new instance of the <see cref="Optional{T}"/> class with the specified value.
/// Creates a new object initialized to a meaningful value.
/// </summary>
/// <param name="value">The value to be set.</param>
public Optional(T value)
{
_value = value;
HasValue = true;
}
/// <summary>
/// Initializes a new instance of the <see cref="Optional{T}"/> class with no value.
/// </summary>
public Optional()
{
_value = default!;
HasValue = false;
}
/// <summary>
/// Implicitly converts a value to an <see cref="Optional{T}"/> object.
/// </summary>
/// <param name="value">The value to be converted.</param>
/// <param name="value"></param>
public static implicit operator Optional<T>(T value)
{
if (value is null)
return new();
{
return default;
}
return new(value);
return new Optional<T>(value);
}
/// <summary>
/// Implicitly converts an <see cref="Optional{T}"/> object to a value.
/// Creates a new object initialized to an empty value.
/// </summary>
/// <param name="optional">The <see cref="Optional{T}"/> object to be converted.</param>
public static implicit operator T(Optional<T> optional)
/// <param name="_"></param>
public static implicit operator Optional<T>(OptionalEmpty _)
{
return optional.Value;
return default;
}
/// <summary>
/// Returns a string representation of this object.
/// </summary>
public override string ToString()
{
// Note: For nullable types, it's possible to have _hasValue true and _value null.
return _hasValue
? _value?.ToString() ?? "null"
: "Empty";
}
/// <summary>
@ -71,17 +103,6 @@ public class Optional<T>
return left.Equals(right);
}
/// <summary>
/// Determines whether the specified <see cref="Optional{T}"/> objects are not equal.
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns></returns>
public static bool operator !=(Optional<T> left, Optional<T> right)
{
return !left.Equals(right);
}
/// <summary>
/// Returns the value of the current <see cref="Optional{T}"/> object if it has been set; otherwise, returns the specified default value.
/// </summary>
@ -96,9 +117,6 @@ public class Optional<T>
/// <returns></returns>
public Optional<T> GetValueOrOptional(Optional<T> defaultValue) => HasValue ? this : defaultValue;
/// <inheritdoc />
public override string ToString() => HasValue ? _value.ToString() : "Empty";
/// <inheritdoc />
public override bool Equals(object obj)
{
@ -129,9 +147,13 @@ public class Optional<T>
public override int GetHashCode() => HasValue ? _value!.GetHashCode() : 0;
/// <summary>
/// Represents an empty <see cref="Optional{T}"/> object.
/// Determines whether the specified <see cref="Optional{T}"/> objects are not equal.
/// </summary>
#pragma warning disable CA1000 // Do not declare static members on generic types
public static Optional<T> Empty => new();
#pragma warning restore CA1000 // Do not declare static members on generic types
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns></returns>
public static bool operator !=(Optional<T> left, Optional<T> right)
{
return !left.Equals(right);
}
}

View file

@ -16,7 +16,7 @@ public class DefaultResourceCache : IResourceCache
if (_templates.TryGetValue(path, out var template))
return new Optional<Template>(template);
return Optional<Template>.Empty;
return Optional.Empty;
}
/// <inheritdoc/>
@ -25,7 +25,7 @@ public class DefaultResourceCache : IResourceCache
if (_tilesets.TryGetValue(path, out var tileset))
return new Optional<Tileset>(tileset);
return Optional<Tileset>.Empty;
return Optional.Empty;
}
/// <inheritdoc/>

View file

@ -91,7 +91,7 @@ internal static partial class Helpers
if (string.IsNullOrWhiteSpace(className))
return null;
var customType = customTypeResolver(className) ?? throw new InvalidOperationException($"Could not resolve custom type '{className}'.");
var customType = customTypeResolver(className);
if (!customType.HasValue)
return null;

View file

@ -16,7 +16,7 @@ public interface IResourceCache
/// Retrieves a tileset from the cache with the given <paramref name="path"/>.
/// </summary>
/// <param name="path">The path to the tileset file.</param>
/// <returns>The tileset if it exists in the cache; otherwise, <see cref="Optional{Tileset}.Empty"/>.</returns>
/// <returns>The tileset if it exists in the cache; otherwise, <see cref="Optional.Empty"/>.</returns>
Optional<Tileset> GetTileset(string path);
/// <summary>
@ -30,6 +30,6 @@ public interface IResourceCache
/// Retrieves a template from the cache with the given <paramref name="path"/>.
/// </summary>
/// <param name="path">The path to the template file.</param>
/// <returns>The template if it exists in the cache; otherwise, <see cref="Optional{Template}.Empty"/>.</returns>
/// <returns>The template if it exists in the cache; otherwise, <see cref="Optional.Empty"/>.</returns>
Optional<Template> GetTemplate(string path);
}

View file

@ -119,6 +119,6 @@ public class Loader
if (_customTypeDefinitions.TryGetValue(name, out var customTypeDefinition))
return new Optional<ICustomTypeDefinition>(customTypeDefinition);
return Optional<ICustomTypeDefinition>.Empty;
return Optional.Empty;
}
}

View file

@ -18,10 +18,10 @@ internal static class ExtensionsJsonElement
internal static Optional<T> GetOptionalProperty<T>(this JsonElement element, string propertyName)
{
if (!element.TryGetProperty(propertyName, out var property))
return Optional<T>.Empty;
return Optional.Empty;
if (property.ValueKind == JsonValueKind.Null)
return Optional<T>.Empty;
return Optional.Empty;
return property.GetValueAs<T>();
}
@ -67,15 +67,15 @@ internal static class ExtensionsJsonElement
internal static Optional<T> GetOptionalPropertyParseable<T>(this JsonElement element, string propertyName) where T : IParsable<T>
{
if (!element.TryGetProperty(propertyName, out var property))
return Optional<T>.Empty;
return Optional.Empty;
return T.Parse(property.GetString()!, CultureInfo.InvariantCulture);
}
internal static Optional<T> GetOptionalPropertyParseable<T>(this JsonElement element, string propertyName, Func<string, T> parser)
internal static Optional<T> GetOptionalPropertyParseable<T>(this JsonElement element, string propertyName, Func<string, Optional<T>> parser)
{
if (!element.TryGetProperty(propertyName, out var property))
return Optional<T>.Empty;
return Optional.Empty;
return parser(property.GetString()!);
}
@ -91,7 +91,7 @@ internal static class ExtensionsJsonElement
internal static Optional<T> GetOptionalPropertyCustom<T>(this JsonElement element, string propertyName, Func<JsonElement, T> parser)
{
if (!element.TryGetProperty(propertyName, out var property))
return Optional<T>.Empty;
return Optional.Empty;
return parser(property);
}

View file

@ -32,8 +32,8 @@ public abstract partial class TmjReaderBase
Y = y,
Width = width,
Height = height,
GlobalTileIDs = data.GlobalTileIDs,
FlippingFlags = data.FlippingFlags
GlobalTileIDs = data.GlobalTileIDs.Value,
FlippingFlags = data.FlippingFlags.Value
};
}

View file

@ -61,7 +61,7 @@ public abstract partial class TmjReaderBase
internal DotTiled.Object ReadObject(JsonElement element)
{
Optional<uint> idDefault = Optional<uint>.Empty;
Optional<uint> idDefault = Optional.Empty;
string nameDefault = "";
string typeDefault = "";
float xDefault = 0f;
@ -80,7 +80,7 @@ public abstract partial class TmjReaderBase
var template = element.GetOptionalProperty<string>("template");
if (template.HasValue)
{
var resolvedTemplate = _externalTemplateResolver(template);
var resolvedTemplate = _externalTemplateResolver(template.Value);
var templObj = resolvedTemplate.Object;
idDefault = templObj.ID;
@ -225,7 +225,7 @@ public abstract partial class TmjReaderBase
text.Value.Visible = visible;
text.Value.Template = template;
text.Value.Properties = properties;
return text;
return text.Value;
}
return new RectangleObject

View file

@ -16,7 +16,7 @@ public abstract partial class TmjReaderBase
{
"zlib" => DataCompression.ZLib,
"gzip" => DataCompression.GZip,
"" => Optional<DataCompression>.Empty,
"" => Optional.Empty,
_ => throw new JsonException($"Unsupported compression '{s}'.")
});
var chunks = element.GetOptionalPropertyCustom<Data>("chunks", e => ReadDataAsChunks(e, compression, encoding));
@ -62,7 +62,7 @@ public abstract partial class TmjReaderBase
Y = y,
Width = width,
Height = height,
Data = data ?? chunks
Data = data.GetValueOrOptional(chunks)
};
}
}

View file

@ -7,25 +7,35 @@ public abstract partial class TmjReaderBase
{
internal Tileset ReadTileset(
JsonElement element,
Optional<string> parentVersion = null,
Optional<string> parentTiledVersion = null)
Optional<string> parentVersion = default,
Optional<string> parentTiledVersion = default)
{
var source = element.GetOptionalProperty<string>("source");
var firstGID = element.GetOptionalProperty<uint>("firstgid");
if (source.HasValue)
{
var resolvedTileset = _externalTilesetResolver(source.Value);
resolvedTileset.FirstGID = firstGID;
resolvedTileset.Source = source;
return resolvedTileset;
}
var backgroundColor = element.GetOptionalPropertyParseable<TiledColor>("backgroundcolor");
var @class = element.GetOptionalProperty<string>("class").GetValueOr("");
var columns = element.GetOptionalProperty<int>("columns");
var columns = element.GetRequiredProperty<int>("columns");
var fillMode = element.GetOptionalPropertyParseable<FillMode>("fillmode", s => s switch
{
"stretch" => FillMode.Stretch,
"preserve-aspect-fit" => FillMode.PreserveAspectFit,
_ => throw new JsonException($"Unknown fill mode '{s}'")
}).GetValueOr(FillMode.Stretch);
var firstGID = element.GetOptionalProperty<uint>("firstgid");
var grid = element.GetOptionalPropertyCustom<Grid>("grid", ReadGrid);
var image = element.GetOptionalProperty<string>("image");
var imageHeight = element.GetOptionalProperty<int>("imageheight");
var imageWidth = element.GetOptionalProperty<int>("imagewidth");
var margin = element.GetOptionalProperty<int>("margin");
var name = element.GetOptionalProperty<string>("name");
var margin = element.GetRequiredProperty<int>("margin");
var name = element.GetRequiredProperty<string>("name");
var objectAlignment = element.GetOptionalPropertyParseable<ObjectAlignment>("objectalignment", s => s switch
{
"unspecified" => ObjectAlignment.Unspecified,
@ -41,11 +51,10 @@ public abstract partial class TmjReaderBase
_ => throw new JsonException($"Unknown object alignment '{s}'")
}).GetValueOr(ObjectAlignment.Unspecified);
var properties = ResolveAndMergeProperties(@class, element.GetOptionalPropertyCustom("properties", ReadProperties).GetValueOr([]));
var source = element.GetOptionalProperty<string>("source");
var spacing = element.GetOptionalProperty<int>("spacing");
var tileCount = element.GetOptionalProperty<int>("tilecount");
var spacing = element.GetRequiredProperty<int>("spacing");
var tileCount = element.GetRequiredProperty<int>("tilecount");
var tiledVersion = element.GetOptionalProperty<string>("tiledversion").GetValueOrOptional(parentTiledVersion);
var tileHeight = element.GetOptionalProperty<int>("tileheight");
var tileHeight = element.GetRequiredProperty<int>("tileheight");
var tileOffset = element.GetOptionalPropertyCustom<TileOffset>("tileoffset", ReadTileOffset);
var tileRenderSize = element.GetOptionalPropertyParseable<TileRenderSize>("tilerendersize", s => s switch
{
@ -54,28 +63,20 @@ public abstract partial class TmjReaderBase
_ => throw new JsonException($"Unknown tile render size '{s}'")
}).GetValueOr(TileRenderSize.Tile);
var tiles = element.GetOptionalPropertyCustom<List<Tile>>("tiles", ReadTiles).GetValueOr([]);
var tileWidth = element.GetOptionalProperty<int>("tilewidth");
var tileWidth = element.GetRequiredProperty<int>("tilewidth");
var transparentColor = element.GetOptionalPropertyParseable<TiledColor>("transparentcolor");
var version = element.GetOptionalProperty<string>("version").GetValueOrOptional(parentVersion);
var transformations = element.GetOptionalPropertyCustom<Transformations>("transformations", ReadTransformations);
var wangsets = element.GetOptionalPropertyCustom<List<Wangset>>("wangsets", el => el.GetValueAsList<Wangset>(e => ReadWangset(e))).GetValueOr([]);
if (source.HasValue)
{
var resolvedTileset = _externalTilesetResolver(source);
resolvedTileset.FirstGID = firstGID;
resolvedTileset.Source = source;
return resolvedTileset;
}
Optional<Image> imageModel = image.HasValue ? new Image
{
Format = Helpers.ParseImageFormatFromSource(image),
Format = Helpers.ParseImageFormatFromSource(image.Value),
Source = image,
Height = imageHeight,
Width = imageWidth,
TransparentColor = transparentColor
} : Optional<Image>.Empty;
} : Optional.Empty;
return new Tileset
{
@ -170,11 +171,11 @@ public abstract partial class TmjReaderBase
Optional<Image> imageModel = image.HasValue ? new Image
{
Format = Helpers.ParseImageFormatFromSource(image),
Format = Helpers.ParseImageFormatFromSource(image.Value),
Source = image,
Height = imageHeight ?? 0,
Width = imageWidth ?? 0
} : Optional<Image>.Empty;
Height = imageHeight.GetValueOr(0),
Width = imageWidth.GetValueOr(0)
} : Optional.Empty;
return new Tile
{

View file

@ -64,10 +64,10 @@ public abstract partial class TmxReaderBase
internal static uint[] ReadTileChildrenInWrapper(string wrapper, XmlReader reader) =>
reader.ReadList(wrapper, "tile", (r) => r.GetOptionalAttributeParseable<uint>("gid").GetValueOr(0)).ToArray();
internal static uint[] ReadRawData(XmlReader reader, DataEncoding encoding, Optional<DataCompression> compression)
internal static uint[] ReadRawData(XmlReader reader, Optional<DataEncoding> encoding, Optional<DataCompression> compression)
{
var data = reader.ReadElementContentAsString();
if (encoding == DataEncoding.Csv)
if (encoding.Value == DataEncoding.Csv)
return ParseCsvData(data);
using var bytes = new MemoryStream(Convert.FromBase64String(data));

View file

@ -74,7 +74,7 @@ public abstract partial class TmxReaderBase
var template = _reader.GetOptionalAttribute("template");
DotTiled.Object obj = null;
if (template.HasValue)
obj = _externalTemplateResolver(template).Object;
obj = _externalTemplateResolver(template.Value).Object;
uint idDefault = obj?.ID.GetValueOr(0) ?? 0;
string nameDefault = obj?.Name ?? "";
@ -84,7 +84,7 @@ public abstract partial class TmxReaderBase
float widthDefault = obj?.Width ?? 0f;
float heightDefault = obj?.Height ?? 0f;
float rotationDefault = obj?.Rotation ?? 0f;
Optional<uint> gidDefault = obj is TileObject tileObj ? tileObj.GID : Optional<uint>.Empty;
Optional<uint> gidDefault = obj is TileObject tileObj ? tileObj.GID : Optional.Empty;
bool visibleDefault = obj?.Visible ?? true;
List<IProperty> propertiesDefault = obj?.Properties ?? null;

View file

@ -49,7 +49,7 @@ public abstract partial class TmxReaderBase
OffsetY = offsetY,
ParallaxX = parallaxX,
ParallaxY = parallaxY,
Data = data ?? Optional<Data>.Empty,
Data = data is null ? Optional.Empty : new Optional<Data>(data),
Properties = properties ?? []
};
}

View file

@ -8,8 +8,8 @@ namespace DotTiled.Serialization.Tmx;
public abstract partial class TmxReaderBase
{
internal Tileset ReadTileset(
Optional<string> parentVersion = null,
Optional<string> parentTiledVersion = null)
Optional<string> parentVersion = default,
Optional<string> parentTiledVersion = default)
{
var firstGID = _reader.GetOptionalAttributeParseable<uint>("firstgid");
var source = _reader.GetOptionalAttribute("source");
@ -18,7 +18,7 @@ public abstract partial class TmxReaderBase
if (source.HasValue && firstGID.HasValue)
{
// Is external tileset
var externalTileset = _externalTilesetResolver(source);
var externalTileset = _externalTilesetResolver(source.Value);
externalTileset.FirstGID = firstGID;
externalTileset.Source = source;
@ -136,7 +136,7 @@ public abstract partial class TmxReaderBase
});
if (!format.HasValue && source.HasValue)
format = Helpers.ParseImageFormatFromSource(source);
format = Helpers.ParseImageFormatFromSource(source.Value);
return new Image
{
@ -225,11 +225,11 @@ public abstract partial class TmxReaderBase
Probability = probability,
X = x,
Y = y,
Width = width.HasValue ? width : image?.Width ?? 0,
Height = height.HasValue ? height : image?.Height ?? 0,
Width = width.HasValue ? width.Value : image?.Width.GetValueOr(0) ?? 0,
Height = height.HasValue ? height.Value : image?.Height.GetValueOr(0) ?? 0,
Properties = properties ?? [],
Image = image is null ? Optional<Image>.Empty : image,
ObjectLayer = objectLayer is null ? Optional<ObjectLayer>.Empty : objectLayer,
Image = image is null ? Optional.Empty : image,
ObjectLayer = objectLayer is null ? Optional.Empty : objectLayer,
Animation = animation ?? []
};
}

View file

@ -8,7 +8,7 @@ public class Template
/// <summary>
/// If the template represents a tile object, this property will contain the tileset that the tile belongs to.
/// </summary>
public Optional<Tileset> Tileset { get; set; } = Optional<Tileset>.Empty;
public Optional<Tileset> Tileset { get; set; } = Optional.Empty;
/// <summary>
/// The object that this template represents.

View file

@ -34,25 +34,25 @@ public class Image
/// <summary>
/// The format of the image.
/// </summary>
public Optional<ImageFormat> Format { get; set; } = Optional<ImageFormat>.Empty;
public Optional<ImageFormat> Format { get; set; } = Optional.Empty;
/// <summary>
/// The reference to the image file.
/// </summary>
public Optional<string> Source { get; set; } = Optional<string>.Empty;
public Optional<string> Source { get; set; } = Optional.Empty;
/// <summary>
/// Defines a specific color that is treated as transparent.
/// </summary>
public Optional<TiledColor> TransparentColor { get; set; } = Optional<TiledColor>.Empty;
public Optional<TiledColor> TransparentColor { get; set; } = Optional.Empty;
/// <summary>
/// The image width in pixels, used for tile index correction when the image changes.
/// </summary>
public Optional<int> Width { get; set; } = Optional<int>.Empty;
public Optional<int> Width { get; set; } = Optional.Empty;
/// <summary>
/// The image height in pixels, used for tile index correction when the image changes.
/// </summary>
public Optional<int> Height { get; set; } = Optional<int>.Empty;
public Optional<int> Height { get; set; } = Optional.Empty;
}

View file

@ -54,12 +54,12 @@ public class Tile : HasPropertiesBase
/// <summary>
/// The image representing this tile. Only used for tilesets that composed of a collection of images.
/// </summary>
public Optional<Image> Image { get; set; } = Optional<Image>.Empty;
public Optional<Image> Image { get; set; } = Optional.Empty;
/// <summary>
/// Used when the tile contains e.g. collision information.
/// </summary>
public Optional<ObjectLayer> ObjectLayer { get; set; } = Optional<ObjectLayer>.Empty;
public Optional<ObjectLayer> ObjectLayer { get; set; } = Optional.Empty;
/// <summary>
/// The animation frames for this tile.

View file

@ -131,17 +131,17 @@ public class Tileset : HasPropertiesBase
/// <summary>
/// The Tiled version used to save the file in case it was loaded from an external tileset file.
/// </summary>
public Optional<string> TiledVersion { get; set; } = Optional<string>.Empty;
public Optional<string> TiledVersion { get; set; } = Optional.Empty;
/// <summary>
/// The first global tile ID of this tileset (this global ID maps to the first tile in this tileset).
/// </summary>
public Optional<uint> FirstGID { get; set; } = Optional<uint>.Empty;
public Optional<uint> FirstGID { get; set; } = Optional.Empty;
/// <summary>
/// If this tileset is stored in an external TSX (Tile Set XML) file, this attribute refers to that file.
/// </summary>
public Optional<string> Source { get; set; } = Optional<string>.Empty;
public Optional<string> Source { get; set; } = Optional.Empty;
/// <summary>
/// The name of this tileset.
@ -201,17 +201,17 @@ public class Tileset : HasPropertiesBase
/// <summary>
/// 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.
/// </summary>
public Optional<Image> Image { get; set; } = Optional<Image>.Empty;
public Optional<Image> Image { get; set; } = Optional.Empty;
/// <summary>
/// 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.
/// </summary>
public Optional<TileOffset> TileOffset { get; set; } = Optional<TileOffset>.Empty;
public Optional<TileOffset> TileOffset { get; set; } = Optional.Empty;
/// <summary>
/// Ths is only used in case of isometric orientation, and determines how tile overlays for terrain and collision information are rendered.
/// </summary>
public Optional<Grid> Grid { get; set; } = Optional<Grid>.Empty;
public Optional<Grid> Grid { get; set; } = Optional.Empty;
/// <summary>
/// Tileset properties.
@ -231,7 +231,7 @@ public class Tileset : HasPropertiesBase
/// <summary>
/// Used to describe which transformations can be applied to the tiles (e.g. to extend a Wang set by transforming existing tiles).
/// </summary>
public Optional<Transformations> Transformations { get; set; } = Optional<Transformations>.Empty;
public Optional<Transformations> Transformations { get; set; } = Optional.Empty;
/// <summary>
/// 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.